martes, 24 de abril de 2012

Creando un dashboard

Creando un dashboard en una home page para un rol



El resultado final de la home queda así:


Y haciendo un drill-down sobre la primera columna:







Para generar los gráficos, usamos la librería Highcharts.
La estrategia es la siguiente:
1) Creamos una home page dinamica y le asignamos un rol. En la home page, incluimos el CSS y los javascript necesarios para que funcione el Higcharts.


2) Creamos un javascript especifico para esta home page, que cuando este listo el DOM traerá los datos via ajax.


3) Creamos un presentation, donde se implementan los métodos de ajax que llama el javascript en la parte 2)


Home Page Dinámica:


<?php
if(!class_exists('home_terminales'))
{
class home_terminales {

public function Render($context)
{
global $sess;
$html = '
<script type="text/javascript" src="'.WEB_PATH.'/common/Highcharts-2.1.9/js/highcharts.js"></script>

<script type="text/javascript" src="'.WEB_PATH.'/includes/homepage/comp/home_terminales.js"></script>

<style>
.botones {text-align:left; width:950px;margin:0 auto;}
#phome {text-align:left; width:950px; margin:0 auto; min-height:800px;}
#shortcuts {display:none;}
#graf_eventos {width:950px;height:320px;margin-top:10px;}
#graf_sitios {width:950px;height:320px;;margin-top:10px;}
#botones {border:solid 1px #ddd; border-radius:5px; padding:10px;text-align:right;height:50px;}
#navegador {font: 18px Helvetica,Arial;font-weight:bold;float:left;}
#fechas, #totales {font: 12px Helvetica,Arial;float:left;clear:left;}
#volver {display:none;}
#cambio_fecha {display:inline;}
</style>
<style media="print">
#botones button, #volver, #main {display:none;}
</style>

<div class="botones">
</div>

<div id="phome">
<div id="botones">
<div id="navegador"></div>
<div id="fechas"></div>
<div id="totales"></div>
<div id="cambio_fecha">
<button onclick="setDias(1)">Dia</button>
<button onclick="setDias(7)">Semana</button>
<button onclick="setDias(15)">Quincena</button>
<button onclick="setDias(30)">Mes</button>
<button onclick="setDias(180)">Semestre</button>
</div>
<button class="btn_imprimir" onclick="print()">Imprimir</button>
<button  id="volver" onclick="goHome()">Volver atrás</button>
</div>
<div id="graf_eventos"></div>
<div id="graf_sitios"></div>
</div>
';
$content["home_terminales"] = $html;
$content["upload"] = "";
return array( $content, array() );
}
}
}



Como se ve, la home page es sencilla. Sigue la convención de estar sobre /includes/homepage/comp y el archivo llamarse como la clase que implementa, en este caso, 'home_terminales.php'


Luego vamos a saltar al presentation, quien se encarga de buscar los datos que luego se usan para hacer los gráficos. La misión es bien especifica, generar un objeto JSON con los datasets.



<?php
 include_once "common/cdatatypes.php";

class CDH_HOME_TERMINALES_AJAX extends CDataHandler 
{
function __construct($parent) 
{
    parent::__construct($parent);
}

function render_eventos($p) {
global $terminales_db;
$total = 0;
$bars = array();
$sql = "select count(*), organismo from log l join terminales t on l.ip=t.ip where fecha>=(NOW() - INTERVAL {$p} DAY) group by organismo order by 1 desc";
$rs = $terminales_db->do_execute($sql);
while( $row = $terminales_db->_fetch_row($rs) ) {
$bars[] = array("cant"=>intval($row[0]), "organismo"=>$row[1]);
$total+=intval($row[0]);
}
$pie = array();
$sql = "select count(*), sitio from log l join terminales t on l.ip=t.ip where fecha>=(NOW() - INTERVAL {$p} DAY) group by sitio order by 1";
$rs = $terminales_db->do_execute($sql);
while( $row = $terminales_db->_fetch_row($rs) ) {
$pie[] = array("cant"=>intval($row[0]), "organismo"=>$row[1]);
}
return json_encode((object) Array("bars"=>$bars, "pie"=>$pie, "total"=>$total));
}


function render_eventos_tiempo($p) {
global $terminales_db;
list($dias, $terminal) = explode("|",$p);
$total=0;
$spline = array();
$sql = "select count(*), UNIX_TIMESTAMP(DATE(fecha)) from log l join terminales t on l.ip=t.ip where  t.organismo='{$terminal}' and fecha>=(NOW() - INTERVAL {$dias} DAY) group by TO_DAYS(fecha) order by 2";
$rs = $terminales_db->do_execute($sql);
while( $row = $terminales_db->_fetch_row($rs) ) {
$spline[] = array("cant"=>intval($row[0]), "fecha"=>intval($row[1]));
$total+=intval($row[0]);
}

$pie = array();
$sql = "select count(*), sitio from log l join terminales t on l.ip=t.ip where t.organismo='{$terminal}' and fecha>=(NOW() - INTERVAL {$dias} DAY) group by sitio order by 1";
$rs = $terminales_db->do_execute($sql);
while( $row = $terminales_db->_fetch_row($rs) ) {
$pie[] = array("cant"=>intval($row[0]), "organismo"=>$row[1]);
}
return json_encode((object) Array("spline"=>$spline, "pie"=>$pie, "total"=>$total));
}

}


En este caso, hay dos funciones declaradas, la primera genera el Dashboard inicial, y la segunda genera la vista de drill-down. En este problema en particular, la consulta a la base de datos es trivial. Esa es la parte donde hay que evaluar con mucho cuidado si conviene usar la base transaccional o pensar un esquema de datamart, donde los resultados esten precalculados por otro script que se ejecuta cada X tiempo.


Finalmente, se junta una cosa con otra mediante el javascript que se ejecuta al completar el DOM.


var chart1 = null;
var chart2 = null;
var dias = 7;
var drilling = "";


$(document).ready(function() {


Highcharts.setOptions({
  global: {
     useUTC: false
  }
});


setDias(7);
});


function actualiza_eventos() {


var j = new rem_request(this,function(obj,json){
var jdata = eval("("+json+")");

$("#totales").html("Total eventos: "+jdata.total);
$("#cambio_fecha").show();


chart1=null;
var options = {
       chart: {
          renderTo: 'graf_eventos',
          defaultSeriesType: 'column',
          height: 300,
          width: 950
       },
       title: {
          text: 'Eventos por terminal'
       },
       xAxis: {
           categories: ['Terminal']
       },
       yAxis: {
          title: {
             text: 'Eventos'
          }
       },
       series: [],
       credits: { enabled: false },
       plotOptions: {
           column: {
              cursor: 'pointer',
              point: { events: {'click':drill_eventos} }
           }               
        }
    };


var bars = jdata.bars;
for( var k=0; k<bars.length; k++) {
var n = bars[k].organismo;
var v = bars[k].cant;
options.series.push( {"name":n, "data":[v], dataLabels: {
enabled: true,
rotation: 0,
color: '#444444',
align: 'right',
x: -3,
y: -3,
formatter: function() {
return this.y;
},
style: {
font: 'normal 13px Verdana, sans-serif'
}
} } );
}

chart1 = new Highcharts.Chart(options);


chart2=null;

var options = {
       chart: {
          renderTo: 'graf_sitios',
          defaultSeriesType: 'pie',
          height: 300,
          width: 950
       },
       title: {
          text: 'Eventos por sitio'
       },
       plotOptions: {
           pie: {
              allowPointSelect: true,
              cursor: 'pointer',
              dataLabels: {
                 enabled: true,
                 color: '#000000',
                 connectorColor: '#000000',
                 formatter: function() {
                    return '<b>'+ this.point.name +'</b>: '+ Highcharts.numberFormat(this.percentage, 0) +' %';
                 }
              }
           }
       },
       series: [{"name":"pie", "type":"pie", "data":[] }],
       credits: { enabled: false },
       tooltip: {
           formatter: function() { 
            return Highcharts.numberFormat(this.y, 0)+ " eventos";
           }
        }
    };


var pie = jdata.pie;
for( var k=0; k<pie.length; k++) {
var n = pie[k].organismo;
var v = pie[k].cant;
options.series[0].data.push( {"name":n, "y":v} );
}

chart2 = new Highcharts.Chart(options);


},"HOME_TERMINALES_AJAX","render_eventos",dias);
}




function setDias(cant) {

dias = cant;
chart1=null;
chart2=null;
actualiza_eventos();

if(cant==1) {
$("#navegador").html("Datos de las últimas 24hs.");
var hoy = new Date();
$("#fechas").html( date_to_string(hoy) );
}
if(cant==7) {
$("#navegador").html("Datos de los últimos 7 días.");
var hasta = new Date();
var desde = new Date();
desde.setDate(desde.getDate()-7);
$("#fechas").html( date_to_string(desde) + " al " + date_to_string(hasta) );
}
if(cant==15) {
$("#navegador").html("Datos de los últimos 15 días.");
var hasta = new Date();
var desde = new Date();
desde.setDate(desde.getDate()-15);
$("#fechas").html( date_to_string(desde) + " al " + date_to_string(hasta) );
}
if(cant==30) {
$("#navegador").html("Datos de los últimos 30 días.");
var hasta = new Date();
var desde = new Date();
desde.setDate(desde.getDate()-30);
$("#fechas").html( date_to_string(desde) + " al " + date_to_string(hasta) );
}
if(cant==180) {
$("#navegador").html("Datos de los últimos 6 meses.");
var hasta = new Date();
var desde = new Date();
desde.setDate(desde.getDate()-180);
$("#fechas").html( date_to_string(desde) + " al " + date_to_string(hasta) );
}
}


function date_to_string( d) {
var e = "DomLunMarMieJueVieSab";
return e.substr(d.getDay()*3, 3) + " " + d.getDate() + "/" + (d.getMonth()+1.0) + "/" + d.getFullYear();
}


function drill_eventos(e) {

drilling = e.point.series.name;

var j = new rem_request(this,function(obj,json){
var jdata = eval("("+json+")");

$("#totales").html("Total eventos: "+jdata.total);
$("#volver").show();
$("#cambio_fecha").hide();


chart1 = null;
var options = {
       chart: {
          renderTo: 'graf_eventos',
          defaultSeriesType: 'spline',
          height: 300,
          width: 950
       },
       title: {
          text: 'Eventos de terminal '+drilling
       },
       xAxis: {
        type: 'datetime',
           tickPixelInterval: 150
       },
       yAxis: {
          title: {text: 'Eventos'},
          plotLines: [{
              value: 0,
              width: 1,
              color: '#808080'
           }]
       },
       series: [{name:"Eventos", data:[] }],
       credits: { enabled: false },
       tooltip: {
           formatter: function() {
          
            return Dia(Highcharts.dateFormat('%a', this.x)) + " " + Highcharts.dateFormat('%d-%m-%Y', this.x) +'<br/>Eventos: '+ Highcharts.numberFormat(this.y, 2);
           }
        }
    };


var spline = jdata.spline;
for( var k=0; k<spline.length; k++) {
var t = spline[k].fecha; //Formato unix timestamp
var v = spline[k].cant;
options.series[0].data.push( {'x':t*1000,'y':v} );
}

chart1 = new Highcharts.Chart(options);


chart2=null;

var options = {
       chart: {
          renderTo: 'graf_sitios',
          defaultSeriesType: 'pie',
          height: 300,
          width: 950
       },
       title: {
          text: 'Eventos por sitio de ' + drilling
       },
       plotOptions: {
           pie: {
              allowPointSelect: true,
              cursor: 'pointer',
              dataLabels: {
                 enabled: true,
                 color: '#000000',
                 connectorColor: '#000000',
                 formatter: function() {
                    return '<b>'+ this.point.name +'</b>: '+ Highcharts.numberFormat(this.percentage, 0) +' %';
                 }
              }
           }
       },
       series: [{"name":"pie", "type":"pie", "data":[] }],
       credits: { enabled: false },
       tooltip: {
           formatter: function() { 
            return Highcharts.numberFormat(this.y, 0)+" eventos";
           }
        }
    };


var pie = jdata.pie;
for( var k=0; k<pie.length; k++) {
var n = pie[k].organismo;
var v = pie[k].cant;
options.series[0].data.push( {"name":n, "y":v} );
}

chart2 = new Highcharts.Chart(options);


},"HOME_TERMINALES_AJAX","render_eventos_tiempo",dias+"|"+drilling);
}




function Dia(eng) {
var i = "MonTueWedThuFriSatSun";
var e = "LunMarMieJueVieSabDom";
return e.substr(i.indexOf(eng, 0), 3);
}


function goHome() {
$("#volver").hide();
setDias(dias);
}


En este caso, los datos que llegan por JSON son la información pura. Toda la data para formatear los gráficos, se declara en este objeto Javascript.


Recomiendo ver los ejemplos de Highchart sobre la página de la librería y eventualmente cortar y pegar el ejemplo.


http://www.highcharts.com/demo/

viernes, 13 de abril de 2012

Gestión de home pages por roles

Cuando se esta trabajando en un proyecto donde cada rol debe tener una home page diferente, se implementan de esta manera.

Organización:
Crear tantos roles como home pages diferentes se van a utilizar. Llamar a cada rol home_<identificador>
Por ejemplo:

home_administrador
home_operador
home_supervisor

Luego se le asignan estos roles a los usuarios. Si se asigna mas de un rol home_ a un usuario, la página inicial será cualquiera de las asignadas.

Template homepage.htm (carpeta /www/templates)
Ejemplo de un template sencillo, parte del body.


<body onload="onPageLoad()">
<div id="main">
<a class="logo" href="/servi/index.php"></a>
        <div id="appmenu"></div>
        
        <div id="user"><com:homepage.comp.mini_who/></div>
<div id="tools">
          <com:homepage.comp.who_servi/>
          <div class="version"><com:includes.homepage.comp.soft_version/></div>
          <com:homepage.comp.gestionadora_sidebar/>
      </div>
  </div>

<div id="contenido">  
            <com:homepage.comp.dynhome/>
</div>
</body>

En el template, solo hay que incluir el componente para mostrar las paginas home dinamicas.

La implementación de cada pagina, se hace en la carpeta /www/includes/homepage/comp
Por cada rol creado, hay que hacer un archivo PHP que se llame igual que el rol, mas otro mas llamado home_default.php que se usará en aquellos casos donde el usuario no tenga un rol home_<?> asignado.

Cada archivo, aparte de llamarse como el rol, debe instanciar una clase que se llame como el rol, y declarar el método Render(). 

Ejemplo archivo home_despacho.php

<?php
if(!class_exists('home_despacho'))
{
class home_despacho
{
public function Render($context)
{
                        $html = 'Mi contenido de homepage;
                        
                        $content["home_despacho"] = $html;
$content["upload"] = "";
return array( $content, array() );
}
}
}
?>


El método Render() debe retornar un array con contenido y errores (que a su vez son dos arrays).

El array de contenido llamado "$content" debe incluir por lo menos un elemento, con el nombre del rol.