Guía 64 – Utilización de la biblioteca «FullCalendar»

Para representar cualquier evento que esté relacionado con una fecha o rango de fechas, la mejor forma es la «imagen» de un calendario.

Para este tipo de presentaciones, lo más utilizado con diferencia, es la biblioteca de JavaScript FullCalendar. He hecho varios ejemplos en donde la he utilizado, por ejemplo «Gestión de Vacaciones«, pero  he utilizado otras formas de representar datos en calendario y, además,  la propia gestión de la aplicación hace más complejo entender su funcionamiento.

Objetivo

Disponer de un ejemplo sencillo del uso de la biblioteca de JavaScript «FullCalendar» . En este ejemplo se dispone de la forma de representación de calendario de formato Mes, Trimestre y Año completo.

ACTUALIZACIÓN: Se ofrecerá en los eventos, la posibilidad de disponer de «tooltip» para facilitar información si tener que «abrir» el evento y , también, la posibilidad de dar de alta nuevos incidentes seleccinando las echas en el calendario.

DEMO: https://fhumanes.com/incidentes/

Solución Técnica

La lógica de la aplicación es muy sencilla. Son Usuarios/Personal de una Organización en donde se registra los permisos y vacaciones que disfrutan esas personas.

Cada tipo de permiso tiene un color y desde la presentación del «calendario» se pueden dar de alta nuevos permisos, se pueden visualizar los detalles o modificar los existentes.

Para la introducción de las fechas (rangos de fecha) se utiliza el Plugin «RangeDatePicker» que podrás descargar de forma gratuita de mi portal.

También, por añadir alguna funcionalidad, se calculan, en los rangos de fechas, los días naturales y los días laborables, de cara a poder hacer cálculos posteriores.

Para la representación de cualquier «calendario» se utilizan 2 ficheros que tienen esta funcionalidad:

1.- Definición de las características del calendario. en este caso fichero «calendario_user.php«.

<?php

$id_user = $_GET['masterkey1']; // ID of User 

$html = <<<EOT

<script src='fullcalendar-6.1.8/dist/index.global.min.js'></script>
<script src='fullcalendar-6.1.8/packages/core/locales-all.global.min.js'></script>

<script src='fullcalendar-6.1.8/dist/popper.min.js'></script>
<script src="fullcalendar-6.1.8/dist/tooltip.min.js"></script>


<style>

  #calendar {
    max-width: 1200px;
    margin: 30px auto;
    font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
    font-size: 14px;
  }

/*
i wish this required CSS was better documented :(
https://github.com/FezVrasta/popper.js/issues/674
derived from this CSS on this page: https://popper.js.org/tooltip-examples.html
*/

.popper,
.tooltip {
  position: absolute;
  z-index: 9999;
  background: #3684EE;
  color: black;
  width: 200px;
  border-radius: 3px;
  box-shadow: 0 0 2px rgba(0,0,0,0.5);
  padding: 10px;
  text-align: center;
  opacity: 1;
}
.tooltip-inner {
  background-color: transparent;
}
.style5 .tooltip {
  background: #1E252B;
  color: #FFFFFF;
  max-width: 200px;
  width: auto;
  font-size: .8rem;
  padding: .5em 1em;
}
.popper .popper__arrow,
.tooltip .tooltip-arrow {
  width: 0;
  height: 0;
  border-style: solid;
  position: absolute;
  margin: 5px;
}

.tooltip .tooltip-arrow,
.popper .popper__arrow {
  border-color: #3684EE;
}
.style5 .tooltip .tooltip-arrow {
  border-color: #3684EE;
}
.popper[x-placement^="top"],
.tooltip[x-placement^="top"] {
  margin-bottom: 5px;
}
.popper[x-placement^="top"] .popper__arrow,
.tooltip[x-placement^="top"] .tooltip-arrow {
  border-width: 5px 5px 0 5px;
  border-left-color: transparent;
  border-right-color: transparent;
  border-bottom-color: transparent;
  bottom: -5px;
  left: calc(50% - 5px);
  margin-top: 0;
  margin-bottom: 0;
}
.popper[x-placement^="bottom"],
.tooltip[x-placement^="bottom"] {
  margin-top: 5px;
}
.tooltip[x-placement^="bottom"] .tooltip-arrow,
.popper[x-placement^="bottom"] .popper__arrow {
  border-width: 0 5px 5px 5px;
  border-left-color: transparent;
  border-right-color: transparent;
  border-top-color: transparent;
  top: -5px;
  left: calc(50% - 5px);
  margin-top: 0;
  margin-bottom: 0;
}
.tooltip[x-placement^="right"],
.popper[x-placement^="right"] {
  margin-left: 5px;
}
.popper[x-placement^="right"] .popper__arrow,
.tooltip[x-placement^="right"] .tooltip-arrow {
  border-width: 5px 5px 5px 0;
  border-left-color: transparent;
  border-top-color: transparent;
  border-bottom-color: transparent;
  left: -5px;
  top: calc(50% - 5px);
  margin-left: 0;
  margin-right: 0;
}
.popper[x-placement^="left"],
.tooltip[x-placement^="left"] {
  margin-right: 5px;
}
.popper[x-placement^="left"] .popper__arrow,
.tooltip[x-placement^="left"] .tooltip-arrow {
  border-width: 5px 0 5px 5px;
  border-top-color: transparent;
  border-right-color: transparent;
  border-bottom-color: transparent;
  right: -5px;
  top: calc(50% - 5px);
  margin-left: 0;
  margin-right: 0;
}

</style>

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

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

  var calendar = new FullCalendar.Calendar(calendarEl, {
      timeZone: 'UTC+1', // the default 'local' (unnecessary to specify)
      locale: initialLocaleCode,
      height: 'auto',
      // navLinks: true, // can click day/week names to navigate views

      headerToolbar: { center: 'dayGridMonth Trimestre multiMonthYear' }, // buttons for switching between views
      initialView: 'Trimestre',
      views: {
       Trimestre: {
        type: 'multiMonth',
        duration: { months: 3 }
       }
      },
      editable: false,
      multiMonthMinWidth: 50,
      multiMonthMaxColumns: 3,
      weekends: true,
      dayHeaders: true,
      firstDay: 1,
       selectable: true,

      // ADD previus select Date
      select: function(info) {        
          console.log('Estoy en evento de Select: '+ info.startStr + ' to ' + info.endStr);
          var startDate = info.startStr;
          var  endDate = info.endStr;
          var title = 'Añadir nuevo Incidente';
          var url = "calendario_add.php?masterkey1=$id_user&page=add&mastertable=incidente_usuario&start="+startDate+"&end="+endDate;
          var header = '<h2 data-itemtype="add_header" data-itemid="add_header">'+ title+'</h2>' ;
          window.popup = Runner.displayPopup({
                url: url,
                width: 800,
                height: 500,
                header: header,
          // footer: '<a href="" onclick="window.win.close();return false;">Close window</a>',
          footer: '<a href="calendario_list.php?masterkey1=$id_user&page=list&mastertable=incidente_usuario" onclick="window.win.close();return false;">Close window and Reload page</a>',
          afterCreate: function(popup) {
            window.popup = popup;
          }
        });
      },

       // Evenet managment the Tooltip
      eventDidMount: function(info) {
        console.log("estoy en evento de Tooltip");
        var tooltip = new Tooltip(info.el, {
/* title: info.event._def.title, */
           title: info.event.extendedProps.description, 
/* title: "primera línea <BR> segunda línea", */
          placement: 'top',
           html: true,
          trigger: 'hover',
          container: '#calendar'
        });
      },

      // Load data the Event
      events: { // you can also specify a plain string like 'json/events-for-resources.json'
      url: 'calendario_user_ajax.php?id_user=$id_user'},
      
      // Event for View and Edit
      eventClick: function(info) {
        console.log("estoy en evento de Click");
        var peticion = info.event.id;
        var title = info.event.title;
        var url = "calendario_view.php?editid1="+peticion;
        var header = '<h2 data-itemtype="view_header" data-itemid="view_header" data-pageid="10">'+'Petición: '+ title+'</h2>' ;
        window.popup = Runner.displayPopup({
                    url: url,
                    width: 800,
                    height: 500,
                    header: header,
        // footer: '<a href="" onclick="window.win.close();return false;">Close window</a>',
        footer: '<a href="calendario_list.php?masterkey1=$id_user&page=list&mastertable=incidente_usuario" onclick="window.win.close();return false;">Close window and Reload page</a>',
        afterCreate: function(popup) {
          window.popup = popup;
        }
       }); 
      },

    });

    calendar.render();
  });

</script>

EOT;

echo $html;

2.- Conjunto de datos a representar en el «calendario» de acuerdo a las fechas del mismo. Procesos AJAX de captura de información. En este caso fichero «calendario_user_ajax.php»

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


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

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

$id_user = $_GET['id_user']; // id of User

// 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';

$sql = "
SELECT
u.`id_incidente_usuario`,
u.`login`,
u.`nombre_apellidos`,
c.`id_incidente_caso`,
c.`incidente_usuario_id`,
c.`incidente_tipo_id`,
c.`fechaDesde`,
c.`fechaHasta`,
c.`titulo`,
c.`diasLaborables`,
c.`diasFinSemana`,
t.`id_incidente_tipo`,
t.`titulo` tipo,
t.`color`
FROM incidente_usuario u
join incidente_caso c on (u.`id_incidente_usuario` = c.`incidente_usuario_id`)
join incidente_tipo t on (c.`incidente_tipo_id` = t.`id_incidente_tipo`)
where u.`id_incidente_usuario` = $id_user and
c.`fechaHasta` >= '$start' and c.`fechaDesde` <= '$end'
";

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

while( $data = $rs->fetchAssoc() ){
$color = $data['color'];

$output_arrays[] = array(
    'id'=>$data['id_incidente_caso'],
    'title'=>$data['tipo'].' - '.date_format(date_create($data['fechaDesde']),"d/m/Y").' - '.date_format(date_create($data['fechaHasta']),"d/m/Y"),
    // 'resourceId'=>$data['usuario_idusuario'],
    'color'=>$color,
    'start'=>$data['fechaDesde'],
    'end'=>date('Y-m-d', strtotime($data['fechaHasta']. ' +1 day')),
    'description'=>'<B>'.$data['tipo'].'</B><BR>'.date_format(date_create($data['fechaDesde']),"d/m/Y").' - '.date_format(date_create($data['fechaHasta']),"d/m/Y")
      );
}


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

?>

Las ediciones (Altas y Edición de registros) se realizan en páginas popup sobre la página de «calendario». Para refrescar la página de calendario he utilizado un «sistema» curioso, pero eficiente y es en el evento de After record Updated o After record Added, he puesto este código:

$html = <<<EOT
<html>
<head>
<meta charset='utf-8' />
</head>
<body>
<div>
<p>Nueva petición actualizada</p>
</div>
<script>
window.parent.location.reload();
</script>

</body>
</html>

EOT;
echo $html;
exit();

Lo que hago es crear una página que lleva un JavaScript que lo que hace es recargar la página de «calendario». Para mí es un método más sencillo que el propuesto en el manual utilizando unas variables «proxy».

Espero que os sea fácil de leer el código del proyecto y que lo encontréis útil para vuestros proyectos,

Si necesitáis cualquier explicación o detalle, indicádmelo escribiendo un mensaje a mi cuenta de email [email protected]

Os dejo los fuentes para que los descarguéis y podáis revisar y modificar lo que consideréis oportuno.

Adjuntos

Archivo Tamaño de archivo Descargas
zip PHPRunner 10.7 y backup de base de datos - Actualización: 22/07/2023 465 KB 500

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