Configurar una App
Desde el apartado de administración accederemos a aplicaciones Off Line y nos saldrá un grid con las aplicaciones que tengamos configuradas. Si no tenemos ninguna pulsamos sobre nuevo para acceder al formulario de datos principales del App.
Cualquier desarrollo de App Off Line implica una configuración previa del Framework, como la creación de los objetos que vamos a utilizar, sus vistas y su seguridad. Además requiere de unos conocimientos mínimos de HTML5 y de JavaScript.
En esta pantalla completaremos:
- El identificativo del App
- El Título
- La dirección de la Web Api.
- Una descripción de lo que realiza la aplicación
A partir de este momento ya estamos listos para trabajar.
Ahora deberemos seguir los siguientes pasos.
1 Crear las estructuras para la base de datos del dispositivo móvil
2 Crear las tablas o consultas para obtener los datos de vuelta
3 Definir el código o estilos que queremos incorporar
4 Definir las diferentes plantillas de la aplicación, diferenciando 7 tipos distintas:
- Editar: Formularios de edición
- Visualizar: Formularios de Visualización
- Grids: Formularios de resultados
- Menús: Menú lateral izquierdo
- Buscadores: Condiciones para buscar resultados
- Panel de Inicio: Panel de inicio
- Impresión: Pantallas especiales para impresión
Crear la estructura de base de datos
El primer concepto que hay que tener claro es que NO creamos como tal ninguna base de datos en el dispositivo móvil. Está se genera de forma automática basándose en una serie de vistas que le preparamos, es decir si queremos generar una tabla en el dispositivo móvil le indicaremos que vista se emplea para tal efecto, y él solo, comprobará el tipo de datos de los campos y creará una tabla con los mismos campos, longitudes y tipos que en la vista.
Veamos un ejemplo de cómo crear una tabla de clientes o cuentas en la base de datos del dispositivo móvil. Lo primero es tener un objeto ya creado por ejemplo Cuentas y asignarle una vista como la que hay a continuación:
select clientes_datos.IdCliente,clientes_datos.IdCliente as Codigo, Cliente, NumTelefono AS Telefono, NumFax AS Fax, E_Mail AS Mail, Web, Bloqueado as Bloq,Nivel,Padre,RazonSocial, MiCod, NIF, IdDelegacion, IdLista, FormaPago, Descuento, ProntoPago, NSerie, Direccion, Ciudad, Provincia, CodPostal, Pais, Extension, NumFax, CC.IdEmpleado , clientes_datos.FechaAlta, pers_NumEfectos.NumEfectos,
RecEquivalencia, 0 as regnuevo, 0 as regmodificado, 0 as regborrado
From Clientes_Datos inner join Clientes_Datos_Economicos CE on Clientes_Datos.IdCliente=CE.IdCliente
inner join Clientes_Datos_Comerciales CC on Clientes_Datos.IdCliente=CC.IdCliente
left join pers_NumEfectos on Clientes_Datos.idcliente=pers_NumEfectos.idcliente
where Clientes_Datos.IdTipo In (0,1) And Clientes_Datos.Bloqueado=0
Básicamente indicamos que campos queremos de la tabla de clientes, le hemos añadido además el número de efectos de pago que tiene pendientes como un campo de la propia tabla de clientes y tres campos numéricos inexistentes (regnuevo, regmodificado y regborrado) que me permitan controlar si he añadido un nuevo cliente, si lo he modificado o si lo he borrado. Además estoy excluyendo los clientes bloqueados.
Ya tenemos la vista veamos cómo proceder para crear nuestra primera tabla. Ahora solo tenemos que acceder a nuestra área de base de datos y darla de alta.
En la pantalla de alta de Base de datos introduciremos
- App: Identificador del App
- Idioma
- Colección: Nombre de la colección que vamos a utilizar
- Vista: Nombre de la vista de esa colección. En este caso la que acabamos de crear
- Criterio de búsqueda: Criterio que se aplicará cuando abramos un buscador sobre la colección seleccionada
- Order by: Como queremos que se ordenen los registros al presentarlos
- Criterio de sincronización parcial: Que registros llevaremos del servidor a la App si realizamos una sincronización parcial. En otras palabras como llevarse solo lo nuevo o lo modificado.
- Índices: Campos índice que queremos crear en el modelo de datos de la App
- Insertable: Indica si se añadirá de forma automática la opción de añadir en el interfaz del objetos
- Función insert: Función JavaScript alternativa para realizar la inserción.
Si lanzamos nuestra App y pulsamos el botón de sincronizar nos debería aparece una nueva tabla llamada Cuentas en el modelo de datos.
Para comprobarlo podemos utilizar el navegador Chrome y pulsando F12 acceder al apartado Resources. Ahí dentro de la base de datos de pedidos encontraremos nuestra nueva tabla Cuentas con los registros obtenidos.
Nota: La forma de Limitar los registros que queremos llevar a la base de datos remota es mediante la seguridad del objeto. Para ello ver apartado de seguridad de objetos por grupo.
Funciones JavaScript Integradas
A partir de este momento empezaremos a diseñar las plantillas de las distintas pantallas. Para ello es conveniente que conozcamos alguna de las funciones JavaScript integradas que podremos utilizar en el diseño de plantillas:
navigateTo(modo,objeto,filter, templateName, tableName,sql,defFields)
Indispensable para navegar entre pantallas. Permite cargar un tipo de formulario indicándole los siguientes parámetros
- Modo: edit, grid,view
- Objeto: Nombre del objeto
- Filter: Filtro para acceder al objeto
- TemplateName: Nombre de la plantilla que queremos mostrar. Se puede obviar si ese objeto en ese modo solo tiene una plantilla
- TableName: por si quisiésemos sobrescribir la tabla principal del objeto
- Sql:por si queremos cambiar la SQL de carga del objeto
- defField: Valores por defecto para heredar del padre por ejemplo: Idlinea={IdLinea}|IdCliente={IdCliente}
transferTo(modo,objeto,filter, templateName, tableName,sql,defFields)
Es igual que la función anterior pero no deja copia en el historial de navegación. Así podremos evitar que el botón atrás vuelva a esta página.
saveForm(ev,cllBck)
guarda el contenido del formulario
- ev:evento que provocó el guardado (variable de sistema event)
- cllBck: función opcional a la que podemos llamar si el saveForm devuelve true
insertReturn(lObjeto,lWhere)
Se utiliza en conjunto con la función saveForm. Si es un registro nuevo va a la página anterior si no irá al view correspondiente a la edición.
- Objeto: Nombre del objeto
- Where: Filtro para acceder al objeto
deleteForm
borra el objeto cargado y en consecuencia el registro de la tabla.
- ev:evento que provocó el guardado
- cllBck: función opcional a la que podemos llamar si el deleteForm devuelve true
setSQLDef (ahofield,maxSql,nullValue)
establece el valor en el campo indicado buscando un máximo basádose en una SQL que le indiquemos
- ahofield: campo sobre el que actual
- maxSql: Cadena SQL para obtener el máximo
- nullValue: Valor opcional en el caso de que la consulta anterior vuelva nulo.
setDefault(ahofield,valueID, defValue)
establece el valor en el campo indicado basandose en los defaults que se le hayan pasado a la pantalla en la llamada.
- ahofield: campo sobre el que actual
- valueID: campo pasado como default del que quiero obtener el valor
- defValue: valor alternativo en el caso de que el default sea nulo.
setMax (ahofield,valueSQL,filterSQL)
calcula el maximo id
- ahofield: campo sobre el que actual
- valueSQL: campo sobre el que tiene que leer y obtener el valor +1
- filterSQL: permite añadir un where opcional al cálculo del máximo.
setValue(ahofield,value)
establece el valor indicado en el campo indicado
- ahofield: campo sobre el que actual
- value: valor a establecer
loadF3(objeto,[whereObj],[lTemp])
Abre una pantalla de busqueda y selección
- Objeto: objeto que queremos abrir
- whereObj: filtro con la condición
- lTemp: Nombre de la plantilla a cargar.
deleteTouchStart(object,where,el,ev)
Elimina un elemento manteniendo pulsado el boton o la línea.
- Objeto: objeto que queremos borrar
- where: filtro con la condición
- el: elemento sobre el que se realiza la acción (this)
- ev: evento que lo dispara (event)
getDate()
devuelve la fecha actual con formato HTML5 que es yyyy-mm-dd
guid()
devuelve un id único
reloadPage()
Recarga la página actual.
LeeTabla(consulta,querySuccess,queryFail)
Realiza consultas sobre la base de datos.
- Consulta: Sql query a ejecutar.
- querySuccess: Función que recibirá los resultados en caso de éxito.
- queryFail: Función que recibirá el error en caso de fallo.
Ej:
function DameIdClienteProyecto(idproyecto){
var consulta = \"select idcliente from Proyectos where idproyecto = \" + idproyecto;
LeeTabla(consulta,DameIdClienteProyectoRet)
}
function DameIdClienteProyectoRet(tx, results){
var len=results.rows.length;
if(len>0){
var idcliente = results.rows.item(0).idcliente;
alert(idcliente); }
else{
alert('No se ha encontrado el cliente');
}
}
ExecuteSQL(consulta,querySuccess,queryFail)
Realiza acciones sobre la base de datos.
- Consulta: Sql query a ejecutar.
- querySuccess: Función que se ejecutará en caso de éxito.
- queryFail: Función que recibirá el error en caso de fallo.
getCoords(cllBack)
Obtiene las coordenadas del dispositivo y ejecuta la función cllBack. La función de retorno recibirá un parámetro position con las siguientes propiedades:
- position.coords.latitude
- position.coords.longitude
- position.coords.altitude
- position.coords.accuracy
- position.coords.altitudeAccuracy
- position.coords.heading
- position.coords.speed
- position.timestamp
Si no se indica función de retorno el dispositivo mostrará una alerta con el valor de dichas propiedades.
getPicture(myPicId,myWidth,myHeight,myQuality,typeCrop)
Obtiene una imágen usando la cámara del dispositivo.
- myPicId: Identificador de la imagen
- myWidth: Ancho máximo de la foto (en caso de no existir valor 1000)
- myHeight: Alto máximo de la foto (en caso de no existir valor 1000)
- myQuality: Calidad de la foto (en caso de no existir valor 50) (Rango entre 0 y 100)
- typeCrop: Si se completa este campo, tras realizar la foto aparecerá un asistente para recortar la imágen (Valores: 'circle' o 'square')
getGalleryPicture(myPicId,myWidth,myHeight,myQuality,typeCrop)
Obtiene una imágen seleccionandola a través de la galeria del dispositivo.
- myPicId: Identificador de la imagen
- myWidth: Ancho máximo de la foto (en caso de no existir valor 1000)
- myHeight: Alto máximo de la foto (en caso de no existir valor 1000)
- myQuality: Calidad de la foto (en caso de no existir valor 50) (Rango entre 0 y 100)
- typeCrop: Si se completa este campo, tras realizar la foto aparecerá un asistente para recortar la imágen (Valores: 'circle' o 'square')
getPicMenu(myPicId,myWidth,myHeight,myQuality,typeCrop)
Abre un menú que permite escoger si la imágen se obtendrá de la camara o del dispositivo.
- myPicId: Identificador de la imagen
- myWidth: Ancho máximo de la foto (en caso de no existir valor 1000)
- myHeight: Alto máximo de la foto (en caso de no existir valor 1000)
- myQuality: Calidad de la foto (en caso de no existir valor 50) (Rango entre 0 y 100)
- typeCrop: Si se completa este campo, tras realizar la foto aparecerá un asistente para recortar la imágen (Valores: 'circle' o 'square')
scanCode(callback, orientation, formats)
Permite escanear un código de barras/QR usando la cámara del dispositivo.
- callback: función de retorno. Recibe un único parametro con un objeto con las siguientes propiedades:
text: texto contenido dentro del código.
format: formato del código escaneado.
Si no se espeficia se mostrará un cuadro de dialogo con el resultado. - orientation: Orientación de la cámara. Por defecto modo vertical. Valores: (portrait|landscape)
- formats: Si se desea se puede limitar el dispositivo para escanear solo un tipo de código. Si no se especifica será valido para cualquier tipo.
Formatos soportados:
Android
QR_CODE, DATA_MATRIX, UPC_E, UPC_A, EAN_8, EAN_13, CODE_128, CODE_39, CODE_93, CODABAR, ITF, RSS14, RSS_EXPANDED, PDF417, AZTEC
iOS
QR_CODE, DATA_MATRIX, UPC_E, UPC_A, EAN_8, EAN_13, CODE_128, CODE_39, ITF
syncSube(cllback)
Realiza un envío de datos. Se puede especificar una función de vuelta que se ejecutará al terminar el envío.
syncCompleta(cllback)
Realiza una sincronización completa. Se puede especificar una función de vuelta que se ejecutará al terminar.
syncParcial(cllback)
Realiza una sincronización parcial. Se puede especificar una función de vuelta que se ejecutará al terminar.
sendMail(to,subject,body,ishtml,cc,bcc)
Envia emails con cuerpo en html.
- to: receptor del email.
- cc: receptor del email en copia.
- bcc: receptor del email en copia oculta.
- subject: Asunto del email.
- body: cuerpo del email
- ishtml: si el cuerpo del email está en html. (Valores: 'true' o 'false')
Marcadores soportados para el cuerpo del email en html
|
createPDF(data,filename,documentsize,landscape,type)
Genera un pdf desde una plantilla HTML.
- data: plantilla HTML.
- filename: nombre con el que se guardará el pdf.
- documentsize: formato. (Valores: \"A4\" o \"A3\")
- landscape: portrait o landscape.
- type: “share” abre el visor de PDF nativo.
Definición de plantillas
En definitiva es un formulario único que nos permite crear plantillas para los ditintos tipos de pantallas:
- Menú
- Panel de inicio
- Grid
- Buscador
- Visualización
- Edición
- Impresión
Los campos a completar a la hora de rellenar una plantilla son:
- App: Identificador del App
- Idioma
- Objeto: Nombre de objeto
- Nombre: Nombre de la plantilla
- Modo: En cuál de los siete modos vamos a utilizar la plantilla (edit, grid, view, menú, inicio,buscador, print)
- Título pantalla: Título que aparecerá en la pantalla en cuestión. Es parseable cuando proceda utilizando {}
- Cabecera HTML: opción de introducir HTML5 como marcado en la cabecera.
- Template HTML: opción de introducir HTML5 como cuerpo.
- Pie HTML: opción de introducir HTML5 como marcado en el pie.
- Botonera: podemos pegar un marcado que represente botones que aparecerán en la parte superior de la plantilla
- SplitView: Indica si nuestro diseño utiliza splitview o pantalla dividida, solo aconsejable si trabajamos con tablets
- Posición: Posición dentro del splitview (izquierda o derecha) en el que visualizaremos la plantilla
Menús
Toda aplicación necesita un menú por lo que crearemos una plantilla de tipo menú con las opciones que queremos en el menú:
Seleccionaremos el objeto Inicio y el modo menú. Agregaremos un título y completaremos la plantilla HTML con un contenido.
Este contenido donde básicamente utilizamos la función NavigateTo
<li onclick=\"navigateTo('grid','Pers_Pedido')\"><span class=\"km-icon km-cart\"></span>Pedidos</li>
<li onclick=\"navigateTo('grid','Cuenta','nivel=\\'0\\'','gridClientes',null,null,'nivel=0')\"><span class=\"km-icon km-contacts\"></span>Clientes</li>
<li onclick=\"navigateTo('grid','Cuenta','numEfectos>0','gridClientesEfectos')\"><span class=\"km-icon km-organize\"></span>Efectos</li>
<li onclick=\"navigateTo('grid','Pers_Articulo')\"><span class=\"km-icon km-play\"></span>Stock (on line)</li>
<li onclick=\"navigateTo('grid','Pers_Stock')\"><span class=\"km-icon km-play\"></span>Stock</li>
Produce el siguiente resultado:
Panel de inicio.
Podemos diseñar un panel de inicio para cuando arranque nuestro App. Para ello seleccionaremos el objeto Inicio y el modo panel Inicio. Agregaremos un título y completaremos la plantilla HTML con un contenido.
Este contenido donde básicamente utilizamos la función NavigateTo
<div style=\"width:75%;max-width:400px;margin:0 auto;margin-top:5em;text-align:center;\">
<img onclick=\"navigateTo('grid','Pers_Pedido')\" style=\"text-align:center;height:75px;\" src=\"data:image/png;base64,
Marcadoimagenbase64\"/>
<br/><br/>
<img onclick=\"navigateTo('grid','Cuenta','nivel=\\'0\\'','gridClientes',null,null,'nivel=0')\" style=\"text-align:center;height:75px;\" src=\"data:image/png;base64, Marcadoimagenbase64\"/>
<br/><br/>
<img onclick=\"navigateTo('grid','Cuenta','numEfectos>0','gridClientesEfectos')\" style=\"text-align:center;height:75px;\" src=\"data:image/png;base64,Imagenbaase64\"/>
<br/><br/>
<img onclick=\"navigateTo('grid','Pers_Articulo')\" style=\"text-align:center;height:75px;\" src=\"data:image/png;base64,ImagenBase64\"/>
<br/><br/>
<div>
Para las imágenes utilizamos su definición en base64, para ello podemos utilizar webs como http://www.base64-image.de/ donde subimos la imagen y nos dan la definición base64 de esta.
El resultado sería el que se muestra a continuación:
Grids
Podemos diseñar grids para los objetos que queramos seleccionaremos el objeto en cuestión y el modo grid, agregaremos un título y completaremos la plantilla HTML con un contenido.
Este contenido donde básicamente utilizamos la función NavigateTo
<li onclick=\"navigateTo('view','Cuenta','IdCliente=\\'{idcliente}\\'','viewClientes')\">{cliente} ({idcliente})</li>
En este caso estamos indicando que cuando se haga click navegue a la plantilla viewClientes. ya que podríamos tener varias plantillas de visualización distintas.
Recordar que los filtro si son alfanumericos deben ir entre comitas (‘) y escapados con una barra (\\’).
El resultado sería el que se muestra a continuación:
El buscador va condicionado a la cadena de busqueda que se haya puesto al definir la tabla del objeto.
Grids Online
Existe la posibilidad de que la consulta de un objeto sea Online, en este caso no hace falta traerse ninguna tabla al dispositivo. Para definir un grid Online deberemos crear un objeto distinto con su correspondiente vista, e indicar que se va a tratar online, como es el caso siguiente donde podemos consultar los Stocks online. Obviamente esta opción solo funcionará cuando tengamos conexión de datos.
Plantillas de visualización
Podemos diseñar pantallas de visualización para los objetos que queramos, para ello seleccionaremos el objeto y el modo view, agregaremos un título y completaremos la plantilla HTML con un contenido.
Este contenido donde básicamente utilizamos la función navigateTo y los marcadores entre llaves para posicionar los valores de las propiedades del objeto. Alguno de esto marcadores permiten el formateado como por ejemplo:
- Los de tipo Booleano:{RecEquivalencia|bool:Sí,No}
- Los de tipo Fecha:{FechaAlta|date:formato}
- Los de tipo Decimal:{Importe|decimal:Numero de decimales}
<ul data-role=\"listview\" data-style=\"inset\" class=\"km-widget km-listview km-list km-listinset\">
<li style=\"text-align:right;\">
<img style=\"float:left\" src=\"data:image/png;base64,Imagen en base64\"/>
<span style=\"font-size:1.5em;float:none;\"> {IdCliente} {Cliente}</span>
</li>
<li>
<label>{RazonSocial}</label><span>{NIF}</span>
</li>
<li>
<label>Teléfono</label><span><a href=\"tel:{Telefono}\">{Telefono}</a></span>
</li>
<li>
<label>E-Mail</label><span><a href=\"mailto:{Mail}\">{Mail}</a></span>
</li>
<li>
<label>Dirección</label><span><a href=\"http://maps.google.es/maps?q={Direccion}\">{Direccion}</a></span>
</li>
<li>
<label>Población</label><span>{Ciudad}</span>
</li>
<li>
<label>C.P.</label><span>{CodPostal}</span>
</li>
<li>
<label>Recargo Equivalencia</label><span>{RecEquivalencia|bool:Sí,No}</span>
</li>
</ul>
<ul data-role=\"listview\" data-style=\"inset\" class=\"km-widget km-listview km-list km-listinset\">
<li onclick=\"navigateTo('grid','Cuenta','padre=\\'{idcliente}\\'','gridPlantas',null,null,'Padre={IdCliente}|nivel=1')\">Plantas<span class=\"km-icon km-globe\"></span></li>
<li onclick=\"navigateTo('grid','Contacto','idcliente=\\'{idcliente}\\'',null,null,null,'IdCliente={IdCliente}')\">Contactos<span class=\"km-icon km-contacts\"></span></li>
<li onclick=\"navigateTo('grid','Pers_Efecto','idcliente=\\'{idcliente}\\' and sustituido=0','gridEfectos',null,null,'IdCliente={IdCliente}')\">Efectos<span class=\"km-icon km-organize\"></span></li>
<li onclick=\"navigateTo('grid','Pers_Pedido','idcliente=\\'{idcliente}\\'','gridPedidos',null,null,'IdCliente={IdCliente}')\">Pedidos<span class=\"km-icon km-cart\"></span> </li>
</ul>
Recordar que los filtro si son alfanumericos deben ir entre comitas (‘) y escapados con una barra (\\’).
El resultado sería el que se muestra a continuación:
En este caso ademas hemos añadido un botón para ir a la pantalla de edición rellenando el campo botonera con el sigueinte valor:
<a href=\"#\" class=\"km-button\" onclick=\"event.preventDefault();navigateTo('edit','Cuenta','Idcliente=\\'{IdCliente}\\'','editCuenta')\"><span class=\"km-icon km-edit\"></span></a>
Nota: Event.preventDefault evita que se dispare la opción href tras la ejecución del onclick
Plantillas de edición
Podemos diseñar pantallas de edición para los objetos que queramos, para ello seleccionaremos el objeto y el modo edit, agregaremos un título y completaremos la plantilla HTML con un contenido.
Este contenido es el más rico y complejo ya que utilizaremos gran parte de las funciones JavaScript estándar para formatear e inicializar los valores. Lo primero que debemos tener en cuenta es que se trata de un formulario por lo que todo deberá ir encerrado dentro de la etiqueta <form>
Utilizaremos los distinstos tipos de inputs de HTML5 para diseñar el formulario con una serie de metadatos especificos nuestros y los tipicos del input:
- data-ahofield: para definir a que campo de la tabla va ligado
- data-ahodef: para definir el valor por defecto en el caso de que sea un registro nuevo
- data-ahosql: para definir el contenido de un desplegable para un campo
- value: Para asignar el valor
- Required: Para indicar que es requerido
- Step: En caso decimal indicar el paso (0.001) tres decimales
- ReadOnly: si es un campo de solo lectura
- Class: Indicar la clase del fichero de estilos css
- Style: Para indicar un estilo fijo
<form>
<ul data-role=\"listview\" data-style=\"inset\" class=\"km-widget km-listview km-list km-listinset\">
<li>
<label>Nombre
<input type=\"text\" data-ahofield=\"Cliente\" value=\"{Cliente}\" required />
</label>
</li>
<li>
<label>Razón Social
<input type=\"text\" data-ahofield=\"RazonSocial\" value=\"{RazonSocial}\" required />
</label>
</li>
<li>
<label>Dirección
<input type=\"text\" data-ahofield=\"Direccion\" value=\"{Direccion}\" required />
</label>
</li>
<li>
<label>Población
<input type=\"text\" data-ahofield=\"Ciudad\" value=\"{Ciudad}\" required />
</label>
</li>
<li>
<label>CP
<input type=\"number\" data-ahofield=\"CodPostal\" value=\"{CodPostal}\" required />
</label>
</li>
<li>
<label>NIF
<input type=\"text\" data-ahofield=\"NIF\" value=\"{NIF}\" required />
</label>
</li>
<li>
<label>Teléfono
<input type=\"text\" data-ahofield=\"Telefono\" value=\"{Telefono}\" required />
</label>
</li>
<li>
<label>E-Mail
<input type=\"text\" data-ahofield=\"Mail\" value=\"{Mail}\" required />
</label>
</li>
<li>
<label>Recargo de equivalencia
<input type=\"checkbox\" data-ahofield=\"RecEquivalencia\" {RecEquivalencia|bool:checked} class=\"km-icon km-check\" />
</label>
</li>
</ul>
<ul data-role=\"listview\" data-style=\"inset\" class=\"km-widget km-listview km-list km-listinset\">
<li style=\"text-align:center\" >
<button onclick=\"saveForm(event,function(){keyopen}insertReturn('Cuenta','IdCliente=\\'' + $('[data-ahofield=\\'IdCliente\\']').val()+'\\''){keyclose})\" class=\"km-button\" title=\"Guardar\" ><span class=\"km-icon icon-save km-notext\"></span></button>
<button onclick=\"deleteForm(event)\" class=\"km-button\" title=\"Delete\"><span class=\"km-icon km-delete km-notext\"></span></button>
</li>
</ul>
<div style=\"display:none\">
<input type=\"text\" data-ahofield=\"Padre\" data-ahodef=\"setDefault('Padre','Padre')\" value=\"{Padre}\" readonly />
<input type=\"text\" data-ahofield=\"IdCliente\" data-ahodef=\"setMax('IdCliente','IdCliente')\" value=\"{IdCliente}\" readonly />
<input type=\"number\" data-ahofield=\"regnuevo\" data-ahodef=\"setValue('regnuevo',1)\" value=\"{regnuevo}\" />
<input type=\"number\" data-ahofield=\"regmodificado\" data-ahodef=\"setValue('regmodificado',1)\" value=\"1\" />
<input type=\"text\" data-ahofield=\"nivel\" data-ahodef=\"setDefault('nivel','nivel',0)\" value=\"{nivel}\" />
<input type=\"date\" data-ahofield=\"fechaalta\" data-ahodef=\"setValue('fechaalta',dameFecha())\" value=\"{fechaalta}\" />
</div>
</form>
Nota: Recordar que los filtro si son alfanumericos deben ir entre comitas (‘) y escapados con una barra (\\’).
Nota: Cuando tengamos que poner llaves de Javascript dentro de un marcado deberemos sustituirlos por {keyopen} y {keyclose}.
Nota: Event.preventDefault evita que se dispare la opción href tras la ejecución del onclick.
El resultado sería el que se muestra a continuación:
Veamos otro ejemplo:
<form>
<ul data-role=\"listview\" data-style=\"inset\" class=\"km-widget km-listview km-list km-listinset\">
<li>
<label>Cliente
<input type=\"text\" id=\"Cliente\" onclick=\"loadF3('Cuenta','nivel=0')\" data-ahofield=\"Cliente\" value=\"{Cliente}\" required onkeydown=\"event.preventDefault();\" />
</label>
</li>
<li>
<label>Planta
<input type=\"text\" id=\"Planta\" onclick=\"loadF3('Cuenta','padre=\\''+$('#IdCliente').val()+'\\'')\" data-ahofield=\"Planta\" value=\"{Planta}\" onkeydown=\"event.preventDefault();\" />
</label>
</li>
<li>
<label>Observaciones
<input type=\"text\" data-ahofield=\"Observaciones\" value=\"{Observaciones}\" />
</label>
</li>
<li>
<label>Descuento
<input type=\"number\" id=\"Descuento\" data-ahofield=\"Descuento\" value=\"{Descuento}\" required />
</label>
</li>
<li>
<label>Pronto Pago
<input type=\"number\" Id=\"ProntoPago\" data-ahofield=\"ProntoPago\" value=\"{ProntoPago}\" required />
</label>
</li>
<li>
<label>Fecha
<input type=\"date\" data-ahofield=\"Fecha\" value=\"{Fecha|date:YYYY-MM-DD}\" data-ahodef=\"setValue('Fecha',dameFecha())\" required />
</label>
</li>
<li>
<label>Nº Pedido Cli
<input type=\"number\" data-ahofield=\"IdPedidoCli\" value=\"{IdPedidoCli}\" />
</label>
</li>
<li>
<label>Serie
<select id=\"SeriePedido\" data-ahofield=\"SeriePedido\" data-ahosql=\"select Descrip, SerieFactura from series order by descrip\" value=\"{SeriePedido}\" data-ahodef=\"setValue('SeriePedido','0')\"\" required />
</label>
</li>
<li >
<label>Forma de Pago
<select id=\"FormaPago\" data-ahofield=\"FormaPago\" data-ahosql=\"select descrip,idFormaPago from formas_Pago order by descrip\" value=\"{FormaPago}\" required />
</label>
</li>
</ul>
<ul data-role=\"listview\" data-style=\"inset\" class=\"km-widget km-listview km-list km-listinset\">
<li >
<button onclick=\"saveForm(event,function(){keyopen}insertReturn('Pers_Pedido','IdPedido=' + $('[data-ahofield=\\'IdPedido\\']').val()){keyclose})\" class=\"km-button\" title=\"Guardar\" style=\"float:right;\" ><span class=\"km-icon icon-save km-notext\"></span></button>
<button onclick=\"saveForm(event)\" class=\"km-button\" title=\"Delete\" style=\"float:left;\"><span class=\"km-icon km-delete km-notext\"></span></button>
</form>
<input id=\"IdPedido\" style=\"display:none;\" type=\"text\" data-ahofield=\"IdPedido\" data-ahodef=\"setMax('IdPedido','IdPedido')\" value=\"{IdPedido}\" readonly required />
<input id=\"IdCliente\" style=\"display:none;\" type=\"text\" data-ahofield=\"IdCliente\" value=\"{IdCliente}\" required />
<input id=\"IdPlanta\" style=\"display:none;\" type=\"text\" data-ahofield=\"IdPlanta\" value=\"{IdPlanta}\" />
<input id=\"IdPlanta\" style=\"display:none;\" type=\"text\" data-ahofield=\"IdLista\" value=\"{IdLista}\" data-ahodef=\"setValue('IdLista','100')\" />
Su resultado sería:
Vemos que en este ejemplo además se ha implementado la opción del buscador F3 para el cliente. O la de la planta que además le pasa como filtro el cliente seleccionado previamente. Este loadF3 se basa en un buscador que veremos a continuación.
Buscadores
Como hemos visto en el punto anterior, es interesante poder diseñar pantallas emergentes que sirvan para buscar y seleccionar partiendo de un campo. Para ello crearemos una plantilla de tipo buscador.
Al crearlo indicaremos el objeto que queramos, el modo buscador, agregaremos un título y completaremos la plantilla HTML con un contenido.
<li onclick=\"funSetClientePlanta('{IdCliente}','{Cliente}','{nivel}','{FormaPago}','{Descuento}','{ProntoPago}')\">{Cliente} ({IdCliente})</li>
Este contenido tiene una función personalizada que me permite hacer cosas a la hora de seleccionar un cliente. En este caso quiero pasarle a mi función distintos campos del cliente para que pueda pegarlos en el formulario. En el siguiente punto veremos donde podemos definir nuestra función personalizada.
Definir código y estilos adicionales
Otro de los apartados a la hora de diseñar APP es la parte de programación. Obviamente se trata de programación JavaScript y JQuery, y hay un apartado específico para ello que además permite incluir otros elementos HTML 5 como clases para estilos etc.
Para ello en el apartado de cabeceras y código crearemos una entrada nueva indicando el contenido y el nombre de la página. En este caso deberá llamarse Index.html
EL contenido de la función funSetClientePlanta pega los campos que se le pasan en el formulario que está activo y luego cierrra la ventana modal.
<script>
//PEDIDOS
function funSetClientePlanta(IdCliente,Cliente,nivel,FormaPago,Descuento,ProntoPago){
if (nivel!=1){
$('#IdCliente').val(IdCliente);
$('#Cliente').val(Cliente);
}
else
{
$('#IdPlanta').val(IdCliente);
$('#Planta').val(Cliente);
}
$('#FormaPago').val(FormaPago);
$('#Descuento').val(Descuento);
$('#ProntoPago').val(ProntoPago);
//cerramos la ventana modal que se llama popover-people
$(\"#popover-people\").data(\"kendoMobilePopOver\").close();
}
</script>
Existe también la opción de personalizar el final de la sincronización. Para ello deberemos indicar como nombre de la página sincronizar.html y la función debe llamarse persAfterSync, que se ejecutará si la sincronización ha finalizado con éxito.
Existe también un persBeforeSync que se ejecutará antes de realizar la sincronización.
Existe también persBeforeSend que se ejecuta antes de enviar los datos
Existe también persAfterSend que se ejecuta después de enviar los datos. En el ejemplo a continuación borramos una serie de tablas una vez completado el envío.
<script>
function persAfterSync(completa)
{
//despues de cada sincronización
}
// cuando termina de enviar datos si ha funcionado se ejecuta persAfterSend()
function persAfterSend()
{
EjecutaSQL('delete from pedidos_lineas');
EjecutaSQL('delete from pedidos');
EjecutaSQL('delete from efectos where regnuevo<>0 and regmodificado<>0');
EjecutaSQL('delete from cuentas where regnuevo<>0');
}
</script>
Plantillas de impresión
Esta opción se implementará en futuras versiones
Crear las consultas de vuelta
Otro proceso fundamental es como crear el proceso de vuelta de los datos nuevos o manipulados. Para ello accedemos al apartado de datos de vuelta e indicaremos que datos queremos traer y que Stored Procedure de nuestro servidor se va a encargar de procesarlos.
Esta parte es la más complicada y crítica de todo el proceso de generación de App, ya que hay que tener un conocimiento exhaustivo de donde queremos introducir la información que recibamos del App.
Básicamente deberemos indicar que información de que tablas queremos devolver, con qué condiciones, en qué orden y que proceso ejecutaremos una vez finalizada la sincronización.
Para ello indicaremos:
- App: Identificador de la aplicación
- Orden: Orden en que se realiza la sincronización
- SQL: Instrucción SQL sobre la base de datos del App
- Tabla: Nombre de la tabla temporal resultante
- Stored Fin: Nombre de la Stored procedure que se ejecutará una vez finalizado todo el proceso de sincronización.
Para que la sincronización de vuelta se pueda llevar acabo deberemos tener en nuestro modelo de datos la sigueinte Stored Procedure pNet_offline_Sync
Crete Procedure [dbo].[pNet_offline_sync]
@SyncGUID nvarchar(max),
@StoredRet nvarchar(150)
as
BEGIN
declare @Json nvarchar(max)
select @Json='{'+jsonvalue+'}' from net_offline_sync where convert(nvarchar(max),syncGUID)=@SyncGUID
DECLARE @TabPropiedades JSONHierarchy
insert into @TabPropiedades
select * from dbo.funParseJson(@Json)
declare @Fields as nvarchar(max)
declare @Ids as nvarchar(max)
declare @name as nvarchar(max)
declare CursorPivot CURSOR for
SELECT
name,
'Select top 1 @FieldsRET=left(S,len(s)-1) from (SELECT parent_id,replace(replace(convert(varchar(max),(SELECT convert(varchar(max),name)
FROM @TabPropiedades A WHERE A.parent_id = B.parent_id FOR XML PATH(''name''), TYPE)),''<name>'',''''),''</name>'','','') S
FROM @TabPropiedades B where parent_id in (select object_id from @TabPropiedades where parent_id ='+convert(varchar,object_id)+')) Oper' as Fields,
'Select distinct @IdsRET=left(S,len(s)-1) from(
SELECT replace(replace(convert(varchar(max),(SELECT convert(varchar(max),parent_id)
FROM (select distinct parent_id from @TabPropiedades A where parent_id in (select object_id from @TabPropiedades where parent_id ='+convert(varchar,object_id)+') ) X
FOR XML PATH(''parent_id''), TYPE)),''<parent_id>'',''''),''</parent_id>'','','') S
) Oper' as Ids
FROM @TabPropiedades where parent_id =(select object_id FROM @TabPropiedades where parent_id is null)
OPEN CursorPivot
FETCH NEXT FROM CursorPivot INTO @name,@Fields, @Ids
declare @SqlPivot as nvarchar(max)
set @sqlpivot=''
DECLARE @ParmDefinition nvarchar(500);
WHILE @@FETCH_STATUS = 0 BEGIN
declare @FieldsRet as nvarchar(max)
declare @IdsRET as nvarchar(max)
SET @ParmDefinition = N'@TabPropiedades JSONHierarchy readonly, @FieldsRet varchar(max) OUTPUT';
EXECUTE sp_executesql @fields, @ParmDefinition, @tabpropiedades = @tabpropiedades, @FieldsRet=@FieldsRet OUTPUT;
SET @ParmDefinition = N'@TabPropiedades JSONHierarchy readonly, @IdsRET varchar(max) OUTPUT';
EXECUTE sp_executesql @Ids, @ParmDefinition, @tabpropiedades = @tabpropiedades, @IdsRET=@IdsRET OUTPUT;
set @SqlPivot=@SqlPivot+'
SELECT parent_ID,'+@FieldsRET+'
INTO #'+@name+'
FROM
(SELECT parent_ID,name,stringValue
FROM @TabPropiedades where parent_ID in ('+@IdsRET+') ) p
PIVOT
(
MAX (stringValue)
FOR NAME IN ('+@FieldsRET+')
) AS pvt
ORDER BY parent_ID;'
FETCH NEXT FROM CursorPivot INTO @name,@Fields, @Ids
END
CLOSE CursorPivot
DEALLOCATE CursorPivot
declare @partes int
set @SqlPivot = @SqlPivot +';exec '+@StoredRet+''''+@SyncGUID+''''
SET @ParmDefinition = N'@TabPropiedades JSONHierarchy readonly';
EXECUTE sp_executesql @SqlPivot, @ParmDefinition, @tabpropiedades = @tabpropiedades;
return -1
END
Y la siguiente tabla encargada de almacenar la información que le llega de cada App y de hacer las funciones de Log Net_Offline_Sync:
CREATE TABLE [dbo].[Net_Offline_Sync](
[IdSync] [int] IDENTITY(1,1) NOT NULL,
[IdApp] [nvarchar](50) NOT NULL,
[FechaSync] [smalldatetime] NOT NULL CONSTRAINT [DF_Net_Offline_Sync_FechaSync] DEFAULT (getdate()),
[Usuario] [nvarchar](150) NOT NULL,
[JsonValue] [nvarchar](max) NOT NULL,
[Finalizado] [bit] NOT NULL CONSTRAINT [DF_Net_Offline_Sync_Finalizado] DEFAULT ((0)),
[Error] [bit] NOT NULL CONSTRAINT [DF_Net_Offline_Sync_Error] DEFAULT ((0)),
[ErrorDesc] [nvarchar](max) NULL,
[SyncGUID] [uniqueidentifier] NOT NULL,
[IdEmpleado] [int] NOT NULL,
CONSTRAINT [PK_Net_Offline_Sync] PRIMARY KEY CLUSTERED
(
[IdSync] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
A partir de aquí ya solo nos quedaría crea nuestra stored procedure de procesado de la vuelta y que tendrá el nombre que hayamos puesto en la lista de sincronizaciones:
Aquí tenemos un ejemplo de cómo podría ser esta Stored para una funcionalidad concreta:
Create PrOCEDURE [dbo].[pPersNet_offline_Pedidos_Efectos]
@SyncGUID nvarchar(max)
as
BEGIN
-- Ojo en todos los updates cambiar la fecha de insertupdate
DECLARE @IdLinea int ,@Fecha smalldatetime
DECLARE @vRet int,@ERRORMSG nvarchar(max)=''
DECLARE @IdLista T_id_lista ,@IdEmpleado T_id_empleado
-- Las tablas tienen un campo de regnuevo y regmodiifcado si los dos están a 0 no hay que hacer nada con los regisros
BEGIN TRAN
BEGIN TRY
--Net_offline_sync
--Obtengo empleado que realiza la sincronización
select @IdEmpleado=IdEmpleado from Net_Offline_Sync where SyncGUID=@SyncGUID
--CUENTAS
IF (SELECT object_id('tempdb..#cuentas')) IS NOT NULL BEGIN
delete from PersWP8_Clientes where idEmpleado=@IdEmpleado
IF EXISTS (select * from #cuentas where regnuevo<>0) BEGIN
INSERT INTO PersWP8_Clientes ([IdEmpleado] ,[IdCliente] ,[Cliente] ,[RazonSocial] ,[Direccion] ,[Ciudad] ,[CodPostal],[Nif] ,[NumTelefono] ,[E_Mail] ,[FechaAlta] ,[RecEquivalencia] ,[Padre] ,[Nuevo])
select @IdEmpleado,Idcliente, cliente,Razonsocial,Direccion,Ciudad,CodPostal,NIf,telefono,Mail,REPLACE (FechaAlta,'-',''), RecEquivalencia,case when Padre='' then Null else Padre end , 1
from #cuentas where regnuevo<>0
END
IF EXISTS (select * from #cuentas where regnuevo=0 and regmodificado <>0) BEGIN
INSERT INTO PersWP8_Clientes ([IdEmpleado] ,[IdCliente] ,[Cliente] ,[RazonSocial] ,[Direccion] ,[Ciudad] ,[CodPostal],[Nif] ,[NumTelefono] ,[E_Mail] ,[FechaAlta] ,[RecEquivalencia] ,[Padre] ,[Nuevo])
select @IdEmpleado,Idcliente, Cliente,Razonsocial,Direccion,Ciudad,CodPostal,NIf,telefono,Mail,REPLACE (FechaAlta,'-',''), RecEquivalencia,case when Padre='' then Null else Padre end, 0 from #cuentas where regnuevo=0 and regmodificado <>0
END
END
--PEDIDOS
IF (SELECT object_id('tempdb..#pedidos')) IS NOT NULL BEGIN
delete from PersWP8_Pedidos where idEmpleado=@IdEmpleado
IF EXISTS (select * from #pedidos ) BEGIN
--PRINT 'PEDIDOS'
INSERT INTO PersWP8_Pedidos ([IdEmpleado],[IdPedido],[IdCliente],[DescripcionPed],[IdPedidoCli],[Fecha],[SeriePedido],[Descuento],[ProntoPago],[FormaPago],[IdLista],[Observaciones],[FechaInsertUpdate])
select @IdEmpleado,[IdPedido],[IdCliente],[DescripcionPed],[IdPedidoCli],REPLACE (Fecha,'-',''),[SeriePedido],[Descuento],[ProntoPago],[FormaPago],[IdLista],[Observaciones],GETDATE() from #pedidos where Idplanta =''
INSERT INTO PersWP8_Pedidos ([IdEmpleado],[IdPedido],[IdCliente],[DescripcionPed],[IdPedidoCli],[Fecha],[SeriePedido],[Descuento],[ProntoPago],[FormaPago],[IdLista],[Observaciones],[FechaInsertUpdate])
select @IdEmpleado,[IdPedido],IdPlanta,[DescripcionPed],[IdPedidoCli],REPLACE (Fecha,'-',''),[SeriePedido],[Descuento],[ProntoPago],[FormaPago],[IdLista],[Observaciones],GETDATE() from #pedidos where Idplanta <>''
END
END
--PEDIDOS_LINEAS
IF (SELECT object_id('tempdb..#pedidos_lineas')) IS NOT NULL BEGIN
delete from PersWP8_Pedidos_Lineas where idEmpleado=@IdEmpleado
IF EXISTS (select * from #pedidos_lineas ) BEGIN
--PRINT 'PEDIDOS_LINEAS'
INSERT INTO PersWP8_Pedidos_Lineas ([IdEmpleado],[IdPedido],[IdLinea] ,[IdArticulo],[Descrip],[Cantidad],[Precio],[DtoLP1],[DtoLP2],[Descuento],[Observaciones],[FechaInsertUpdate])
select @IdEmpleado,[IdPedido],[IdLinea] ,[IdArticulo],[Descrip],[Cantidad],[Precio],[DtoLP1],[DtoLP2],isnull([Descuento_linea],0),[Observaciones], convert(smalldatetime,GETDATE()) from #pedidos_lineas
END
END
-- LANZAR STORED PARA PROCESAR PEDIDOS
EXEC @vRet=pPersWP8_ProcesarPedidos @IdEmpleado
if @vRet<>-1 raiserror('ERROR PROCESANDO LOS CLIENTES/PEDIDOS.',12,1)
--EFECTOS
IF (SELECT object_id('tempdb..#efectos')) IS NOT NULL BEGIN
IF EXISTS (select * from #efectos where regnuevo<>0 or regmodificado <>0) BEGIN
--PRINT 'EFECTOS'
Insert Into Efectos_Temporal (IdDocEfecto, FormaCobro, ImporteCobrado, FechaCobro,Sustituido,Tipo,Importe,IdDocsPadres)
select [IdDoc],formacobro,ImporteCobrado,REPLACE (FechaCobro,'-',''),sustituido,tipo,importeTotal,IdDocsPadres from #efectos where regnuevo<>0 or regmodificado<>0
END
END
UPDATE Net_offline_sync SET Finalizado=1 where SyncGUID =@SyncGUID
-- se borrar lo sincronizado aqui
delete from PersWP8_Clientes where IdEmpleado=@IdEmpleado
delete from PersWP8_Pedidos where IdEmpleado=@IdEmpleado
delete from PersWP8_Pedidos_Lineas where idEmpleado=@IdEmpleado
COMMIT TRAN
return -1
END TRY
BEGIN CATCH
IF @@TRANCOUNT >0 BEGIN
ROLLBACK TRAN
END
UPDATE Net_offline_sync SET Error=1 , ErrorDesc=ERROR_MESSAGE() , Finalizado=1 where SyncGUID =@SyncGUID
DECLARE @CatchError NVARCHAR(MAX)
SET @CatchError=dbo.funImprimeError(ERROR_MESSAGE(),ERROR_NUMBER(),ERROR_PROCEDURE(),@@PROCID ,ERROR_LINE())
RAISERROR(@CatchError,12,1)
RETURN 0
END CATCH
END
Modo Debug
Desde el dispositivo podemos lanzar el modo Debug manteniendo pulsada la barra administración del menú unos 5 segundos.
Esto hará que aparezca el siguiente menú:
Este menú nos permitirá:
- Ver el código HTML de la página
- Acceder a la BD del dispositivo y realizar consultas SQL sobre ella
- Lanzar un depurador de JavaScript
- Recargar solo las plantillas de la aplicación
¿Le ha sido útil este artículo?
¡Qué bien!
Gracias por sus comentarios
¡Sentimos mucho no haber sido de ayuda!
Gracias por sus comentarios
Sus comentarios se han enviado
Agradecemos su esfuerzo e intentaremos corregir el artículo