En ocasiones anteriores ya te hemos mostrado algunos tips, trucos y guías de nuestro framework php favorito Symfony en está oportunidad te mostramos como crear un Calendario de eventos en Symfony 3 con dhtmlxScheduler.

Calendario de eventos en Symfony 3 con dhtmlxScheduler

Un calendario o programador de eventos es una herramienta imprescindible para una empresa, inclusive para una una persona normal, en donde se pueden programar y hacer seguimientos de citas, eventos, tareas, etc. Para ellos haremos uso del programador dhtmlx que es una de las mejores bibliotecas JavaScript para esto.

dhtmlxScheduler es un calendario de eventos JS parecido a Google con una amplia gama de vistas y características. Tiene una interfaz de usuario limpia y apariencia personalizable.

Tal como indicamos al principio de este artículo, aprenderás cómo crear tu propio Calendario de eventos personalizado en el frontend y el backend con Symfony 3 y dhtmlxscheduler.

Calendario de eventos en Symfony 3 con dhtmlxScheduler

Requisitos

Antes e comenzar para crear nuestro calendario debemos contar con las siguientes librerías:


dhtmlxScheduler

Necesitaremos una copia de la biblioteca del programador dhtmlx (archivo .zip). Esta biblioteca ofrece 2 versiones, la versión de código abierto (Edición estándar) y la Versión de pago (Edición profesional) donde recibe asistencia y una licencia comercial.

Desde el archivo zip de origen, solo necesitaremos el código JavaScript, ya que el backend se implementará totalmente con Symfony. Este programador es muy flexible y podemos personalizar muchas cosas de la manera que deseemos, recomendamos leer la documentación también. Puedes descargar cualquiera de las versiones mencionadas aquí.

Lo primero que debe hacer, una vez que tenga el archivo zip, es crear un directorio donde guardar la biblioteca. Para está guía, crearemos la carpeta de bibliotecas en el directorio /web de la aplicación Symfony. Por lo tanto, el código fuente de JavaScript será accesible desde tuaplicacion/web/libraries/dhtmlx . No arruinaremos la estructura original del archivo zip descargado, por lo que tendremos en este caso las bases de código de las carpetas y los demos dentro de dhtmlx que podemos usar para verificar los ejemplos.

moment.js

El archivo principal de JavaScript de Moment.js deberá estar accesible en
tuaplicacion/web/libraries/moment.js puedes descargar de aquí

Jquery o cualquier otra biblioteca AJAX personalizada

Usaremos jQuery AJAX para enviar nuestras citas en la vista. Alternativamente, puede escribir su propio código XMLHttpRequest para enviar los datos a su servidor de forma asíncrona con JavaScript o, en caso de que no quiera jQuery, minAjax es bastante útil y funciona de la misma manera que jQuery.

Pasos para crear el calendario

Una vez cubiertos los requerimientos podemos comenzar a escribir el código para nuestro calendario

Paso 1: Implementar la entidad para citas

Nota: Si ya tienes algún diseño de tabla personalizado para tus «citas», omite este paso y sigue la estructura del controlador en el paso 2.

Con el programador podrá programar eventos gráficamente en el lado del cliente, sin embargo, también deben almacenarse en alguna base de datos. Esto se puede lograr con la comunicación con AJAX entre el cliente y el servidor.

El objetivo de está guía será conservar algunas clases de citas en una base de datos (MySql, MongoDB, CouchDB, etc.). Nuestro primer trabajo, entonces, es crear la clase Cita para la aplicación. Esta clase puede verse y actuar como deseemos, así que agrega cualquier propiedad o método que te resulte útil. En este ejemplo, nuestra Entidad se generará a partir de la siguiente tabla: citas. La tabla de citas en su base de datos tendrá 5 campos, a saber, id (autoincrement not null), title(columna de texto), description (columna de texto), start_date (datetime column) y end_date (datetime column):

CREATE TABLE `TuTablaExistente`.`citas` 
  ( 
     `id`          BIGINT NOT NULL auto_increment, 
     `title`       VARCHAR(255) NOT NULL, 
     `description` TEXT NULL, 
     `start_date`  DATETIME NOT NULL, 
     `end_date`    DATETIME NOT NULL, 
     PRIMARY KEY (`id`) 
  ) 
engine = innodb; 

De acuerdo con la forma en que trabajes, puedes seguir el proceso para generar los archivos orm y la entidad manualmente o desde tu base de datos. Si estás generando la entidad a partir de una base de datos existente, ahora puedes ejecutar el siguiente comando para generar los archivos ORM:

php bin/console doctrine:mapping:import --force AppBundle yml

Eso generará nuestro archivo ORM para la tabla de citas con el siguiente resultado en AppBundle/Resources/config/doctrine/Citas.orm.yml:

AppBundle\Entity\Citas:
    type: entity
    table: citas
    id:
        id:
            type: bigint
            nullable: false
            options:
                unsigned: false
            id: true
            generator:
                strategy: IDENTITY
    fields:
        title:
            type: string
            nullable: false
            length: 255
            options:
                fixed: false
        description:
            type: text
            nullable: true
            length: 65535
            options:
                fixed: false
        startDate:
            type: datetime
            nullable: false
            column: start_date
        endDate:
            type: datetime
            nullable: false
            column: end_date
    lifecycleCallbacks: {  }

Luego, una vez que exista el archivo orm, podemos generar automáticamente la Entidad de citas utilizando:

php bin/console doctrine:generate:entities AppBundle

La entidad generada en AppBundle/Entity/Citas se verá así:

<?php

namespace AppBundle\Entity;

/**
 * Citas
 */
class Citas
{
    /**
     * @var integer
     */
    private $id;

    /**
     * @var string
     */
    private $title;

    /**
     * @var string
     */
    private $description;

    /**
     * @var \DateTime
     */
    private $startDate;

    /**
     * @var \DateTime
     */
    private $endDate;


    /**
     * Get id
     *
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set title
     *
     * @param string $title
     *
     * @return Citas
     */
    public function setTitle($title)
    {
        $this->title = $title;

        return $this;
    }

    /**
     * Get title
     *
     * @return string
     */
    public function getTitle()
    {
        return $this->title;
    }

    /**
     * Set description
     *
     * @param string $description
     *
     * @return Citas
     */
    public function setDescription($description)
    {
        $this->description = $description;

        return $this;
    }

    /**
     * Get description
     *
     * @return string
     */
    public function getDescription()
    {
        return $this->description;
    }

    /**
     * Set startDate
     *
     * @param \DateTime $startDate
     *
     * @return Citas
     */
    public function setStartDate($startDate)
    {
        $this->startDate = $startDate;

        return $this;
    }

    /**
     * Get startDate
     *
     * @return \DateTime
     */
    public function getStartDate()
    {
        return $this->startDate;
    }

    /**
     * Set endDate
     *
     * @param \DateTime $endDate
     *
     * @return Citas
     */
    public function setEndDate($endDate)
    {
        $this->endDate = $endDate;

        return $this;
    }

    /**
     * Get endDate
     *
     * @return \DateTime
     */
    public function getEndDate()
    {
        return $this->endDate;
    }
}

Ahora la Entidad de citas puede persistir en la base de datos. Si no tienes un diseño existente para almacenar el registro en la base de datos, puedes modificar los campos según lo necesites.

Paso 2: Implementar el controlador del calendario y las rutas

El controlador para el calendario tendrá solo 4 rutas. Las rutas que definiremos deben ser accesibles en la ruta /calendario de tu proyecto, así que modifica el archivo principal routing.yml de tu proyecto Symfony y registra otro archivo de enrutamiento que maneje las rutas del calendario:

# Crear ruta para el calendario en su aplicación
app_scheduler:
    resource: "@AppBundle/Resources/config/routing/calendario.yml"
    prefix:   /calendario

Ten en cuenta que almacenaremos el nuevo archivo de enrutamiento en la carpeta config/routing del paquete principal. El archivo de enrutamiento calendario.yml es el siguiente:

# app/config/routing.yml
calendario_index:
    path:      /
    defaults:  { _controller: AppBundle:Calendario:index }
    methods:  [GET]

calendario_create:
    path:      /cita-create
    defaults:  { _controller: AppBundle:Calendario:create }
    methods:  [POST]

calendario_update:
    path:      /cita-update
    defaults:  { _controller: AppBundle:Calendario:update }
    methods:  [POST]

calendario_delete:
    path:      /cita-delete
    defaults:  { _controller: AppBundle:Calendario:delete }
    methods:  [DELETE]

Cada ruta es manejada por una función en el controlador del calendario ubicado en el AppBundle (que crearemos ahora). 3 de ellos solo se utilizarán para crear, eliminar y modificar las citas a través de AJAX. La ruta del índice (tuwebsite/calendario) mostrará el Calendario en el navegador.

Ahora que las rutas están registradas, debemos crear el controlador que maneje las rutas y la lógica en cada una de ellas. Como la lógica puede variar de acuerdo con la forma en que maneja las entidades, el siguiente controlador muestra cómo manejar cada evento trabajando con la entidad Cita. Todas las respuestas se dan en formato JSON (excepto el index) para proporcionar información sobre el estado de la acción:

<?php

namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller; 

// Incluir las clases utilizadas como JsonResponse y el objeto Request
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;

// La entidad de Cita
use AppBundle\Entity\Citas as Cita;

class CalendarioController extends Controller
{
    /**
     * Vista que hace que el calendario.
     *
     */
    public function indexAction()
    {
        // Recuperar administrador de entidades
        $em = $this->getDoctrine()->getManager();
        
        // Obtener repositorio de citas
        $repositoryCitas = $em->getRepository("AppBundle:Citas");

        // Tenga en cuenta que es posible que desee filtrar las citas que desea enviar
        // por fechas o algo, de lo contrario enviarás todas las citas para renderizar
        $citas = $repositoryCitas->findAll();

        // Generar estructura JSON a partir de las citas para representar en el calendario de inicio.
        $formatedCitas = $this->formatCitasToJson($citas);

        // Renderizar calendario
        return $this->render("default/calendario.html.twig", [
            'citas' => $formatedCitas
        ]);
    }

    /**
     * Manejar la creación de una cita.
     *
     */
    public function createAction(Request $request){
        $em = $this->getDoctrine()->getManager();
        $repositoryCitas = $em->getRepository("AppBundle:Citas");

        // Utilice el mismo formato utilizado por Moment.js en la vista
        $format = "d-m-Y H:i:s";

        // Instanciar la entidad cita y establecer los valores de los campos.
        $cita = new Cita();
        $cita->setTitle($request->request->get("title"));
        $cita->setDescription($request->request->get("description"));
        $cita->setStartDate(
            \DateTime::createFromFormat($format, $request->request->get("start_date"))
        );
        $cita->setEndDate(
            \DateTime::createFromFormat($format, $request->request->get("end_date"))
        );

        // Crear cita
        $em->persist($cita);
        $em->flush();

        return new JsonResponse(array(
            "status" => "success"
        ));
    }
    
    /**
     * Manejar la actualización de una cita.
     *
     */
    public function updateAction(Request $request){
        $em = $this->getDoctrine()->getManager();
        $repositoryCitas = $em->getRepository("AppBundle:Citas");

        $citaId = $request->request->get("id");

        $cita = $repositoryCitas->find($citaId);

        if(!$cita){
            return new JsonResponse(array(
                "status" => "error",
                "message" => "La cita a actualizar $citaId no existe."
            ));
        }

        // Utilice el mismo formato utilizado por Moment.js en la vista
        $format = "d-m-Y H:i:s";

        // Establecer los nuevo valores de los campos de la cita
        $cita->setTitle($request->request->get("title"));
        $cita->setDescription($request->request->get("description"));
        $cita->setStartDate(
            \DateTime::createFromFormat($format, $request->request->get("start_date"))
        );
        $cita->setEndDate(
            \DateTime::createFromFormat($format, $request->request->get("end_date"))
        );

        // Actualizar cita
        $em->persist($cita);
        $em->flush();

        return new JsonResponse(array(
            "status" => "success"
        ));
    }

    /**
     * Eliminiar una cita de la base de datos
     *
     */
    public function deleteAction(Request $request){
        $em = $this->getDoctrine()->getManager();
        $repositoryCitas = $em->getRepository("AppBundle:Citas");

        $citaId = $request->request->get("id");

        $cita = $repositoryCitas->find($citaId);

        if(!$cita){
            return new JsonResponse(array(
                "status" => "error",
                "message" => "La cita $citaId no existe."
            ));
        }

        // Eliminar la cita de la base de datos
        $em->remove($cita);
        $em->flush();       

        return new JsonResponse(array(
            "status" => "success"
        ));
    }


    /**
     * Devuelve una cadena JSON de un grupo de citas que se representarán en el calendario.
     * Puedes usar una librería serializadora si quieres.
     *
     * Las fechas deben seguir el formato d-m-Y H: i por ejemplo: "01-04-2019 09:00"
     *
     *
     * @param $citas
     */
    private function formatCitasToJson($citas){
        $formatedCitas = array();
        
        foreach($citas as $cita){
            array_push($formatedCitas, array(
                "id" => $cita->getId(),
                "description" => $cita->getDescription(),
                // Es importante mantener la fecha de inicio, la fecha de finalización y el texto con la misma clave
                // para el área de JavaScript
                // aunque el captador podría ser diferente, por ejemplo:
                // "start_date" => $cita->getBeginDate();
                "text" => $cita->getTitle(),
                "start_date" => $cita->getStartDate()->format("Y-m-d H:i"),
                "end_date" => $cita->getEndDate()->format("Y-m-d H:i")
            ));
        }

        return json_encode($formatedCitas);
    }
}

NOTA: Como el programador dhtmlx requiere las claves start_date, end_date y text en un evento, deberá proporcionarlas en cada evento, esto significa que no puede cambiar su nombre.

Paso 3: Implementar diseño y estructura de scripts

Ahora que la lógica del lado del servidor está lista, podemos proceder a crear el diseño de nuestra aplicación. En este caso renderizaremos un programador de pantalla completa.

Usaremos el siguiente archivo base para nuestro diseño en Twig (base.html.twig)

{# application/resources/views/base.html.twig #}
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <title>{% block title %}Welcome!{% endblock %}</title>
        {% block stylesheets %}{% endblock %}
    </head>
    <body>
        {% block body %}{% endblock %}
        {% block javascripts %}{% endblock %}
    </body>
</html>

Luego, como se define en nuestro controlador, nuestro archivo calendario.html.twig se ubicará en el directorio app/resources/views/default, así que asegúrate de crearlo en la ruta mencionada (o cambiarlo en el controlador). El diseño del programador se verá así:

{# default/calendario.html.twig #}
{% extends "base.html.twig" %}

{% block stylesheets %}
    <!-- Incluir el estilo flat del calendario -->
    <link rel='stylesheet' type='text/css' href='{{ asset("libraries/dhtmlx/codebase/dhtmlxscheduler_flat.css") }}' charset="utf-8"/>
    <!-- Si no utilizas el modo de pantalla completa, ignora el siguiente estilo -->
    <style type="text/css" media="screen">
        html, body{
            margin:0px;
            padding:0px;
            height:100%;
            overflow:hidden;
        }   
    </style>
{% endblock %}

{% block body -%}

<div id="scheduler_element" class="dhx_cal_container" style='width:100%; height:100%;'>
    <div class="dhx_cal_navline">
        <div class="dhx_cal_prev_button"> </div>
        <div class="dhx_cal_next_button"> </div>
        <div class="dhx_cal_today_button"></div>
        <div class="dhx_cal_date"></div>
        <div class="dhx_cal_tab" name="day_tab" style="right:204px;"></div>
        <div class="dhx_cal_tab" name="week_tab" style="right:140px;"></div>
        <div class="dhx_cal_tab" name="month_tab" style="right:76px;"></div>
    </div>
    <div class="dhx_cal_header"></div>
    <div class="dhx_cal_data"></div>       
</div>

{% endblock %}

{% block javascripts %}
    <!-- Incluir la biblioteca del dhtmlxscheduler -->
    <script src='{{ asset("libraries/dhtmlx/codebase/dhtmlxscheduler.js") }}' type='text/javascript' charset="utf-8"></script>
    
    <!-- Incluye jQuery para manejar solicitudes AJAX -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>

    <!-- Incluye Momentjs para jugar con las fechas -->
    <script src="{{ asset("libraries/momentjs/moment.js") }}"></script>

    <script>
        // Exponer las citas globalmente imprimiendo la cadena JSON con twig y el filtro raw
        // para que puedan ser accesibles por el schedulerScripts.js el controlador
        window.GLOBAL_APPOINTMENTS = {{ citas|raw }};

        // Como los scripts del programador estarán en otros archivos, las rutas generadas por twig
        // Debería estar expuesto en la ventana también
        window.GLOBAL_SCHEDULER_ROUTES = {
            create: '{{ path("calendario_create") }}',
            update: '{{ path("calendario_update") }}',
            delete: '{{ path("calendario_delete") }}'
        };
    </script>

    <!-- Incluya el SchedulerScripts que necesitará escribir en el siguiente paso -->
    <script src='{{ asset("libraries/calendarioScripts.js") }}' type='text/javascript' charset="utf-8"></script>
{% endblock %}

Ahora vamos a escribir el contenido del archivo calendarioScripts.js que contendrá el código para manejar la lógica del planificador en la vista.

Paso 4: Escribir lógica de lado cliente

Para nuestro Calendario, le permitiremos al usuario crear citas en el con la ayuda de un Lightbox predeterminado del programador dhtmlx. Lo primero que debe hacer es configurar el comportamiento predeterminado de su programador modificando el objeto de configuración del programador. Al menos debe proporcionar el formato xml_date, el resto es puramente opcional.

Luego configuraremos las secciones del formulario para insertar y editar las citas. En este caso, ya que solo tenemos 2 campos (Título y Descripción), el título se asignará al campo de texto predeterminado del Calendario. Los campos predeterminados de tiempo y texto deben existir en el
Lightbox.

El código de calendarioScripts.js será el siguiente:

// 1. Configurar los ajustes básicos del Calendario
scheduler.config.xml_date="%Y-%m-%d %H:%i";
scheduler.config.first_hour = 6;
scheduler.config.last_hour = 24;
scheduler.config.limit_time_select = true;
scheduler.config.details_on_create = true;
// Deshabilitar la edición de eventos con un solo clic
scheduler.config.select = false;
scheduler.config.details_on_dblclick = true;
scheduler.config.max_month_events = 5;
scheduler.config.resize_month_events = true;

// 2. Configurar secciones de lightbox (formulario)
scheduler.config.lightbox.sections = [
    // Si tiene otro campo en su entidad Cita (por ejemplo, columna_de_ejemplo), lo agregaría como
    // {name:"Campo de Ejemplo", height:30, map_to:"columna_de_ejemplo", type:"textarea"},
    {name:"Título", height:30, map_to:"text", type:"textarea"},
    {name:"Descripción", height:30, map_to:"description", type:"textarea"},
    {name:"Tiempo", height:72, type:"time", map_to:"auto"}
];

// 3. Iniciar calendario con configuraciones personalizadas
var initSettings = {
    // Elemento donde se iniciará el planificador
    elementId: "scheduler_element",
    // Objeto de fecha donde debe iniciarse el planificador
    startDate: new Date(),
    // modo de inicio
    mode: "week"
};

scheduler.init(initSettings.elementId, initSettings.startDate , initSettings.mode);

// 4. Analizar las citas iniciales (desde el controlador de index)
scheduler.parse(window.GLOBAL_APPOINTMENTS, "json");

// 5. Función que formatea los eventos al formato esperado en el lado del servidor.

/**
 * Devuelve un objeto con la estructura deseada del servidor.
 * 
 * @param {*} id 
 * @param {*} useJavascriptDate 
 */
function getFormatedEvent(id, useJavascriptDate){
    var event;

    // Si id ya es un objeto de evento, utilícelo y no lo busque
    if(typeof(id) == "object"){
        event = id;
    }else{
        event = scheduler.getEvent(parseInt(id));
    }

    if(!event){
        console.error("El ID del evento no existe: " + id);
        return false;
    }
     
    var start , end;
    
    if(useJavascriptDate){
        start = event.start_date;
        end = event.end_date;
    }else{
        start = moment(event.start_date).format('DD-MM-YYYY HH:mm:ss');
        end = moment(event.end_date).format('DD-MM-YYYY HH:mm:ss');
    }
    
    return {
        id: event.id,
        start_date : start,
        end_date : end,
        description : event.description,
        title : event.text
    };
}

// 6. Adjuntar controladores de eventos!

/**
 * Manejar el evento CREATE del calendario
 */
scheduler.attachEvent("onEventAdded", function(id,ev){
    var schedulerState = scheduler.getState();
    
    $.ajax({
        url:  window.GLOBAL_SCHEDULER_ROUTES.create,
        data: getFormatedEvent(ev),
        dataType: "json",
        type: "POST",
        success: function(response){
            // Muy importante:
            // Actualizar el ID de la cita del programador con el ID de la base de datos
            // ¡Así podemos editar la misma cita ahora!
            
            scheduler.changeEventId(ev.id , response.id);

            alert('La cita '+ev.text+ " ha sido creada satisfactoriamente");
        },
        error:function(error){
            alert('Error: La cita '+ev.text+' no pudo ser creada');
            console.log(error);
        }
    }); 
});

/**
 * Manejar el evento UPDATE del calendario en todos los casos posibles (arrastrar y soltar, cambiar el tamaño, etc.)
 *  
 */
scheduler.attachEvent("onEventChanged", function(id,ev){
    $.ajax({
        url:  window.GLOBAL_SCHEDULER_ROUTES.update,
        data: getFormatedEvent(ev),
        dataType: "json",
        type: "POST",
        success: function(response){
            if(response.status == "success"){
                alert("Cita actualizado satisfactoriamente!");
            }
        },
        error: function(err){
            alert("Error: Cambios no guardados");
            console.error(err);
        }
    });

    return true;
});

/**
 * Manejar el evento DELETE del calendario
 */
scheduler.attachEvent("onConfirmedBeforeEventDelete",function(id,ev){
    $.ajax({
        url: window.GLOBAL_SCHEDULER_ROUTES.delete,
        data:{
            id: id
        },
        dataType: "json",
        type: "DELETE",
        success: function(response){
            if(response.status == "success"){
                if(!ev.willDeleted){
                    alert("Cita elminidad satisfactoriamente");
                }
            }else if(response.status == "error"){
                alert("Error: Cita no eliminada");
            }
        },
        error:function(error){
            alert("Error: No se pudo eliminar la cita: " + ev.text);
            console.log(error);
        }
    });
    
    return true;
});


/**
 * Editar evento con el botón derecho también
 * 
 * @param {type} id
 * @param {type} ev
 * @returns {Boolean}
 */
scheduler.attachEvent("onContextMenu", function (id, e){
    scheduler.showLightbox(id);
    e.preventDefault();
});

Finalmente, guarde los cambios, acceda a la URL de su proyecto http://tuwebsite/calendario y ahora puedes probar el calendario. Como recomendación final, consulte la documentación del programador dhtmlx para descubrir más utilidades increíbles que permitirán que creemos la mejor aplicación de calendarios que necesitemos.