Gestión de las Vacaciones (actualización)

Es impresionante lo rápido que envejecen los desarrollos.

El que existía en la versión anterior tenía por objetivo explicar cómo se podían integrar las librerías de JavaScript DayPilot y AmCharts, el problema, es que ambas eran de pago en la parte funcional de representar un gráfico de Recursos.
En esta ocasión utilizo la librerías de FullCalendar, que no son de pago y son muy utilizadas en los desarrollos de PHP.

Objetivo

  • Hacer un desarrollo para que los trabajadores de una organización puedan solicitar sus vacaciones.
  • Con el perfil Administrador, se especifica el periodo de las vacaciones que se pueden solicitar.
  • Calcula los días naturales, los festivos, los fines de semana y los laborales.
  • Que todas las peticiones estén integradas en un sólo aplicativo y que todas ellas se puedan observar gráficamente por agrupaciones diversas.
  • Que los «Jefes» o «Responsables» de los trabajadores puedan aprobar dichas vacaciones. La petición sin aprobar, gráficamente salen en color «negro» y las aprobadas salen en el color del empleado.
  • Que todos los usuarios puedan consultar sus vacaciones y la de sus compañeros, con el fin de organizar las actividades.

DEMO: https://fhumanes.com/vacaciones/

Usuarios: admin/admin y humanes/humanes. Podéis dar de alta nuevos usuarios, pero por favor, no cambiéis estos 2.

Solución Técnica

Como he indicado, he utilizado la librería FullCalendar para la representación gráfica.

Soy consciente que este aplicativo, para gestionar los horarios y/o permisos del personal es insuficiente, pero creo que tiene muy poco código, para aquellos que empiezan y que tiene aspectos de programación que pueda interesar integrarlo en otros proyecto, como son:

  • Los cálculos de días de fin de semana, festivos naturales y laborales.
  • Representación gráfica utilizando FulCalendar.
  • Modificación dinámica del menú del aplicativo (si eres Jefe o no).
  • Permisos a los registros dependiendo del usuario conectado.

A modo de ejemplo, explico cómo funciona la librería FullCalendar. Cada gráfico se divide en 3 partes:

1.- La parte de definición del gráfico, su configuración, eventos, etc.

2.- El código AJAX de solicitar los recursos del gráficos (los grupos y las personas de estos grupos).

3.- El Código AJAX de solicitar las peticiones de Vacaciones por cada uno de los recursos en el periodo de visualización en que está en gráfico. Es decir, sólo se entregan las peticiones el periodo que se está visualizando. Según se varia de periodo el gráfico, se van recargando los datos.

4.- Deseaba representar en el «calendario» os días que corresponde a fin de semana (fue sencillo encontrar ejemplos en internet – CSS personalizado), pero no encontré cómo poner fondo diferente a los días festivos.
Conseguí entender que cada vez que repinta el calendario informa que ha terminado en el evento «eventSourceSuccess» y a partir de ahí con JQUERY cambio el fondo de los días festivos.

1.- «grupo_grafico.php»:

<?php
global $conn;

$language = 'es';
$firstDay = 1;
$idgrupo_trabajo = $data['idgrupo_trabajo'];
$grupo = $data['Titulo'];  // Recogemos el código de Grupo
$_SESSION['grupo'] = $grupo; // Lo poneos en variable de sessión para los procesos Ajax
unset($_SESSION['jefe']);		 // Para que no se confundan los procesos AJAX

$now = substr(now(),0,10);
if (isset($_SESSION['q_fromDate'])) { $now = substr($_SESSION['q_fromDate'],0,10); }
// Holidays the system
$holidays = array();
$rs = DB::Query("select * from festivos");
while( $data = $rs->fetchAssoc() ) {
  $holidays[] = $data['DiaFiesta'];
}
$holidays = "['" . implode("','", $holidays) . "']";

$popup = <<<EOT
eventClick: function(info) {
    var peticion = info.event.id;
    var title = info.event.title;
    var url = "peticion_view.php?editid1="+peticion;
    var header = '<h2 data-itemtype="view_header" data-itemid="view_header" data-pageid="10">'+'Petición: '+ title+'</h2>' ;
    var popup = Runner.displayPopup( {
      url: url,
      header: header,
      width: 1000,
      height: 500,
      footer: '<a href="user_grupo_view.php?editid1=$idgrupo_trabajo&" onclick="window.win.close();return false;">Close window</a>',
      afterCreate: function(popup) {
        window.popup = popup;
      }
    });
  },

EOT;

$str1 = <<<EOD
 <style>
    /*
    html, body {
      margin: 0;
      padding: 0;
      font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
      font-size: 12px; 
    }
    */
    .fc-sat, .fc-sun {
    background-color: #FBE4FD !important;
    }
    #calendar {
      max-width: 100%;
      max-height: 200px;
      font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
      font-size: 12px;
      /* margin: 40px auto; */
    }
  </style>


<link href='fullcalendar/packages/core/main.css' rel='stylesheet' />
<link href='fullcalendar/packages-premium/timeline/main.css' rel='stylesheet' />
<link href='fullcalendar/packages-premium/resource-timeline/main.css' rel='stylesheet' />
<script src='fullcalendar/packages/core/main.js'></script>
<script src='fullcalendar/packages/interaction/main.js'></script>
<script src='fullcalendar/packages-premium/timeline/main.js'></script>
<script src='fullcalendar/packages-premium/resource-common/main.js'></script>
<script src='fullcalendar/packages-premium/resource-timeline/main.js'></script>

<script src='fullcalendar/packages/core/locales-all.js'></script>
<script src='fullcalendar/tooltip/tooltip.min.js'></script>

<script>
  var initialLocaleCode = '$language';
  var firstDay = '$firstDay'; 
  
  document.addEventListener('DOMContentLoaded', function() {
    var calendarEl = document.getElementById('calendar');

    var calendar = new FullCalendar.Calendar(calendarEl, {
      plugins: [ 'interaction', 'resourceTimeline' ],
      now: '$now',
      
      // timeZone: 'UTC+1', // the default 'local' (unnecessary to specify)
      locale: initialLocaleCode,
       // locale: 'es',
      
       height: 550,
      editable: false,
      aspectRatio: 1.8,
      scrollTime: '00:00',
      header: {
        left: 'today prev,next',
        center: 'title',
        right: 'resourceTimelineMonth,resourceTimelineYear'
      },
      defaultView: 'resourceTimelineMonth',
      navLinks: true,
      resourceAreaWidth: '15%',
      resourceLabelText: 'Empleados',
      //
      $popup
      //
      resourceLabelText: 'Empleados',
      resources: { // you can also specify a plain string like 'json/resources.json'
        url: 'timeline_ajax_resource.php'},
      // Holidays
      eventSourceSuccess: function(content, response) {
          console.log('Refresh data');
          const Holidays = $holidays;
          Holidays.forEach(myHolidays);
          return content.eventArray;
      },

      events: { // you can also specify a plain string like 'json/events-for-resources.json'
        url: 'timeline_ajax_event.php'},
    });

    calendar.render();
  });

// Holidays Put color with JQUERY
function myHolidays(value, index, array) {
    let tag = 'td[data-date="'+value+'"]';
    $(tag).css("background-color", "#DDF8F3");
}

</script>
  <div id='calendar'></div>

EOD;

$value = $str1;
?>

2.- «timeline_ajax_resource.php». Este proceso sirve para los 3 gráficos:

<?php
require_once("include/dbcommon.php"); // DataBase PHPRunner

if (isset($_SESSION['grupo'])) {
    $grupo = $_SESSION['grupo']; // Grupo que estamos accediendo
} elseif (isset($_SESSION['jefe'])) {
    $jefe = $_SESSION['jefe'];
}
  else {
    $user_idusuario = $_SESSION["user_idusuario"]; // Usuario que estamos accediendo
}

// Require our Event class and datetime utilities
require 'fullcalendar/php/utils.php';


$language = 'es';

if (isset($grupo)) { // Accediendo por Grupo
$sql = "
SELECT
grupo_trabajo.`Titulo`,
usuario.`idusuario`,
usuario.`Login`,
usuario.`NombreyApellido`,
usuario.`Color`
FROM grupo_trabajo
JOIN usuario_grupo ON (usuario_grupo.`grupo_trabajo_idgrupo_trabajo` = grupo_trabajo.`idgrupo_trabajo`)
JOIN usuario ON (usuario.`idusuario` = usuario_grupo.`usuario_idusuario`)
WHERE grupo_trabajo.`Titulo` = '$grupo'
ORDER BY usuario.`NombreyApellido`
";
}
if (isset($user_idusuario)) { // Accediendo por Usuario
$sql = "
SELECT
'Empresa' Titulo,
usuario.`idusuario`,
usuario.`Login`,
usuario.`NombreyApellido`,
usuario.`Color`
FROM usuario
WHERE usuario.`idusuario` = $user_idusuario
";
}
if (isset($jefe)) { // Accediendo por Jefe
$sql = "
SELECT
'Empresa' Titulo,
idusuario, 
NombreyApellido, 
Color 
FROM usuario
WHERE Jefe_id = $jefe
";
}


$rs = DB::Query($sql);
// Accumulate an output array of event data arrays.
// $output_arrays = array();
$str = '[';

$group ='';
while( $data = $rs->fetchAssoc() ){
 
    If ($group <> $data['Titulo']) {
        if ($group <> '' ) {
            $str = substr($str,0,strlen($str)-1); // erase last ","
            $str .= '  ] },';
        }
        $str .= '{ "id": "'.$data['Titulo'].'", "title": "'.$data['Titulo'].'", "children": [';
        $group = $data['Titulo'];
        
    } 
    $str .= '{ "id": "'.$data['idusuario'].'", "title": "'.$data['NombreyApellido'].'", "eventColor": "'.$data['Color'].'" },';
    
}
$str = substr($str,0,strlen($str)-1); // erase last ","
$str .= '  ] } ]';

echo $str;
// Send JSON to the client.
// $str = json_encode($output_arrays);
// echo json_encode($output_arrays);
?>

3.- «timeline_ajax_event.php». Este proceso sirve para los 3 gráficos:

<?php
require_once("include/dbcommon.php"); // DataBase PHPRunner

if (isset($_SESSION['grupo'])) {
    $grupo = $_SESSION['grupo']; // Grupo que estamos accediendo
} elseif (isset($_SESSION['jefe'])) {
    $jefe = $_SESSION['jefe'];
}
  else {
    $user_idusuario = $_SESSION["user_idusuario"]; // Usuario que estamos accediendo
}

// Require our Event class and datetime utilities
require 'fullcalendar/php/utils.php';

// Short-circuit if the client did not give us a date range.
if (!isset($_GET['start']) || !isset($_GET['end'])) {
  die("Please provide a date range.");
}

// Parse the start/end parameters.
// These are assumed to be ISO8601 strings with no time nor timeZone, like "2013-12-29".
// Since no timeZone will be present, they will parsed as UTC.
$range_start = parseDateTime($_GET['start']);
$range_end = parseDateTime($_GET['end']);

// Parse the timeZone parameter if it is present.

$time_zone ='UTC';
if (isset($_GET['timeZone'])) {
//   $time_zone = new DateTimeZone($_GET['timeZone']);
}


$start = substr($_GET['start'],0,10).' 00:00:00'; 
$end   = substr($_GET['end']  ,0,10).' 00:00:00';
$now   = date('Y-m-d H:i:s', strtotime(now(). ' +2 hour')); // now() + 1
if ( $start < $now ) { //  Minimum date of the moment
//		$start = $now;
    }
$language = 'es';

if (isset($grupo)) { // Accediendo por Grupo
$sql = "
SELECT
grupo_trabajo.Titulo,
peticion.idpeticion,
peticion.usuario_idusuario,
peticion.FechaInicio,
peticion.FechaFin,
peticion.DiasNaturales,
peticion.DiasLaborables,
peticion.DiasFinSemana,
peticion.DiasFiesta,
usuario.Color,
usuario.NombreyApellido,
peticion.Status
FROM grupo_trabajo
JOIN usuario_grupo ON (grupo_trabajo.idgrupo_trabajo = usuario_grupo.grupo_trabajo_idgrupo_trabajo)
JOIN usuario ON ( usuario_grupo.usuario_idusuario = usuario.idusuario)
JOIN peticion ON ( peticion.usuario_idusuario = usuario_grupo.usuario_idusuario)
WHERE grupo_trabajo.Titulo = '$grupo' and
peticion.FechaFin >= '$start' and peticion.FechaInicio <= '$end' 
";
}
if (isset($user_idusuario)) { // Accediendo por Usuario
$sql = "
SELECT
'Empresa' Titulo,
 peticion.idpeticion,
 peticion.usuario_idusuario,
 peticion.FechaInicio,
 peticion.FechaFin,
 peticion.DiasNaturales,
 peticion.DiasLaborables,
 peticion.DiasFinSemana,
 peticion.DiasFiesta,
 usuario.Color,
 usuario.NombreyApellido,
 peticion.Status

FROM peticion
JOIN usuario ON (peticion.usuario_idusuario = usuario.idusuario)
 WHERE peticion.usuario_idusuario =$user_idusuario and
  peticion.FechaFin >= '$start' and peticion.FechaInicio <= '$end' 
  ";
  }
if (isset($jefe)) { // Accediendo por Jefe
$sql = "
SELECT
'Empresa' Titulo,
usuario.idusuario,
usuario.NombreyApellido,
usuario.Color,
usuario.Jefe_id,
peticion.idpeticion,
peticion.usuario_idusuario,
peticion.FechaInicio,
peticion.FechaFin,
peticion.Status,
periodo.idperiodo,
periodo.Abierto
FROM usuario usuario
JOIN peticion ON (usuario.idusuario = peticion.usuario_idusuario)
JOIN periodo ON (peticion.periodo_idperiodo = periodo.idperiodo and periodo.Abierto = 1)
 WHERE usuario.Jefe_id = $jefe and
  peticion.FechaFin >= '$start' and peticion.FechaInicio <= '$end' 
  ";
  }

$rs = DB::Query($sql);
// Accumulate an output array of event data arrays.
$output_arrays = array();

while( $data = $rs->fetchAssoc() ){
$color = $data['Color'];
if ( $data['Status'] == '0' ) { 
  $color = 'black'; 
}
$output_arrays[] = array(
    'id'=>$data['idpeticion'],
    'title'=>date_format(date_create($data['FechaInicio']),"d/m/Y").' - '.date_format(date_create($data['FechaFin']),"d/m/Y").' - '.$data['NombreyApellido'],
    'resourceId'=>$data['usuario_idusuario'],
    'color'=>$color,
    'start'=>$data['FechaInicio'],
    'end'=>date('Y-m-d', strtotime($data['FechaFin']. ' +1 day')),
    'description'=>' ');
    }


// Send JSON to the client.
// $str = json_encode($output_arrays);
echo json_encode($output_arrays);
?>

Os dejo los fuentes y  un backup de los datos, para que lo podáis descargar y probarlo en vuestros PC’s.

Como siempre, cualquier duda me escribís a [email protected].

Adjuntos

Archivo Tamaño de archivo Descargas
zip PHPRunner 10.7 y backup - Actualizado 04/05/2023 3 MB 400

Blog personal para facilitar soporte gratuito a usuarios de React y PHPRunner