Gestión de Cita Previa

En la situación actual de pandemia mundial, casi todas las empresas y servicios públicos han tenido que recurrir a una gestión de citas para mantener la distancia social entre las personas.

Este ejemplo es un posible sistema de gestión de Cita Previa o al menos, así lo he querido identificar yo.

El ejemplo más sencillo es pensar en las necesidades de los servicios médicos, aunque he intentado que pudiera ser válido para cualquier otra necesidad.

Requisitos funcionales

Cuando he estado pensando qué es lo que una empresa requiere para esta gestión, he definido:

  • He creado un “Objeto de negocio”, que le he llamado “Reserva”, que es la unidad a la que vamos a asociar los huecos de la gestión de citas. Este “Objeto”, puede ser un Médico, un Servicio Médico, una Unidad de Registro, un Agente bancario, etc., cualquier Objeto que requiera una lista de personas a las que atender.
  • Estas “Reservas” estarán asociadas a 2 niveles de agrupación. En el sistema he asociado “Compañía” y “Departamento”, pero podría ser cualquier otra estructura.
  • El registro de “Reserva” tiene que tener los datos que se requieran para establecer los huecos del calendario de la Cita Previa.
  • El análisis de huecos disponibles debe ser muy fácil de identificar y gestionar, con posibilidad de que se pueda facilitar en un sistema de autogestión (que el usuario solicite la fecha y hora de cita). Se debe ofrecer la posibilidad de solicitar fecha de cita para cualquier día y hora de las disponibles.
  • También, el sistema debe ofrecer listas o conteos, de os huecos o citas que se han cerrado a la fecha que se requiera.

Solución

DEMO : http://fhumanes.com/citations

Usuario y password:  admin/admin

El ejemplo funciona para los idiomas inglés y español.

Una vez que nos hayamos identificado aparecerá el menú con todas las opciones.

Las principales opciones son las que están en el grupo “Citations” y desde cualquiera de ellas se podrá ir a la gestión de reserva de la Cita y del listado de las citas acordadas.

Las “Reservas” requieren un nombre corto o clave, un color para su identificación, su nombre  y unos datos que se requieren para la planificación de las agendas:

  • Fecha inicial o alta del recurso. Antes de esta fecha no se puede agendar nada.
  • Fecha final. Después de esta fecha no se puede agendar nada.
  • Hora de comienzo de la agenda. Este y los 2 siguientes datos fijan horario de agenda.
  • Hora de final de la agenda.
  • Tiempo que durará los huecos de la agenda.

Los registros de “Planificación” definen los rangos de fecha que se establecen para crear la agenda.  Para cada “Reserva”, se debe establecer una fecha de inicio y fecha final. Hay controles para que no haya solapamientos de fechas y para que estás no excedan de la fecha de la “Reserva”.

Mientras no está activa, se podrá modificar e incluso borrar, pero una vez que esté activa (y se haya creado la agenda) ya no deja modificar ni borrar la Planificación.

La agenda o hueco de cita, tiene datos básicos y quedará por completar 3 campos:

  • Nombre .-Datos de la persona que ha suscrito la cita.
  • Descripción.- Cualquier otro dato que se aporte a la cita.
  • Ocupado?.- Cuando el hueco de la agenda ha sido ocupado (de forma automática) se pone a 1.

En las tablas de “Compañías”, “Departamentos” y “Reservas”, todas ellas disponen de un botón “Calendario”, que nos lleva a la presentación de la agenda con los huecos disponibles. Las citas disponibles serán de las “Reservas” de acuerdo a donde hayamos pulsado el botón. Nos pueden interesar las citas disponibles de un médico, de un servicio, de un departamento o de un hospital.

Nos vamos a poder desplazar por un calendario en donde nos va a facilitar información de las citas que están libres. Muestra las 3 primeras disponibles y el número total de las que no puede mostrar. Pulsando en el enlace de las otras nos aparece una lista de todas las citas disponibles para ese día.

Sólo requiere hacer clic sobre una de ella, para iniciar la solicitud de esa cita.

En la ventana “popup” que nos aparece cuando se selecciona una cita, aparecen los datos básicos de la agenda y la fecha y hora de la cita, permitiendo que introduzcamos el nombre de la persona de la cita y un campo de descripción que puede cumplimentarse con el objeto de la cita.

Al igual que hemos podido ver las agendas disponibles por “Compañía”, “Departamento” y “Reserva”, se puede obtener la relación de citas agendadas, para si se requiere, obtener un listado de las mismas.

Solución Técnica

He utilizado 2 plugines que podéis descargar de mi portal (Color y Toogle).

Para la presentación del calendario he utilizado la librería de JavaScript FullCalendar https://fullcalendar.io/. Una buenísima librería para todo lo que tenga que ver con calendarios y planificaciones.

Explico brevemente algunos códigos, para que veáis la simplicidad del ejemplo.

control_plannig.php – Controles de fechas para validar la planificación

<?php
// Planning range control
$idreserves = $values['reserves_idreserves']; // Master Reserves
// Recover data of Reserves
$sql = "SELECT idreserves, start_date, end_date, active FROM reserves where idreserves = $idreserves"; 
$resql = db_query($sql,$conn);
if ($resql->num_rows <> 0) { // Found                         
            $master = db_fetch_array($resql); 
            $master_start_date = $master['start_date'];
            $master_end_date = $master['end_date'];
            $master_active = $master['active'];
            } else {
            $message = GetCustomLabel("MASTER_NOT_FOUND");
            $return = false;
            return;
            }
if ( $master_active == 0 ){
            $message = GetCustomLabel("MASTER_NOT_ACTIVE");
            $return = false;
            return;
            }
if ( !($values['start_date'] >= $master_start_date and $values['end_date'] <= $master_end_date)) {
            $message = GetCustomLabel("MASTER_NOT_RANGE_DATE");
            $return = false;
            return;
            }
if ( !isset($keys['idplanning']) ){  // Is Edit, Key of record
            $idplanning = 0;
            } else {
            $idplanning = $keys['idplanning'];
            }
if ( !($values['start_date'] <= $values['end_date']) ) {
            $message = GetCustomLabel("RANGE_DATES_DUPLICATED");
            $return = false;
            return;
            }
$start_date = $values['start_date'];
$end_date = $values['end_date'];
//  Check if date range is not duplicated
$sql = "
select idplanning from planning
where reserves_idreserves= $idreserves
and idplanning <> $idplanning
and  ( start_date <= '$end_date' and end_date >= '$start_date' )
"; 
$resql = db_query($sql,$conn);
if ($resql->num_rows <> 0) { // Found                         
            $message = GetCustomLabel("RANGE_DATES_DUPLICATED");
            $return = false;
            return;
            }

 

create_planning.php  – Crear las agendas desde los datos de planificación y reservas.

<?php
// Create insert into Citation form planning
$idreserves = $values['reserves_idreserves']; // Master Reserves
$idplanning = $values['idplanning']; // Planing
$iduser = $_SESSION['user_id']; //id of Users
$now = now();
// Recover data of Reserves
$sql = "SELECT idreserves, weekends, from_time, to_time, time_by_citation FROM reserves where idreserves = $idreserves"; 
$resql = db_query($sql,$conn);
if ($resql->num_rows <> 0) { // Found                         
            $master = db_fetch_array($resql); 
            $master_weekends = $master['weekends'];
            $master_from_time = $master['from_time'];
            $master_to_time = $master['to_time'];
            $master_time_citation = $master['time_by_citation'];
            } else {
            $message = GetCustomLabel("MASTER_NOT_FOUND");
            die($message);
            }
$start_date = date('Y-m-d', strtotime($values['start_date'])).' '.$master_from_time;
$end_date   = date('Y-m-d', strtotime($values['end_date'])).' '.$master_to_time;  
$temp = explode(":", $master_time_citation);
$increment_hour = $temp[0]+0; // Hour
$increment_minute = $temp[1]+0; // Minutes
$increment = '';
if ($increment_hour <> 0 ) { $increment .= ' +'.$increment_hour.' hour'; }
if ($increment_minute <> 0 ) { $increment .= ' +'.$increment_minute.' minutes'; }
$i = $start_date;
while ($i <= $end_date ) { // Loop increment Citations
      $day_week = date('N', strtotime($i));  // 1 =domingo y 7 = sábado
      If ( $master_weekends == 0 && ( $day_week == 6 || $day_week == 7 )) { // It is the weekend and there is no work on the weekend
                  $i = date('Y-m-d H:i', strtotime($i. ' +1 day'));
                  continue;
                  }
$j = date('Y-m-d H:i', strtotime($i. $increment));
$time_i = substr($i, -5, 5); 
$time_j = substr($j, -5, 5); 
if ($time_j > $master_to_time ) { // new day
            $i = date('Y-m-d', strtotime($i. ' +1 day')).' '.$master_from_time;
            continue;
            }
                 
// INSERT in DB the citations
$sql=" 
INSERT into citations
       (reserves_idreserves, planning_idplanning, short_name, start_date, end_date, created, updated, creator, modifier)
values ($idreserves,$idplanning,'$time_i','$i','$j', '$now' , '$now' ,$iduser, $iduser)      
 ";
$res=db_exec($sql,$conn);
 $i = $j;              
}

calendar.php – Para mostrar la rejilla del calendario

<?php
global $conn;
/* Session variable
$_SESSION['reserve_id']
$_SESSION['option_calendar'] valor:  reserve | company | departament 
$_SESSION['language']
$_SESSION['user_id']
$_SESSION['login'] 
$_SESSION['email'] 
$_SESSION['username']
$_SESSION['company_id']
$_SESSION['dept_id']
 */
if ($_SESSION['language'] == 'Spanish') {
    $language = 'es'; // Spanish
            $firstDay = '1'; // Lunes
} else {
    $language = 'en'; // English
            $firstDay = '0'; // Sun
}
$now = substr(now(),0,10);
$str1 = <<<EOD
 <style>
    html, body {
      margin: 0;
      padding: 0;
      font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
      font-size: 12px; 
    }
    #calendar {
      max-width: 900px;
      margin: 40px auto;
    }
  </style>
<meta charset='utf-8' />
<link href='fullcalendar/packages/core/main.css' rel='stylesheet' />
<link href='fullcalendar/packages/daygrid/main.css' rel='stylesheet' />
<link href='fullcalendar/packages/timegrid/main.css' rel='stylesheet' />
<link href='fullcalendar/packages/list/main.css' rel='stylesheet' />
<script src='fullcalendar/packages/core/main.js'></script>
<script src='fullcalendar/packages/interaction/main.js'></script>
<script src='fullcalendar/packages/daygrid/main.js'></script>
<script src='fullcalendar/packages/timegrid/main.js'></script>
<script src='fullcalendar/packages/list/main.js'></script>
<script src='fullcalendar/packages/core/locales-all.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', 'dayGrid', 'timeGrid', 'list' ],
      header: {
        left: 'prev,next today',
        center: 'title',
                         right: 'dayGridMonth,listWeek'
        // right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek'
      },
      // defaultView: 'dayGridMonth',
      defaultDate: '$now',
      // timeZone: 'Europe/Madrid', // the default 'local' (unnecessary to specify)
      locale: initialLocaleCode,
      businessHours: {
      // days of week. an array of zero-based day of week integers (0=Sunday)
      daysOfWeek: [ 1, 2, 3, 4, 5 ], // Monday - Thursday
      startTime: '08:00', // a start time (08am in this example)
      endTime: '20:00', // an end time (8pm in this example)
        },
         
      weekNumbers: true,
      weekNumbersWithinDays: true,
      // weekNumberCalculation: 'ISO',
       
      editable: true,
      navLinks: true, // can click day/week names to navigate views
      eventLimit: true, // allow "more" link when too many events
       
      eventClick: function(info) {
                        var citation = info.event.id;
                        var title = info.event.title;
                        var url = "timetable_edit.php?editid1="+citation;
                        var header = '<h2 data-itemtype="view_header" data-itemid="view_header" data-pageid="10">'+'Citation: '+ title+'</h2>' ;
/*
                        var win = Runner.displayPopup( {
                                                url: url,
                                                width: 800,
                                                height: 500,
                                                header: header
                        });
*/
                        window.popup = Runner.displayPopup({
                    url: url,
                    width: 800,
                    height: 500,
                    header: header
            }); 
      },
      events: {
        url: 'calendar_ajax_own.php'
      }
         
    });
    calendar.render();
  });
</script>
  <div id='calendar'></div>
EOD;
echo $str1;
?>

calendar_ajax_own.php – Para cargar dinámicamente las citas disponibles.

<?php
require_once("include/dbcommon.php"); // DataBase PHPRunner
/* Session variable
$_SESSION['option_calendar'] valor:  project | user | company | departament | meeting
$_SESSION['language']
$_SESSION['user_id']
$_SESSION['reserve_id']
$_SESSION['login'] 
$_SESSION['email'] 
$_SESSION['username']
$_SESSION['company_id']
$_SESSION['dept_id']
 * ----------------------------------------------
 */
// 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 ='';
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;
            }
switch ($_SESSION['option_calendar']) {
    case 'reserve':
        $sql = "SELECT idcitations id, citations.short_name name,  reserves.short_name resource, citations.start_date, citations.end_date, reserves.color
            FROM citations
            join reserves on (reserves_idreserves = idreserves)
            where citations.status = 0 and
            reserves_idreserves =" . $_SESSION['reserve_id'] . " and  citations.end_date >= '$start' and citations.start_date < '$end'";
        break;
    case 'company':
        $sql = "SELECT idcitations id, citations.short_name name,  reserves.short_name resource, citations.start_date, citations.end_date, reserves.color
            FROM citations
            join reserves on (reserves_idreserves = idreserves)
    join companies on (reserves.companies_company_id = companies.company_id)
            where citations.status = 0 
            and companies.company_id =" . $_SESSION['company_id'] . " and  citations.end_date >= '$start' and citations.start_date < '$end'";
        break;
    case 'departament':
        $sql = "SELECT idcitations id, citations.short_name name,  reserves.short_name resource, citations.start_date, citations.end_date, reserves.color
            FROM citations
            join reserves on (reserves_idreserves = idreserves)
    join departments on (reserves.departments_dept_id = departments.dept_id)
            where citations.status = 0
            and departments.dept_id =" . $_SESSION['departament_id'] . " and  citations.end_date >= '$start' and citations.start_date < '$end'";
        break;
}
$resql = db_query($sql,$conn);
// Accumulate an output array of event data arrays.
$output_arrays = array();
while ($data = db_fetch_array($resql)){
$output_arrays[] = array(
    'id'=>$data['id'],
    'title'=>$data['name'],
    'resource'=>$data['resource'],
    'start'=>$data['start_date'],
    'end'=>$data['end_date'],
    'color'=>$data['color']);
    }
// Send JSON to the client.
// $str = json_encode($output_arrays);
echo json_encode($output_arrays);
?>

Como siempre, podréis contactar conmigo, para cualquier necesidad en [email protected]

También, como es habitual, os dejo todos los ficheros que necesitáis para que instaléis el ejemplo en vuestros equipos.

Adjuntos

Archivo Tamaño de archivo Descargas
zip Backup de la Base de Datos 78 KB 729
zip PHPRunner 10.4 y Mysql WorkBench 21 MB 918

Blog personal para facilitar soporte gratuito a usuarios de PHPRunner