Añadir elementos inexistentes en la BD

Hay ocasiones en las que se quiere añadir elementos para que formen parte del formulario, aunque no pertenezcan a la tabla o VIEW que se vaya a editar. Esto es útil, por ejemplo, para recopilar datos adicionales que se pasarán a proceso de POST y modificar el comportamiento o hacer cálculos adicionales.

El método más sencillo es sobreescribir la función form($packedID = ''), añadiendo a la TableView que corresponda los FieldView que se quiera. Si el parámetro $packedID está vacío, se trata de una creación. Si el parámetro $packedID no está vacío, es una edición y la variable $packedID es la clave primaria (empaquetada):

El esquema de la función es:

protected function form($packedID = '')
{
    // ... Añadir aquí los elementos
    parent::form($packedID);
}

Ejemplo: añadir un FieldViewStringMap cuando estemos editando (no creando) un registro. Lo llamaremos '_prop'.

protected function form($packedID = '')
{
    if ($packedID) {
        $prop = new \zfx\FieldViewStringMap();
        $prop->setEditable(TRUE);
        $prop->setElementName('_prop');
        $opciones = [
            1 => 'Opción A',
            2 => 'Opción B',
        ];
        $prop->setMap($opciones);
        $this->tableView->addfieldView($prop, '_prop');
        $this->tableView->setLabels(array(
            '_prop' => 'Elegir opción para _prop:'
        ));
    }
    parent::form($packedID);
}

Pestañas

Por defecto el CRUD presenta un formulario de creación o de edición sin pestañas. En este documento se describe cómo crear pestañas y colocar en ellas diferentes elementos, como por ejemplo CRUDs de tablas relacionadas.

Funcionalidad básica

Para usar pestañas a la hora de crear/editar una tabla, añadir a la función initData() lo siguiente:

$this->vTemplateForm = 'zaf/' . \zfx\Config::get('admTheme') . '/crud-form-tabs';
$this->tabList       = array(
    'princ' => 'Parte de equipo',
    'int'   => 'Integrantes',
    'ext'   => 'Externos',
);
$this->tabForm       = 'princ';
$this->tabSelected   = 'princ';

La propiedad vTemplateForm es necesaria siempre para que se cargue la vista especial con soporte para pestañas.

El resto de parámetros dependerá de las pestañas:

  • tabList es un array con las pestañas que queremos crear;

  • tabForm indica qué pestaña va a contener el formulario principal;

  • tabSelected indica qué pestaña aparecerá seleccionada de forma predeterminada.

Ocultar pestañas al crear

Cuando se usan pestañas para mostrar tablas relacionadas, puede ser útil, para evitar confusiones a los usuarios, ocultar las pestañas durante la creación (ya que estarán en blanco) y solo mostrarlas durante la edición.

Añadir a la función initData() lo siguiente:

$this->tabHideOnAdd = TRUE;

Durante la creación, solo se mostrará la pestaña que indica la variable tabForm (en el ejemplo anterior, princ), mientras que durante la edición se mostrarán todas las pestañas.

Ocultar pestañas condicionalmente

Puede ocurrir que, dependiendo de cierta condición (por ejemplo de los datos almacenados en la tabla principal), se quiera activar u ocultar ciertas pestañas. El lugar apropiado para hacer esto es en la redefinición de la función setupViewForm(). En el siguiente ejemplo se añaden un par de CRUDs a la pestaña int solo si la tabla padre cumple cierto valor. Si no, la pestaña es suprimida.

protected function setupViewForm($packedID = '')
{
    parent::setupViewForm($packedID);
    // Solo si estamos en modo edición
    if ($packedID != '') {
        // Si el valor de la tabla principal 'finalizado' es 0, mostrar esto en la pestaña 'integrantes'
        if (\zfx\a($this->rvr->getRS(), 'finalizado') == 0) {
            $this->addFrmSectionRel($packedID, 'mec_parteeq_op_rel_mec_parteeq', 'PartesEquipoOpCrud', 'Operarios', 'int');
            $this->addFrmSectionRel($packedID, 'mec_parteeq_maq_rel_mec_parteeq', 'PartesEquipoMaqCrud', 'Máquinas', 'int');
        }
        // Si no, la pestaña de integrantes será borrada
        else {
            unset($this->viewData['_tabList']['int']);
        }
        // Colocamos CRUDs en otras pestañas
        $this->addFrmSectionRel($packedID, 'mec_parteeq_subop_rel_mec_parteeq', 'PartesEquipoSubopCrud', 'Operarios contratados', 'ext');
        $this->addFrmSectionRel($packedID, 'mec_parteeq_submaq_rel_mec_parteeq', 'PartesEquipoSubmaqCrud', 'Máquinas alquiladas', 'ext');
    }
}

Tablas relacionadas

Se puede añadir bloques para el mantenimiento de tablas relacionadas tanto en el propio formulario principal (al final) como en una pestaña separada.

Se hace sobrecargando la función setupViewForm() y usando addFrmSectionRel() o bien usando código propio.

Ejemplo: Los grupos de un usuario:

protected function setupViewForm($packedID = '')
{
    parent::setupViewForm($packedID);
    if ($packedID != '') {
        $this->addFrmSectionRel(
           $packedID,
           'zfx_user_group_relUser',
           'CrudConfiguracionGruposUsuario',
           'Grupos del usuario'
        );
    }
}

Aquí $packedID se usa para saber si se está en modo creación o edición. Normalmente editar una tabla relacionada solo tiene sentido cuando ya se ha creado una fila de la tabla padre. Si no está vacía, contiene el ID de la tabla padre (empaquetado).

Por defecto el bloque se coloca en la sección footer del formulario principal. Si se quiere colocar en una pestaña, después del título en la función SetupViewForm() se puede especificar la clave de la pestaña.

La función addFrmSectionRel básicamente añade a la sección que hemos especificado (por ejemplo footer o bien una pestaña) una vista crud-bootstrap-list con los siguientes datos:

  • controller: el controlador CRUD de la tabla hija (se le pasó por parámetros a la función addFrmSectionRel).

  • _title: El título a mostrar (también se pasó por parámetros).

  • _loc: Un localizador, en concreto el del CRUD de la tabla padre.

  • _url: Normalmente la vista crud-bootstrap-list calcula la URL para cargar por AJAX con el nombre del controlador + '/lst', pero aquí se le pasa /rel/$packedID/$relName/, donde:

    • $packedID es el ID (empaquetado) de la tabla padre

    • $relName es el nombre de la relación establecida (y que existe en el esquema), con SafeEncode.

La funcion rel() del CRUD de la tabla hija hace un lst() pero antes fabrica un filtro y lo aplica en virtud de la relación que se le pasa y el ID de la tabla padre que se le pasa.

Cómo averiguar la clave primeria de la tabla padre

A veces hace falta saber cuál es la clave primaria de la tabla padre. En el controlador CRUD se puede averiguar llamando primero a la función calcFilRel(), que calculará dicha clave y lo almacenará en la propiedad filRel en forma de array.

Usar un picker

Un picker se usa para elegir desde una tabla un registro y almacenar algún dato en otra tabla.

Llamaremos R a la tabla que recibe la información, o sea, la tabla donde almacenar (normalmente) el ID de la fila de la tabla donde seleccionamos (pick up) una fila.

Llamaremos S a la tabla donde estamos seleccionando una fila para después pasar información (normalmente la clave primaria) proveniente de esa fila a la tabla R. En el CRUD de R tene que aparecer:

$this->tableView->pickFromTable(
     'id_com_localizacion',  // Campo de R donde guardaremos la información
     'com_localizacion', // Nombre de S
      array(
             'fields' => array('denominacion'),
             'format' => '%1$s',
             'length' => 60
      ),
      \zfx\Config::get('rootUrl') . 'com-ajax/pickloc');

El tercer parámetro es un array con la información que necesitamos para representar el registro de S que hemos elegido. No confundir con el ID de dicho registro, que es lo que obtenemos. En el ejemplo anterior queremos mostrar el campo denominacion y una longitud máxima de 60. La clave format se refiere a una expresión que sprintf() entiende.

El cuarto y último parámetro es una URL de un controlador que saldrá en un pop-up para poder elegir de S. Ese controlador hace el trabajo habitual: dibuja una tabla, permite buscar, etc. llamando a su vez a su propio CRUD. Pero si usamos un controlador convencional se nos dibuja el menú, y nosotros no queremos eso, solo queremos los paneles de búsqueda y selección (y quizá añadir o editar). Por eso usaremos un controlador por ejemplo AJAX.

Para el ejemplo anterior cuya URL era rootURL/com-ajax/pickloc necesitamos un fichero en controllers llamado Ctrl_ComAjax.php que contenga:

<?php
include_once('Abs_AppAjaxController.php');
class Ctrl_ComAjax extends Abs_AppAjaxController
{
    protected function pickloc()
    {
        \zfx\View::direct('zaf/' . \zfx\Config::get('admTheme') . '/crud-bootstrap-search', array(
            '_title' => 'Localizaciones',
            '_controller' => 'ComPickLocalizacion',
            '_autoFocus' => true
        ));
        \zfx\View::direct('zaf/' . \zfx\Config::get('admTheme') . '/crud-bootstrap-list', array(
            '_controller' => 'ComPickLocalizacion'
        ));
    }

    // --------------------------------------------------------------------
}

Como se puede ver se necesita crear el controlador CRUD llamado Ctrl_ComPickLocalizacion, que es un controlador CRUD estándar, pero además tiene en su initData() lo siguiente:

$this->pickerTargetField = 'id_com_localizacion'; // Campo de R
$this->pickerSelectFields = array('id', 'denominacion'); // Campos de S
$this->pickerSetValueFormat = '%1$s';
$this->pickerSetDisplayFormat = '[[%2$s]]';

Aquí le estamos especificando que es un picker y que debería permitir elegir una fila, así como transmitirla adecuadamente:

  • pickerTargetField es el campo de R donde vamos a colocar la info proveniente de la fila seleccionada de S.

  • pickerSelectFields contiene los campos que vamos a querer usar de la fila seleccionada.

  • pickerSetValueFormat contiene un formato para sprintf() cuyo resultado lo colocaremos en el valor del campo de R que vamos a rellenar. El número corresponde al elemento de pickerSelectFields, el anterior array descrito.

  • pickerSetDisplayFormat contiene un formato para sprintf() cuyo resultado lo colocaremos para dibujar (representar) la fila que hemos escogido justo en el momento de escogerla. Normalmente coincidirá con lo elegido en pickFromTable().

Mejorar un picker filtrando a partir de otro campo

A veces se tiene un picker mostrando demasiada información y se querría filtrar a partir de un dato que ha sido introducido en el formulario, sin existir todavía en la base de datos.

El bootstrapper crud-bootstrap-list permite un parámetro llamado _getValFromSelector donde podemos especificar un selector (por ejemplo un tag input) y cuyo valor será pasado a la URL del controlador CRUD que va a invocar como parámetro.

Por ejemplo, supongamos un picker para trabajadores y queremos filtrar por la empresa a la que pertenecen. Teníamos un controlador AJAX:

class Ctrl_ComAjax extends Abs_AppAjaxController
{
    protected function picktrab()
    {
        \zfx\View::direct(\zfx\Config::get('admTheme') . '/crud-bootstrap-search', array(
            '_title'      => 'Trabajadores',
            '_controller' => 'ComPickTrabajador',
            '_autoFocus'  => true
        ));
        \zfx\View::direct(\zfx\Config::get('admTheme') . '/crud-bootstrap-list', array(
            '_controller' => 'ComPickTrabajador'
        ));
    }
...

Vamos a decirle que queremos leer el ID de la empresa del trabajador a partir de cierto control del formulario: cambiamos la última parte por:

...
        \zfx\View::direct(\zfx\Config::get('admTheme') . '/crud-bootstrap-list', array(
            '_controller' => 'ComPickTrabajador',
            '_getValFromSelector' => '#_DBPicker_k69k64k5fk65k6dk7l72k65k73k61_hid'
        ));
...

Donde

'#_DBPicker_k69k64k5fk65k6dk7l72k65k73k61_hid'

Lo hemos obtenido inspeccionando el input del que queríamos obtener el dato (en el ejemplo, es un input hidden de otro picker).

Ahora mismo al activar el picker, se activaría el controlador http://ruta/com-pick-trabajador/lst/xxxx donde xxxx es el valor del input, o sea:

$('#_DBPicker_k69k64k5fk65k6dk7l72k65k73k61_hid').val()

Como es un valor numérico, no es escapado.

Claro que pasar xxxx a lst() del CRUD no sirve de mucho, nos interesa tener otra función (la llamaremos lstfil) que filtre y luego liste. Entonces cambiamos la URL que llama a lst() por otra que llama a lstfil():

...
        \zfx\View::direct(\zfx\Config::get('admTheme') . '/crud-bootstrap-list', array(
            '_controller' => 'ComPickTrabajador',
            '_getValFromSelector' => '#_DBPicker_k69k64k5fk65k6dk7l72k65k73k61_hid',
            '_url' => \zfx\Config::get('rootUrl') . 'com-pick-trabajador/lstfil/'
        ));
...

Y creamos en el CRUD la función que necesitamos. Por ejemplo:

public function lstfil($par = '')
{
    $emp = (int)$par;
    if ($emp > 0) {
        $filtro = 
        "id IN (SELECT id_trabajador FROM p_trabajadores_empresas WHERE id_empresa = $emp)";
        $this->applySessionFilter("_pick_trab_fil_empresa", '_id_empresa', $emp, '', '', $filtro);
    }
    $this->lst();
}

Además en el ejemplo anterior, que es real, en initData() se limpiaba siempre este filtro, ya que el picker podría ser usado sin necesidad de filtrar y el filtro se queda en la sesión.

Para limpiar un filtro de sesión hay que llamar a applySessionFilter con los mismos parámetros pero un filtro en blanco ('').

Dependencias en el interior de formularios

En ocasiones se necesita establecer dependencias dentro de los formularios de tal forma que el contenido de un campo influya en otros campos, modificando sus contenidos, ocultándolos o mostrándolos.

Actualmente se hace escribiendo directamente en la propiedad interDeps del controlador CRUD la información que se necesita para activar el control de interdepedencias. Esta información está estructurada como un array y debe cumplir cierto formato.

Por ejemplo:

$this->interDeps = [
    \zfx\MapperFieldView::getMappedFieldName('id_srv_equipoestado') => [
        'type'   => 'select-one-from-group',
        'slaves' => [
            1 => \zfx\MapperFieldView::getMappedFieldName('modelo'),
            2 => \zfx\MapperFieldView::getMappedFieldName('fabricante'),
            3 => \zfx\MapperFieldView::getMappedFieldName('numserie')
        ],
        'onnull' => 'show-all'
        'req'    => 1
    ]
];

El array interDeps tiene como claves los nombres de los elementos que provocan una acción (lo que se denomina campo principal): en el ejemplo es un <select> que corresponde al campo id_srv_equipoestado. Se usa la función \zfx\MapperFieldView::getMappedFieldName() en los nombres de elementos porque no se usan los nombres de campo directamente, sino los nombres de campo codificados.

Para dicho elemento, o sea el campo principal, el valor define qué acción realizar. También se estructura como un array. El array contiene la clave type que indica qué comportamiento se va a seguir. En el ejemplo el comportamiento es select-one-from-group. El resto de claves del array dependerá del comportamiento especificado.

select-one-from-group

Cuando type es select-one-from-group, se establece la siguiente dependencia: hay un grupo de campos que permanecerán ocultos salvo uno de ellos. El campo que será visible viene determinado por el valor de otro campo que es un select.

La clave slaves que contiene una lista de campos y los valores que activan dichos campos como claves.

O sea, en el ejemplo propuesto, si en id_srv_equipestado, que es un <select>, se elige el 1, se mostrará el campo modelo y se ocultarán fabricante y numserie. Si se elige el 2, se mostrará fabricante y se ocultarán modelo y numserie, y análogamente con el 3. Si se elige nulo, se muestran todos.

La clave onnull indica qué pasa cuando el elemento que provoca la acción adopta el valor nulo. En el ejemplo la acción es show-all que significa mostrar todos los campos. Si se establece hide-all, se ocultarán todos los campos.

La clave req, si está a 1 (por defecto es 0) hará el campo que se muestra se considera requerido aunque no lo sea en la BD.

La clave extra, si está a 1 (por defecto es 0) hará que en vez de usar el valor del select para realizar las acciones, se usará el atributo data-extra de las opciones. Si el select se generó usando FieldViewStringMap, hay que usar el método setExtra para configurarlo.

select-subset-from-group

Cuando type es select-subset-from-group la dependencia consiste en mostrar un subgrupo de campos y dejar el resto ocultos a partir de cierto valor de otro campo que es un select.

La diferencia con select-one-from-group es que cada valor del array slaves es a su vez otro array. Ejemplo:

$this->interDeps = [
    \zfx\MapperFieldView::getMappedFieldName('id_srv_equipo') => [
        'type'   => 'select-one-from-group',
        'slaves' => [
            1 => [
                      \zfx\MapperFieldView::getMappedFieldName('horas_consumo'),
                      \zfx\MapperFieldView::getMappedFieldName('horas_contador')
                 ],
            2 => [    
                      \zfx\MapperFieldView::getMappedFieldName('km_consumo'),
                      \zfx\MapperFieldView::getMappedFieldName('km_contador')
                 ]
        ],
        'onnull' => 'show-all'
        'req'    => 1
    ]
];

select-makes-required

Cuando type es select-makes-required se establece la siguiente dependencia: un campo será requerido o no dependiendo del valor de otro campo que es un select.

La clave slaves es un array cuyas claves son los valores que adoptará el select y los valores los códigos de campo que serán requeridos.

La clave onnull indica que si el select adopta el valor nulo entonces será equivalente a cierto valor. Si no se quiere establecer ninguno se puede dejar a cero.

Ejemplo:

Cuando el valor del campo r_tension_alimentacion sea 2, entonces el campo o_tension_alimentacion será obligatorio, y opcional en el resto de casos:

$this->interDeps = [
    \zfx\MapperFieldView::getMappedFieldName('r_tension_alimentacion') => [
        'type'   => 'select-makes-required',
        'slaves' => [
            2 => \zfx\MapperFieldView::getMappedFieldName('o_tension_alimentacion')
        ],
        'onnull' => 0
    ]
];

change-set-js

Cuando type es change-set-js se establece la siguiente dependencia: el cambio en el valor del campo provocará cambiar el valor de otro u otros campos diferentes (los slaves), a través de una función JS. Para cada slave se puede definir una función JS diferente. Dichas funciones recibirán el valor del campo modificado y devolverán el valor del campo a establecer.

Las funciones no solo van a recibir el valor del campo modificado (campo principal), sino que también recibirán el valor de otros campos que, aunque no se hayan modificado, necesiten intervenir. Son los campos othervalues. Las funciones por lo tanto tendrán como paráemtro tanto el valor del campo modificado como el de los partners. Para estos últimos se hará pasando un objeto. Ejemplo:

$this->interDeps = [
    \zfx\MapperFieldView::getMappedFieldName('base_imponible') => [
        'type'     => 'change-set-js',
        'othervalues' => [
            'iva' => \zfx\MapperFieldView::getMappedFieldName('iva'),
            'ret' => \zfx\MapperFieldView::getMapperFieldName('retenciones')
        ],
        'slaves'   => [
            \zfx\MapperFieldView::getMappedFieldName('total') => 'setTotal';
        ],
        'onnull' => 0
    ]
];

Donde setTotal es una función JS con la siguiente implementación:

function setTotal(base, p)
{
   return (base + base*p.iva/100 - base*p.ret/100);
}

any-change-set-js

Es similar a change-set-js, pero solo tiene en cuenta othervalues, y se activa cuando cualquiera de ellos cambia, al contrario que change-set-js, que solo se activa cuando cambia el campo principal. De hecho en este caso no hace falta especificar campo principal.

La función JS solo recibe el objeto con los valores de othervalues; el valor de campo principal no es enviado a la función.

on-select-activate

El select, al tener un valor no nulo, el que sea, activa todos los esclavos.