Aprende a crear un calendario de eventos JS similar al de Google con la biblioteca dhtmlxScheduler en el frontend y con Symfony 3 en el backend.

Crear un calendario de eventos (planificador) con dhtmlxScheduler en Symfony 3

Un Programador es un componente que no puede faltar en un Producto de software para una empresa. Con el programador, una empresa (o una persona normal) podría programar y realizar un seguimiento de citas, eventos, tareas y otras cosas. Como se muestra en nuestro Top 5: Mejor, el programador dhtmlx es una de las mejores bibliotecas de JavaScript para programadores que le permite implementar dicha característica en su aplicación. dhtmlxScheduler es un calendario de eventos JS similar a Google con una amplia gama de vistas y funciones. Tiene una interfaz de usuario limpia y una apariencia personalizable.

En este artículo, aprenderá a crear su propio calendario de eventos personalizado (programador) en el frontend y backend con Symfony y dhtmlxscheduler.

Requisitos

Para crear su propio Programador, deberá preparar las siguientes bibliotecas en su proyecto. Describiremos lo que necesitamos de ellos y si no puede incluirlos (excluyendo el programador dhtmlx, ya que obviamente es necesario), puede escribir su propio respaldo:

A. Planificador dhtmlx

Necesitará una copia de la biblioteca del programador dhtmlx (archivo .zip). Esta biblioteca ofrece 2 versiones, la versión Open Source (Standard Edition) en la que puede leer la documentación de la biblioteca en el sitio web oficial o la versión Paid (Pro Edition) donde recibe soporte y una licencia comercial.

Desde el archivo zip de origen, solo necesitará el código JavaScript, ya que el backend se implementará totalmente con Symfony. Este programador es muy flexible y puede personalizar muchas cosas de la forma que desee, le recomendamos que lea la documentación también. Puede 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. En este artículo, crearemos la carpeta de bibliotecas en el /webdirectorio de la aplicación Symfony. Entonces, el código fuente de JavaScript será accesible en  yourapplication/web/libraries/dhtmlx. No estropearemos la estructura original del archivo zip descargado, por lo que tendrá en este caso la base de código de las carpetas y las muestras dentro de dhtmlx que puede usar para verificar los ejemplos y hacer que su programador sea mejor más adelante.

B. Moment.js

El archivo principal de JavaScript de Moment.js deberá ser accesible en yourapplication/web/libraries/momentjs. Si no desea utilizar la biblioteca MomentJS para formatear nuestra fecha donde la necesitemos (paso 4), puede crear un respaldo reemplazando el getFormatedEventcon el siguiente código:

// Recupere el método de formato de fecha (que sigue el patrón dado) de la biblioteca del programador
var formatDate = scheduler.date.date_to_str("%d-%m-%Y %H:%i:%s");

/**
 * 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("The ID of the event doesn't exist: " + id);
        return false;
    }
     
    var start , end;
    
    if(useJavascriptDate){
        start = event.start_date;
        end = event.end_date;
    }else{
        start = formatDate(event.start_date);
        end = formatDate(event.end_date);
    }
    
    return {
        id: event.id,
        start_date : start,
        end_date : end,
        description : event.description,
        title : event.text
    };
}

C. jQuery o cualquier otra biblioteca AJAX relacionada personalizada

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

1. Implementar entidad de nombramiento

Nota

Si ya tiene un diseño de tabla personalizado para sus "citas", omita este paso y siga 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 para su usuario. Esto se puede lograr con la comunicación con AJAX entre el cliente y el servidor. 

El objetivo de este ejemplo será conservar alguna clase Appointment en una base de datos (MySql, MongoDB, CouchDB, etc.). Entonces, su primer trabajo es crear la clase  Appointment para su aplicación. Esta clase puede verse y actuar como desee, así que agregue las propiedades o métodos que considere útiles. En este ejemplo, nuestra entidad se generará a partir de la siguiente tabla, especificamente  appointments . La tabla de citas en su base de datos tendrá 5 campos, a saber, id (autoincrement not null), title (columna de texto), descripción (columna de texto), start_date (columna de fecha y hora) y end_date (columna de fecha y hora): 

CREATE TABLE `YourExistentTable`.`appointments` 
  ( 
     `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; 

Según tu forma de trabajar, puedes seguir el proceso para generar los archivos orm y la entidad manualmente o desde tu base de datos. Si está generando la entidad a partir de una base de datos existente, ahora puede 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/Appointments.orm.yml:

AppBundle\Entity\Appointments:
    type: entity
    table: appointments
    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 existe el archivo orm, puede generar automáticamente la Entidad de citas usando:

php bin/console doctrine:generate:entities AppBundle

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

<?php

namespace AppBundle\Entity;

/**
 * Appointments
 */
class Appointments
{
    /**
     * @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 Appointments
     */
    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 Appointments
     */
    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 Appointments
     */
    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 Appointments
     */
    public function setEndDate($endDate)
    {
        $this->endDate = $endDate;

        return $this;
    }

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

Ahora la entidad de citas se puede conservar en la base de datos. Si no tiene un diseño existente para almacenar el registro en la base de datos, puede modificar los campos según lo necesite.

2. Implementar el controlador del programador y las rutas

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

# Cree una ruta para el programador en su aplicación
app_scheduler:
    resource: "@AppBundle/Resources/config/routing/scheduler.yml"
    prefix:   /scheduler

Tenga en cuenta que almacenaremos el nuevo archivo de enrutamiento en la carpeta config/routing del paquete principal. El scheduler.ymlarchivo de enrutamiento es el siguiente:

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

scheduler_create:
    path:      /appointment-create
    defaults:  { _controller: AppBundle:Scheduler:create }
    methods:  [POST]

scheduler_update:
    path:      /appointment-update
    defaults:  { _controller: AppBundle:Scheduler:update }
    methods:  [POST]

scheduler_delete:
    path:      /appointment-delete
    defaults:  { _controller: AppBundle:Scheduler:delete }
    methods:  [DELETE]

Cada ruta es manejada por una función en el Controlador del Programador 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 de índice ( yourwebsite/scheduler) mostrará el Programador en el navegador.

Ahora que las rutas están registradas, deberá crear el controlador que maneje las rutas y la lógica en cada una de ellas. Como la lógica puede variar según 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 índice) para proporcionar información sobre el estado de la acción:

<?php

namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller; 

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

// La entidad de su cita
use AppBundle\Entity\Appointments as Appointment;

class SchedulerController extends Controller
{
    /**
     * Vista que genera el planificador.
     *
     */
    public function indexAction()
    {
        // Recuperar administrador de entidad
        $em = $this->getDoctrine()->getManager();
        
        // Obtener repositorio de citas
        $repositoryAppointments = $em->getRepository("AppBundle:Appointments");

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

        // Genere la estructura JSON de las citas para representar en el programador de inicio.
        $formatedAppointments = $this->formatAppointmentsToJson($appointments);

        // Renderizar planificador
        return $this->render("default/scheduler.html.twig", [
            'appointments' => $formatedAppointments
        ]);
    }

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

        // Use el mismo formato que usa Moment.js en la vista
        $format = "d-m-Y H:i:s";

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

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

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

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

        $appointment = $repositoryAppointments->find($appointmentId);

        if(!$appointment){
            return new JsonResponse(array(
                "status" => "error",
                "message" => "The appointment to update $appointmentId doesn't exist."
            ));
        }

        // Use el mismo formato que usa Moment.js en la vista
        $format = "d-m-Y H:i:s";

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

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

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

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

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

        $appointment = $repositoryAppointments->find($appointmentId);

        if(!$appointment){
            return new JsonResponse(array(
                "status" => "error",
                "message" => "The given appointment $appointmentId doesn't exist."
            ));
        }

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

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


    /**
     * Devuelve una cadena JSON de un grupo de citas que se representarán en el calendario.
     * Puede usar una biblioteca de serializador si lo desea.
     *
     * Las fechas deben seguir el formato d-m-Y H:i e.g : "13-07-2017 09:00"
     *
     *
     * @param $appointments
     */
    private function formatAppointmentsToJson($appointments){
        $formatedAppointments = array();
        
        foreach($appointments as $appointment){
            array_push($formatedAppointments, array(
                "id" => $appointment->getId(),
                "description" => $appointment->getDescription(),
                // Es importante mantener start_date, end_date y text con la misma clave
                // para el área de JavaScript
                // aunque el getter podría ser diferente, por ejemplo:
                // "start_date" => $appointment->getBeginDate();
                "text" => $appointment->getTitle(),
                "start_date" => $appointment->getStartDate()->format("Y-m-d H:i"),
                "end_date" => $appointment->getEndDate()->format("Y-m-d H:i")
            ));
        }

        return json_encode($formatedAppointments);
    }
}

Como el programador dhtmlx requiere las claves start_dateend_datetexten un evento, deberá proporcionarlas en cada evento, esto significa que no puede cambiar su nombre.

3. Implementar el diseño y la estructura de los scripts

Ahora que la lógica del lado del servidor está lista, puede proceder a crear el diseño de su aplicación. En este caso, representaremos 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>

Como su proyecto puede seguir otro esquema, asegúrese de incluir el contenido que agregaremos en el bloque respectivo suyo.

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

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

{% block stylesheets %}
    <!-- Incluir el estilo plano del planificador -->
    <link rel='stylesheet' type='text/css' href='{{ asset("libraries/dhtmlx/codebase/dhtmlxscheduler_flat.css") }}' charset="utf-8"/>
    <!-- Si no usa el modo de pantalla completa, ignore 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">&nbsp;</div>
        <div class="dhx_cal_next_button">&nbsp;</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 planificador -->
    <script src='{{ asset("libraries/dhtmlx/codebase/dhtmlxscheduler.js") }}' type='text/javascript' charset="utf-8"></script>
    
    <!-- Incluya 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 sin procesar
        // para que puedan ser accesibles por el programador Scripts.js el controlador
        window.GLOBAL_APPOINTMENTS = {{ appointments|raw }};

        // Como los scripts del planificador estarán en otros archivos, las rutas generadas por twig
        // también debe estar expuesto en la ventana
        window.GLOBAL_SCHEDULER_ROUTES = {
            create: '{{ path("scheduler_create") }}',
            update: '{{ path("scheduler_update") }}',
            delete: '{{ path("scheduler_delete") }}'
        };
    </script>

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

Incluye en el bloque de hojas de estilo el estilo plano del Programador y algunas reglas para que se vea bien en el modo de pantalla completa. Luego, en el cuerpo del bloque, el marcado requerido para el Programador y en el bloque JavaScripts, incluiremos en el siguiente orden las bibliotecas: dhtmlxscheduler, jQuery para AJAX, MomentJS para manipular fechas fácilmente.

La etiqueta de script sin formato declara 2 variables en la ventana (global) a saber, GLOBAL_APPOINTMENTS y ​​GLOBAL_SCHEDULER_ROUTES. El objeto de citas almacena las citas de la vista de índice (consulte el controlador de índice para obtener más información) como formato JSON (pero interpretado como objeto en JS), por lo tanto, necesitamos usar el filtro sin procesar de Twig . El objeto de rutas almacena las rutas generadas por Twig que se utilizarán para actualizar, crear y eliminar las citas. Como la lógica para manejar el planificador se escribirá en otro archivo JavaScript, no podemos usar twig en su interior, por lo que es recomendable generarlos donde twig esté disponible y luego acceder a ellos en los archivos con la ventana.

Ahora vamos a escribir el contenido del schedulerScripts.jsarchivo que contendrá el código para manejar la lógica del programador en la vista.

4. Escriba la lógica del lado del cliente

Para nuestro Programador, permitiremos al usuario crear citas en el calendario con la ayuda de un diálogo, es decir, el 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 son puramente opcionales.

Luego configure las secciones del formulario para insertar y editar las citas. En este caso, como solo tenemos 2 campos, a saber, Título y Descripción, el título se asignará al campo de texto predeterminado del Programador. Los campos predeterminados de tiempo y texto deben existir en la caja de luz, el tiempo especifica automáticamente los campos de inicio y fin. Luego proceda a inicializar el programador en algún modo (día, semana o mes) en un elemento DIV y, opcionalmente, especifique la fecha en la que debe comenzar el programador. Luego, analice los eventos devueltos por el controlador de índice (todas las citas almacenadas en la ventana. Matriz GLOBAL_APPOINTMENTS. Como último, puede adjuntar los eventos para manejar lo que el usuario hace con el Programador.

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

// 1. Configurar los ajustes básicos del planificador
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;
// Desactivar 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 de cita (por ejemplo, columna example_field), lo agregaría como
    // {name:"Example Field", height:30, map_to:"example_field", type:"textarea"},
    {name:"Title", height:30, map_to:"text", type:"textarea"},
    {name:"Description", height:30, map_to:"description", type:"textarea"},
    {name:"time", 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 se debe iniciar 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 índice)
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;

    // If id is already an event object, use it and don't search for it
    if(typeof(id) == "object"){
        event = id;
    }else{
        event = scheduler.getEvent(parseInt(id));
    }

    if(!event){
        console.error("The ID of the event doesn't exist: " + 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.  ¡Adjunte controladores de eventos!

/**
 * Manejar el evento del planificador CREATE
 */
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:
            // Actualice el ID de la cita del programador con el ID de la base de datos
            // ¡para que podamos editar la misma cita ahora!
            
            scheduler.changeEventId(ev.id , response.id);

            alert('The appointment '+ev.text+ " has been succesfully created");
        },
        error:function(error){
            alert('Error: The appointment '+ev.text+' couldnt be created');
            console.log(error);
        }
    }); 
});

/**
 * Manejar el evento UPDATE del programador 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("Event succesfully updated !");
            }
        },
        error: function(err){
            alert("Error: Cannot save changes");
            console.error(err);
        }
    });

    return true;
});

/**
 * Manejar el evento de cita DELETE
 */
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("Appointment succesfully deleted");
                }
            }else if(response.status == "error"){
                alert("Error: Cannot delete appointment");
            }
        },
        error:function(error){
            alert("Error: Cannot delete appointment: " + ev.text);
            console.log(error);
        }
    });
    
    return true;
});


/**
 * Edite el evento con el clic 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://yourproject/schedulery ahora puede probar el programador. Como recomendación final, consulte la documentación del programador dhtmlx para descubrir más utilidades increíbles que le permitirán crear la mejor aplicación de programación para sus clientes.

Mostrar datos de un repositorio en el formulario de cita

Según la estructura de su proyecto, sus citas no serán simplemente título, descripción y hora, sino que pueden tener un tipo que depende de los valores de otra tabla (claves externas). En el siguiente ejemplo, nuestra tabla de citas tendrá una ManyToOnerelación en la columna  categorycon una tabla categories, a saber , cuya estructura se ve así:

AppBundle\Entity\Categories:
    type: entity
    table: categories
    id:
        id:
            type: bigint
            nullable: false
            options:
                unsigned: false
            id: true
            generator:
                strategy: IDENTITY
    fields:
        name:
            type: string
            nullable: false
            length: 255
            options:
                fixed: false
    lifecycleCallbacks: {  }

Una vez que existe el archivo orm de la tabla Categorías, puede generar automáticamente la Entidad Categorías usando:

php bin/console doctrine:generate:entities AppBundle

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

<?php
// AppBundle\Entity\Categories.php

namespace AppBundle\Entity;

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

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


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

    /**
     * Set name
     *
     * @param string $name
     *
     * @return Categories
     */
    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

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

Ahora tiene un nuevo repositorio al que se puede acceder con el archivo AppBundle:Categories. Al configurar el nuevo campo en la categoría de la tabla de citas para que tenga una ManyToOne relación con otra tabla, nuestro archivo ORM original de la tabla de citas obviamente también cambiará:

AppBundle\Entity\Appointments:
    type: entity
    table: appointments
    indexes:
        category:
            columns:
                - category
    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
    manyToOne:
        category:
            targetEntity: Categories
            cascade: {  }
            fetch: LAZY
            mappedBy: null
            inversedBy: null
            joinColumns:
                category:
                    referencedColumnName: id
            orphanRemoval: false
    lifecycleCallbacks: {  }

Y si genera la entidad nuevamente, agregará 2 nuevos métodos:

// project/AppBundle/Entity/Appointments.php

/**
    * @var \AppBundle\Entity\Categories
    */
private $category;


/**
    * Set category
    *
    * @param \AppBundle\Entity\Categories $category
    *
    * @return Appointments
    */
public function setCategory(\AppBundle\Entity\Categories $category = null)
{
    $this->category = $category;

    return $this;
}

/**
    * Get category
    *
    * @return \AppBundle\Entity\Categories
    */
public function getCategory()
{
    return $this->category;
}

Entonces ahora puede insertar un nuevo campo en la entidad de citas en el backend.

Como nuestro formulario no es un formulario de Symfony puro, sino un "formulario" creado con JavaScript por la biblioteca del programador, si desea agregar una entrada de selección que enumere todas las filas de categorías de la base de datos para que su usuario pueda seleccionar la categoría para la cita , necesitará de la misma manera que lo hizo con las citas, convertir las filas del repositorio de Categorías a JSON para que puedan ser procesadas por el programador. 

En su controlador del programador, cree un nuevo método que formatee sus categorías en JSON:

/**
    * Devuelve una cadena JSON de datos de un repositorio. La estructura puede variar según el
    * complejidad de sus formas.
    *
    * @param $categories
    */
private function formatCategoriesToJson($categories){
    $formatedCategories = array();
    
    foreach($categories as $categorie){
        array_push($formatedCategories, array(
            // ¡Importante configurar un objeto con las 2 propiedades siguientes!
            "key" => $categorie->getId(),
            "label" => $categorie->getName()
        ));
    }

    return json_encode($formatedCategories);
}

Es importante enviar un objeto con la clave de estructura y la etiqueta y nada más. Luego, debe modificar su indexAction que representa el programador, aquí envíe la estructura JSON desde los datos de su repositorio de Categorías como una variable a twig, a saber categories:

/**
 * Vista que genera el planificador.
 *
 */
public function indexAction()
{
    // Recuperar administrador de entidad
    $em = $this->getDoctrine()->getManager();
    
    // Obtener repositorio de citas
    $repositoryAppointments = $em->getRepository("AppBundle:Appointments");

    // Obtener repositorio de categorías
    $repositoryCategories = $em->getRepository("AppBundle:Categories");

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

    // Genere la estructura JSON de las citas para representar en el programador de inicio.
    $formatedAppointments = $this->formatAppointmentsToJson($appointments);

    // Recuperar los datos de las categorías del repositorio
    $categories = $repositoryCategories->findAll();

    // Generar estructura JSON a partir de los datos del repositorio (en este caso las categorías)
    // para que se puedan representar dentro de una selección en la caja de luz
    $formatedCategories = $this->formatCategoriesToJson($categories);

    // Renderizar planificador
    return $this->render("default/scheduler.html.twig", [
        'appointments' => $formatedAppointments,
        'categories' => $formatedCategories
    ]);
}

Las categorías ahora serán accesibles por Twig como una cadena, sin embargo, aún no para JavaScript, por lo que deberá exponerlas globalmente en la vista Twig para que pueda ser accesible al archivo SchedulerScripts, en este caso lo haremos a través de window.GLOBAL_CATEGORIES:

{% block javascripts %}
    <!-- Incluir la biblioteca del planificador -->
    <script src='{{ asset("libraries/dhtmlx/codebase/dhtmlxscheduler.js") }}' type='text/javascript' charset="utf-8"></script>
    
    <!-- Incluya 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>
        // Exponga las citas globalmente imprimiendo la cadena JSON con twig y el filtro raw
        // para que puedan ser accesibles por el programador Scripts.js el controlador
        window.GLOBAL_APPOINTMENTS = {{ appointments|raw }};

        // Como los scripts del planificador estarán en otros archivos, las rutas generadas por twig
        // también debe estar expuesto en la ventana
        window.GLOBAL_SCHEDULER_ROUTES = {
            create: '{{ path("scheduler_create") }}',
            update: '{{ path("scheduler_update") }}',
            delete: '{{ path("scheduler_delete") }}'
        };

        // Importante: 
        // Exponga las categorías de las Citas para que se puedan mostrar en la selección
        window.GLOBAL_CATEGORIES = {{ categories|raw }};
    </script>

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

Ahora, el objeto de categorías debe representarse en nuestro formulario para la cita en el calendario, lo que significa que debe modificar el archivo SchedulerScripts.js y modificar el paso 2 que define las secciones de la caja de luz:

// 2. Configurar secciones de Lightbox (formulario)
scheduler.config.lightbox.sections = [
    // Si tiene otro campo en su entidad de cita (por ejemplo, columna example_field), lo agregaría como
    // {name:"Example Field", height:30, map_to:"example_field", type:"textarea"},
    {name:"Title", height:30, map_to:"text", type:"textarea"},
    {name:"Description", height:30, map_to:"description", type:"textarea"},

    // Agregue una selección que le permita seleccionar la categoría de la cita según una tabla
    // "categories" from the database :)
    {name:"Category", options: window.GLOBAL_CATEGORIES , map_to: "category", type: "select", height:30 },

    // Agregar el campo de hora
    {name:"time", height:72, type:"time", map_to:"auto"},
];

Tenga en cuenta que la propiedad map_to asigna los eventos con este valor como propiedad de categoría que almacena un número simple que indica qué categoría se está utilizando. También debe modificar la getFormatedEventfunción para enviar la categoría como una propiedad; de lo contrario, este campo no se enviará cuando modifique o actualice una cita:

/**
 * 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("The ID of the event doesn't exist: " + id);
        return false;
    }
     
    var start , end;
    
    if(useJavascriptDate){
        start = event.start_date;
        end = event.end_date;
    }else{
        start = formatDate(event.start_date);
        end = formatDate(event.end_date);
    }
    
    return {
        id: event.id,
        start_date : start,
        end_date : end,
        description : event.description,
        title : event.text,

        // Importante agregar el ID de categoría
        category: event.category
    };
}

Finalmente, debe manejar los eventos en el backend (crear y actualizar) para que puedan convertirse en un objeto de categoría de tipo y la entidad de cita pueda persistir:

Nota

Esta modificación también debe realizarse en el updateAction.

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

    
    // Use el mismo formato que usa Moment.js en la vista
    $format = "d-m-Y H:i:s";

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

    // No olvide actualizar el controlador de creación o actualización con el nuevo campo
    $repositoryCategories = $em->getRepository("AppBundle:Categories");
    
    // Busque en el repositorio un objeto de categoría con el ID dado y
    // ¡Ponlo como valor!
    $appointment->setCategory(
        $repositoryCategories->find(
            $request->request->get("category")
        )
    );

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

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

Puede comprobar si la categoría existe o no para evitar errores. Ahora su programador tendría un componente de selección que le permite al usuario seleccionar la categoría de la cita:

Nota

En nuestra base de datos, la tabla de categorías solo contiene 2 filas, a saber, Cita médica y Cita de tiempo libre.

Scheduler Data from Repository inside select

Que te diviertas ❤️!


Ingeniero de Software Senior en EPAM Anywhere. Interesado en la programación desde los 14 años, Carlos es un programador autodidacta, fundador y autor de la mayoría de los artículos de Our Code World.

Conviertete en un programador más sociable

Patrocinadores