Features

  • Web application for managing doctor appointments
  • Public interface for patients (see available slots and request an appointment)
  • Doctor's management interface (edit and delete appointments)
  • Manager's managements interface (schedule shifts and create appointment slots)
  • Uses DayPilot Pro for JavaScript (trial version)
  • SQLite and MySQL database storage

License

Licensed for testing and evaluation purposes. Please see the license agreement included in the sample project. You can use the source code of the tutorial if you are a licensed user of DayPilot Pro for JavaScript. Buy a license.

Doctor Appointment Reservation User Interface

The user interface or this web application includes three independent views:

1. Patients View

html5-doctor-appointment-scheduling-javascript-php-overview-patient.png

This is the public user interface the is accessible to patients. Patients can access a weekly calendar view that displays available slots. The slots are predefined by the managers in the manager's view. Patients can request an appointment at the specified time.

The patients can't view slots that are already requested and they can't access slots in the past. They don't have access to details of other patients or general doctor availability.

When the patient clicks a free slot an appointment request modal dialog appears where they can enter their name and submit the request.

2. Doctors

html5-doctor-appointment-scheduling-javascript-php-overview-doctor.png

Doctors can see all slots (free slots and appointments) with patient details. In the doctor's view, users can modify the existing appointments (change time, appointment status, patient's name). They can also delete appointments.

The doctor's view only displays appointments and free slots for the specified doctor in a weekly calendar.

The doctors can't create new appointment slots (define shifts).

3. Managers

html5-doctor-appointment-scheduling-javascript-php-overview-manager.png

Managers can see an integrated view of all doctors and their appointments. They manage shifts and define the time slots.

The details of all free slots and booked appointments are displayed side by side for each of the doctors. This provides a quick overview of availability and workload.

The managers can see patient names but they can't edit customer details, reschedule the appointments or change the appointment status.

1. Appointment UI for Patients (index.php)

html5-doctor-appointment-scheduling-javascript-php-patient-user-interface.png

The patient interface is created using DayPilot JavaScript Calendar component.

The HTML includes a placeholder <div> element that specifies the location of the calendar on page.

HTML5

<!-- Calendar placeholder -->
<div id="calendar"></div>

The calendar component is initialized using JavaScript in a <script> section below.

JavaScript

<script>

  var calendar = new DayPilot.Calendar("calendar");
  calendar.viewType = "Week";
  calendar.init();

</script>

The calendar is switched to the week view using viewType property.

The calendar loads the available slots from the server side using a simple HTTP request that returns the slots as a JSON message:

  function loadEvents() {
      var params = {
          start: nav.visibleStart().toString(),
          end: nav.visibleEnd().toString()
      };

      $.post("backend_events_free.php", JSON.stringify(params), function(data) {
          calendar.events.list = data;
          calendar.update();
      });
  }

The backend_events.free.php script return slots that were created in advance by the shift administrator. Only the free slots are loaded (status == "free").

The time slots were created in advance when planning the shifts using the manager's interface (manager.php; see below).

<?php
require_once '_db.php';

$json = file_get_contents('php://input');
$params = json_decode($json);

$session_id = session_id();

$stmt = $db->prepare("SELECT * FROM appointment JOIN doctor ON appointment.doctor_id = doctor.doctor_id WHERE (appointment_status = 'free' OR (appointment_status <> 'free' AND appointment_patient_session = :session)) AND NOT ((appointment_end <= :start) OR (appointment_start >= :end))");
$stmt->bindParam(':start', $params->start);
$stmt->bindParam(':end', $params->end);
$stmt->bindParam(":session", $session_id);
$stmt->execute();
$result = $stmt->fetchAll();

class Event {}
class Tags {}
$events = array();

foreach($result as $row) {
  $e = new Event();
  $e->id = $row['appointment_id'];
  $e->text = "";
  $e->start = $row['appointment_start'];
  $e->end = $row['appointment_end'];
  $e->tags = new Tags();
  $e->tags->status = $row['appointment_status'];
  $e->tags->doctor = $row['doctor_name'];
  $events[] = $e;
}

header('Content-Type: application/json');
echo json_encode($events);

?>

Sample JSON response:

[
  {
    "id": "121",
    "text": "",
    "start": "2019-09-16T09:00:00",
    "end": "2019-09-16T10:00:00",
    "tags": {
      "status": "free",
      "doctor": "Doctor 1"
    }
  },
  {
    "id": "122",
    "text": "",
    "start": "2019-09-16T10:00:00",
    "end": "2019-09-16T11:00:00",
    "tags": {
      "status": "free",
      "doctor": "Doctor 1"
    }
  },
  
  // ...

]

The Calendar is read-only. Drag and drop actions (such as moving, resizing) are forbidden:

<script>

  var calendar = new DayPilot.Calendar("calendar");
  calendar.viewType = "Week";
  calendar.timeRangeSelectedHandling = "Disabled";
  calendar.eventMoveHandling = "Disabled";
  calendar.eventResizeHandling = "Disabled";
  // ...
  calendar.init();

</script>

The only action that is allowed is clicking an existing time slot:

<script>

  var calendar = new DayPilot.Calendar("calendar");

  // ...

  calendar.onEventClick = function(args) {
      if (args.e.tag("status") !== "free") {
          calendar.message("You can only request a new appointment in a free slot.");
          return;
      }

      var modal = new DayPilot.Modal({
          onClosed: function(args) {
              if (args.result) {  // args.result is empty when modal is closed without submitting
                  loadEvents();
              }
          }
      });

      modal.showUrl("appointment_request.php?id=" + args.e.id());
  };

  calendar.init();

</script>

It opens a modal dialog ("appointment_request.php") where the patient can request an appointment.

html5-doctor-appointment-scheduling-javascript-php-request.png

As soon as the appointment request is saved the slot status is changed to "waiting" and the color is changed to orange. 

In this demo, the user identification is saved using the session ID. In a standard application, you would use the user id instead. 

index.php

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8"/>
  <title>HTML5 Doctor Appointment Scheduling (JavaScript/PHP)</title>

  <link type="text/css" rel="stylesheet" href="css/layout.css"/>

  <!-- DayPilot library -->
  <script src="js/daypilot/daypilot-all.min.js"></script>
</head>
<body>
<?php require_once '_header.php'; ?>

<div class="main">
  <?php require_once '_navigation.php'; ?>

  <div>

    <div class="column-left">
      <div id="nav"></div>
    </div>
    <div class="column-main">
      <div class="toolbar">Available time slots:</div>
      <div id="calendar"></div>
    </div>

  </div>
</div>

<script src="js/jquery-1.9.1.min.js"></script>
<script src="js/daypilot/daypilot-all.min.js"></script>

<script>
  var nav = new DayPilot.Navigator("nav");
  nav.selectMode = "week";
  nav.showMonths = 3;
  nav.skipMonths = 3;
  nav.onTimeRangeSelected = function (args) {
    loadEvents(args.start.firstDayOfWeek(DayPilot.Locale.find(nav.locale).weekStarts), args.start.addDays(7));
  };
  nav.init();

  var calendar = new DayPilot.Calendar("calendar");
  calendar.viewType = "Week";
  calendar.timeRangeSelectedHandling = "Disabled";
  calendar.eventMoveHandling = "Disabled";
  calendar.eventResizeHandling = "Disabled";
  calendar.onBeforeEventRender = function (args) {
    if (!args.data.tags) {
      return;
    }
    switch (args.data.tags.status) {
      case "free":
        args.data.barColor = "green";
        args.data.html = "Available<br/>" + args.data.tags.doctor;
        args.data.toolTip = "Click to request this time slot";
        break;
      case "waiting":
        args.data.barColor = "orange";
        args.data.html = "Your appointment, waiting for confirmation";
        break;
      case "confirmed":
        args.data.barColor = "#f41616";  // red
        args.data.html = "Your appointment, confirmed";
        break;
    }
  };
  calendar.onEventClick = function (args) {
    if (args.e.tag("status") !== "free") {
      calendar.message("You can only request a new appointment in a free slot.");
      return;
    }

    var modal = new DayPilot.Modal({
      onClosed: function (args) {
        if (args.result) {  // args.result is empty when modal is closed without submitting
          loadEvents();
        }
      }
    });

    modal.showUrl("appointment_request.php?id=" + args.e.id());
  };
  calendar.init();

  loadEvents();

  function loadEvents(day) {
    var start = nav.visibleStart() > new DayPilot.Date() ? nav.visibleStart() : new DayPilot.Date();

    var params = {
      start: start.toString(),
      end: nav.visibleEnd().toString()
    };

    $.post("backend_events_free.php", JSON.stringify(params), function (data) {
      if (day) {
        calendar.startDate = day;
      }
      calendar.events.list = data;
      calendar.update();

      nav.events.list = data;
      nav.update();

    });
  }
</script>

</body>
</html>

2. Appointment Administration UI for Doctors (doctor.php)

html5-doctor-appointment-scheduling-javascript-php-doctor-user-interface.png

The doctors can use a special user interface to manage their appointments. Doctors can see all slots that are assigned to them (including free ones).

The UI layout is similar to what the patients can see but the configuration is different.

Appointment slot drag and drop operations are allowed:

calendar.onEventMoved = function(args) {
    $.post("backend_move.php", JSON.stringify(args), function(data) {
        calendar.message(data.message);
    });
};
calendar.onEventResized = function(args) {
    $.post("backend_move.php", JSON.stringify(args), function(data) {
        calendar.message(data.message);
    });
};

The slots use a color code depending on the status. The color of the appointment bar is customized using onBeforeEventRender event:

calendar.onBeforeEventRender = function(args) {
    if (!args.data.tags) {
        return;
    }
    switch (args.data.tags.status) {
        case "free":
            args.data.barColor = "green";
            break;
        case "waiting":
            args.data.barColor = "orange";
            break;
        case "confirmed":
            args.data.barColor = "#f41616";  // red
            break;
    }
};

The slots/events are loaded from a different JSON endpoint ("backend_events_doctor.php") - it returns all slots for the selected doctor (including slots from the past and existing reservations):

function loadEvents(day) {
    var params = {
        doctor: $("#doctor").val(),
        start: nav.visibleStart().toString(),
        end: nav.visibleEnd().toString()
    };

    $.post("backend_events_doctor.php", JSON.stringify(params), function(data) {
        calendar.events.list = data;
        calendar.update();
    });
} 

When the doctor clicks an appointment slot the application opens an special edit page using a modal dialog:

calendar.onEventClick = function(args) {
    var modal = new DayPilot.Modal({
        onClosed: function(args) {
            if (args.result) {  // args.result is empty when modal is closed without submitting
                loadEvents();
            }
        }
    });

    modal.showUrl("appointment_edit.php?id=" + args.e.id());
};

html5-doctor-appointment-scheduling-javascript-php-edit.png

This edit page ("appointment_edit.php") allows changing the status and the patient name. It's possible to delete the appointment slot using "Delete" button.

doctor.php

<?php
require_once '_db.php';
?>
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8"/>
  <title>HTML5 Doctor Appointment Scheduling (JavaScript/PHP)</title>

  <link type="text/css" rel="stylesheet" href="css/layout.css"/>

  <!-- DayPilot library -->
  <script src="js/daypilot/daypilot-all.min.js"></script>
</head>
<body>
<?php require_once '_header.php'; ?>

<div class="main">
  <?php require_once '_navigation.php'; ?>

  <div>

    <div class="column-left">
      <div id="nav"></div>
    </div>
    <div class="column-main">
      <div class="space">
        <select id="doctor" name="doctor">
          <?php
          foreach ($db->query('SELECT * FROM doctor ORDER BY doctor_name') as $item) {
            echo "<option value='" . $item["doctor_id"] . "'>" . $item["doctor_name"] . "</option>";
          }
          ?>
        </select>
      </div>
      <div id="calendar"></div>
    </div>

  </div>
</div>

<script src="js/jquery-1.9.1.min.js"></script>
<script src="js/daypilot/daypilot-all.min.js"></script>

<script>
  var nav = new DayPilot.Navigator("nav");
  nav.selectMode = "week";
  nav.showMonths = 3;
  nav.skipMonths = 3;
  nav.onTimeRangeSelected = function (args) {
    loadEvents(args.start.firstDayOfWeek(), args.start.addDays(7));
  };
  nav.init();

  var calendar = new DayPilot.Calendar("calendar");
  calendar.viewType = "Week";
  calendar.timeRangeSelectedHandling = "Disabled";

  calendar.onEventMoved = function (args) {
    $.post("backend_move.php", JSON.stringify(args), function (data) {
      calendar.message(data.message);
    });
  };
  calendar.onEventResized = function (args) {
    $.post("backend_move.php", JSON.stringify(args), function (data) {
      calendar.message(data.message);
    });
  };
  calendar.onBeforeEventRender = function (args) {
    if (!args.data.tags) {
      return;
    }
    switch (args.data.tags.status) {
      case "free":
        args.data.barColor = "green";
        break;
      case "waiting":
        args.data.barColor = "orange";
        break;
      case "confirmed":
        args.data.barColor = "#f41616";  // red
        break;
    }
  };

  calendar.onEventClick = function (args) {
    var modal = new DayPilot.Modal({
      onClosed: function (args) {
        if (args.result) {  // args.result is empty when modal is closed without submitting
          loadEvents();
        }
      }
    });

    modal.showUrl("appointment_edit.php?id=" + args.e.id());
  };
  calendar.init();

  loadEvents();

  function loadEvents(day) {
//                var start = nav.visibleStart() > new DayPilot.Date() ? nav.visibleStart() : new DayPilot.Date();
    var start = nav.visibleStart();

    var params = {
      doctor: $("#doctor").val(),
      start: start.toString(),
      end: nav.visibleEnd().toString()
    };

    $.post("backend_events_doctor.php", JSON.stringify(params), function (data) {
      if (day) {
        calendar.startDate = day;
      }
      calendar.events.list = data;
      calendar.update();

      nav.events.list = data;
      nav.update();
    });
  }

  $(document).ready(function () {
    $("#doctor").change(function () {
      loadEvents();
    });
  });
</script>

</body>
</html>

3. Shift Administration UI for Managers (manager.php)

html5-doctor-appointment-scheduling-javascript-php-manager-user-interface.png

The manager's view uses DayPilot JavaScript Scheduler to display appointments of all doctors side by side. The Scheduler provides a more compact view which provides a good overview.

The managers can quickly scroll to the selected date by clicking the date picker (Navigator component) on the left side.

HTML5

<!-- Scheduler placeholder -->
<div id="scheduler"></div>

JavaScript

<script>
  var scheduler = new DayPilot.Scheduler("scheduler");
  scheduler.scale = "Manual";
  scheduler.timeline = getTimeline();
  scheduler.timeHeaders = getTimeHeaders();
  scheduler.resources = [
    {"id":"1","name":"Doctor 1"},
    {"id":"2","name":"Doctor 2"},
    {"id":"3","name":"Doctor 3"},
    {"id":"4","name":"Doctor 4"},
    {"id":"5","name":"Doctor 5"}
  ];
  scheduler.init();
</script>

New appointment slots can be creating using drag and drop. As soon as you select a date range it will be filled with slots with a predefined size (1 hour by default)

html5-doctor-appointment-scheduling-javascript-php-define-slots.png

<script>
  var scheduler = new DayPilot.Scheduler("scheduler");
  
  // ...
  
  scheduler.onTimeRangeSelected = function(args) {
      var dp = scheduler;
      var scale = $("input[name=scale]:checked").val();

      var params = {
          start: args.start.toString(),
          end: args.end.toString(),
          resource: args.resource,
          scale: scale
      };

      $.post("backend_create.php", JSON.stringify(params), function(data) {
          loadEvents();
          dp.message(data.message);
      });

      dp.clearSelection();

  };
  scheduler.init();
</script>

The onTimeRangeSelected event handler calls the backend_create.php script which uses predefined logic to generate the individual time slots.

The time slot duration is defined using $slot_duration variable (the default value is 60 minutes). The time slots are only generated for the work hours which are defined using $morning_shift_starts (9 a.m.), $morning_shift_ends (1 p.m.),   $afternoon_shift_starts (2 p.m.), and $afternoon_shift_ends (6 p.m.). As you can see the time slots are not created for the lunch break which is from 1 p.m. to 2 p.m.

Each time slot is saved as a separate record in the database ("appointment" table). The appointment status is set to "free".

<?php
require_once '_db.php';

$json = file_get_contents('php://input');
$params = json_decode($json);

$received_range_start = $params->start;
$received_range_end = $params->end;

$start = new DateTime($received_range_start);
$start_day = clone $start;
$start_day->setTime(0, 0, 0);

$end = new DateTime($received_range_end);
$end_day = clone $end;
$end_day->setTime(0, 0, 0);

$days = $end_day->diff($start_day)->days;

if ($end > $end_day) {
  $days += 1;
}

$scale = $params->scale;

$timeline = load_timeline();

$slot_duration = 60;
$doctor_id = $params->resource;

foreach ($timeline as $cell) {
  if ($start <= $cell->start && $cell->end <= $end) {
    for ($shift_start = clone $cell->start; $shift_start < $cell->end; $shift_start->add(new DateInterval("PT" . $slot_duration . "M"))) {
      $shift_end = clone $shift_start;
      $shift_end->add(new DateInterval("PT" . $slot_duration . "M"));
      create_shift($shift_start->format("Y-m-d\\TH:i:s"), $shift_end->format("Y-m-d\\TH:i:s"), $doctor_id);
    }
  }
}

function create_shift($start, $end, $doctor)
{
  global $db;
  $stmt = $db->prepare("INSERT INTO appointment (appointment_start, appointment_end, doctor_id) VALUES (:start, :end, :doctor)");
  $stmt->bindParam(':start', $start);
  $stmt->bindParam(':end', $end);
  $stmt->bindParam(':doctor', $doctor);
  $stmt->execute();
}

class TimeCell
{
}

function load_timeline()
{
  global $scale, $days, $start_day;

  $morning_shift_starts = 9;
  $morning_shift_ends = 13;
  $afternoon_shift_starts = 14;
  $afternoon_shift_ends = 18;

  switch ($scale) {
    case "hours":
      $increment_morning = 1;
      $increment_afternoon = 1;
      break;
    case "shifts":
      $increment_morning = $morning_shift_ends - $morning_shift_starts;
      $increment_afternoon = $afternoon_shift_ends - $afternoon_shift_starts;
      break;
    default:
      die("Invalid scale");
  }

  $timeline = array();

  for ($i = 0; $i < $days; $i++) {
    $day = clone $start_day;
    $day->add(new DateInterval("P" . $i . "D"));

    for ($x = $morning_shift_starts; $x < $morning_shift_ends; $x += $increment_morning) {
      $cell = new TimeCell();

      $from = clone $day;
      $from->add(new DateInterval("PT" . $x . "H"));

      $to = clone $day;
      $to->add(new DateInterval("PT" . ($x + $increment_morning) . "H"));

      $cell->start = $from;
      $cell->end = $to;
      $timeline[] = $cell;
    }

    for ($x = $afternoon_shift_starts; $x < $afternoon_shift_ends; $x += $increment_afternoon) {
      $cell = new TimeCell();

      $from = clone $day;
      $from->add(new DateInterval("PT" . $x . "H"));

      $to = clone $day;
      $to->add(new DateInterval("PT" . ($x + $increment_afternoon) . "H"));

      $cell->start = $from;
      $cell->end = $to;
      $timeline[] = $cell;
    }

  }

  return $timeline;
}

class Result
{
}

$response = new Result();
$response->result = 'OK';
$response->message = 'Shifts created';
//$response->id = $db->lastInsertId();

header('Content-Type: application/json');
echo json_encode($response);

?>

The appointment slots can be deleted using a built-in "delete" icon:

html5-doctor-appointment-scheduling-javascript-php-delete-slot.png

<script>
  var scheduler = new DayPilot.Scheduler("scheduler");
  
  // ...
  
  scheduler.onEventDeleted = function(args) {
      var params = {
          id: args.e.id(),
      };
      $.post("backend_delete.php", JSON.stringify(params), function(data) {
          scheduler.message("Deleted.");
      });
  };

  scheduler.init();
</script>

The appointment status is highlighted using a custom duration bar color (green/orange/red):

<script>
  var scheduler = new DayPilot.Scheduler("scheduler");
  
  // ...
  
  scheduler.onBeforeEventRender = function(args) {
      switch (args.data.tags.status) {
          case "free":
              args.data.barColor = "green";
              args.data.deleteDisabled = $('input[name=scale]:checked').val() === "shifts";  // only allow deleting in the more detailed hour scale mode
              break;
          case "waiting":
              args.data.barColor = "orange";
              args.data.deleteDisabled = true;
              break;
          case "confirmed":
              args.data.barColor = "#f41616";  // red
              args.data.deleteDisabled = true;
              break;
      }
  };

  scheduler.init();
</script>

manager.php

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8"/>
  <title>HTML5 Doctor Appointment Scheduling (JavaScript/PHP)</title>

  <link type="text/css" rel="stylesheet" href="css/layout.css"/>

  <!-- DayPilot library -->
  <script src="js/daypilot/daypilot-all.min.js"></script>
</head>
<body>
<?php require_once '_header.php'; ?>

<div class="main">
  <?php require_once '_navigation.php'; ?>

  <div>

    <div class="column-left">
      <div id="nav"></div>
    </div>
    <div class="column-main">

      <div class="toolbar">
                    <span class="toolbar-item">Scale:
                        <label for='scale-15min'><input type="radio" value="15min" name="scale" id='scale-15min'> 15-Min</label>
                        <label for='scale-hours'><input type="radio" value="hours" name="scale" id='scale-hours'
                                                        checked> Hours</label>
                        <label for='scale-shifts'><input type="radio" value="shifts" name="scale" id='scale-shifts'> Shifts</label></span>
        <span class="toolbar-item"><label for="business-only"><input type="checkbox" id="business-only"> Hide non-business hours</label></span>
        <span class="toolbar-item">Slots: <button id="clear">Clear</button> Deletes all free slots this month</span>

      </div>

      <div id="scheduler"></div>
    </div>

  </div>
</div>

<script src="js/jquery-1.9.1.min.js"></script>
<script src="js/daypilot/daypilot-all.min.js"></script>

<script>
  var nav = new DayPilot.Navigator("nav");
  nav.selectMode = "month";
  nav.showMonths = 3;
  nav.skipMonths = 3;
  nav.onTimeRangeSelected = function (args) {
    //loadEvents(args.start.firstDayOfWeek(), args.start.addDays(7));
    if (scheduler.visibleStart().getDatePart() <= args.day && args.day < scheduler.visibleEnd()) {
      scheduler.scrollTo(args.day, "fast");  // just scroll
    } else {
      loadEvents(args.day);  // reload and scroll
    }
  };
  nav.init();

  var scheduler = new DayPilot.Scheduler("scheduler");
  scheduler.visible = false; // will be displayed after loading the resources
  scheduler.scale = "Manual";
  scheduler.timeline = getTimeline();
  scheduler.timeHeaders = getTimeHeaders();
  scheduler.useEventBoxes = "Never";
  scheduler.eventDeleteHandling = "Update";
  scheduler.eventClickHandling = "Disabled";
  scheduler.eventMoveHandling = "Disabled";
  scheduler.eventResizeHandling = "Disabled";
  scheduler.allowEventOverlap = false,
    scheduler.onBeforeTimeHeaderRender = function (args) {
      args.header.html = args.header.html.replace(" AM", "a").replace(" PM", "p");  // shorten the hour header
    };
  scheduler.onBeforeEventRender = function (args) {
    switch (args.data.tags.status) {
      case "free":
        args.data.barColor = "green";
        args.data.deleteDisabled = $('input[name=scale]:checked').val() === "shifts";  // only allow deleting in the more detailed hour scale mode
        break;
      case "waiting":
        args.data.barColor = "orange";
        args.data.deleteDisabled = true;
        break;
      case "confirmed":
        args.data.barColor = "#f41616";  // red
        args.data.deleteDisabled = true;
        break;
    }
  };
  scheduler.onEventDeleted = function (args) {
    var params = {
      id: args.e.id(),
    };
    $.post("backend_delete.php", JSON.stringify(params), function (data) {
      scheduler.message("Deleted.");
    });
  };

  scheduler.onTimeRangeSelected = function (args) {
    var dp = scheduler;
    var scale = $("input[name=scale]:checked").val();

    var params = {
      start: args.start.toString(),
      end: args.end.toString(),
      resource: args.resource,
      scale: scale
    };

    $.post("backend_create.php", JSON.stringify(params), function (data) {
      loadEvents();
      dp.message(data.message);
    });

    dp.clearSelection();

  };
  scheduler.init();


  loadResources();
  loadEvents(DayPilot.Date.today());

  function loadEvents(day) {
    var from = scheduler.visibleStart();
    var to = scheduler.visibleEnd();
    if (day) {
      from = new DayPilot.Date(day).firstDayOfMonth();
      to = from.addMonths(1);
    }

    var params = {
      start: from.toString(),
      end: to.toString()
    };

    $.post("backend_events.php", JSON.stringify(params), function (data) {
      scheduler.timeline = getTimeline(day);
      scheduler.events.list = data;
      scheduler.update();
      scheduler.scrollTo(day, "fast", "left");

      nav.events.list = data;
      nav.update();
    });
  }

  function loadResources() {
    $.post("backend_resources.php", function (data) {
      scheduler.resources = data;
      scheduler.visible = true;
      scheduler.update();
    });
  }

  function getTimeline(date) {
    var date = date || DayPilot.Date.today();
    var start = new DayPilot.Date(date).firstDayOfMonth();
    var days = start.daysInMonth();
    var scale = $("input[name=scale]:checked").val();
    var businessOnly = $("#business-only").prop("checked");

    var morningShiftStarts = 9;
    var morningShiftEnds = 13;
    var afternoonShiftStarts = 14;
    var afternoonShiftEnds = 18;

    if (!businessOnly) {
      var morningShiftStarts = 0;
      var morningShiftEnds = 12;
      var afternoonShiftStarts = 12;
      var afternoonShiftEnds = 24;
    }

    var timeline = [];

    var increaseMorning;  // in hours
    var increaseAfternoon;  // in hours
    switch (scale) {
      case "15min":
        increaseMorning = 0.25;
        increaseAfternoon = 0.25;
        break;
      case "hours":
        increaseMorning = 1;
        increaseAfternoon = 1;
        break;
      case "shifts":
        increaseMorning = morningShiftEnds - morningShiftStarts;
        increaseAfternoon = afternoonShiftEnds - afternoonShiftStarts;
        break;
      default:
        throw "Invalid scale value";
    }

    for (var i = 0; i < days; i++) {
      var day = start.addDays(i);

      for (var x = morningShiftStarts; x < morningShiftEnds; x += increaseMorning) {
        timeline.push({start: day.addHours(x), end: day.addHours(x + increaseMorning)});
      }
      for (var x = afternoonShiftStarts; x < afternoonShiftEnds; x += increaseAfternoon) {
        timeline.push({start: day.addHours(x), end: day.addHours(x + increaseAfternoon)});
      }
    }

    return timeline;
  }

  function getTimeHeaders() {
    var scale = $('input[name=scale]:checked').val();
    switch (scale) {
      case "15min":
        return [{groupBy: "Month"}, {groupBy: "Day", format: "dddd d"}, {
          groupBy: "Hour",
          format: "h tt"
        }, {groupBy: "Cell", format: "m"}];
        break;
      case "hours":
        return [{groupBy: "Month"}, {groupBy: "Day", format: "dddd d"}, {groupBy: "Hour", format: "h tt"}];
        break;
      case "shifts":
        return [{groupBy: "Month"}, {groupBy: "Day", format: "dddd d"}, {groupBy: "Cell", format: "tt"}];
        break;
    }
  }

  $(document).ready(function () {
    $("#business-only").click(function () {
      scheduler.timeline = getTimeline();
      scheduler.update();
    });
    $("input[name=scale]").click(function () {
      scheduler.timeline = getTimeline();
      scheduler.timeHeaders = getTimeHeaders();
      scheduler.update();
    });
    $("#clear").click(function () {
      var dp = scheduler;
      var params = {
        start: dp.visibleStart(),
        end: dp.visibleEnd()
      };
      $.post("backend_clear.php", JSON.stringify(params), function (data) {
        dp.message(data.message);
        loadEvents();
      });
    });
  });

</script>

</body>
</html>

Backend Database

The tutorial uses a simple database with just two tables ('appointment', 'doctor').

By default, it uses SQLite storage:

  • The database is stored in daypilot.sqlite file in the application root directory.
  • Make sure the application has permissions to write to the database file.
  • You can use SQLiteStudio or a similar to view and edit the database.

SQLite Database Schema

CREATE TABLE appointment (
    appointment_id              INTEGER       PRIMARY KEY AUTOINCREMENT
                                              NOT NULL,
    appointment_start           DATETIME      NOT NULL,
    appointment_end             DATETIME      NOT NULL,
    appointment_patient_name    VARCHAR (100),
    appointment_status          VARCHAR (100) DEFAULT ('free') 
                                              NOT NULL,
    appointment_patient_session VARCHAR (100),
    doctor_id                   INTEGER       NOT NULL
);

CREATE TABLE doctor (
    doctor_id   INTEGER       PRIMARY KEY AUTOINCREMENT
                              NOT NULL,
    doctor_name VARCHAR (100) NOT NULL
);

Using MySQL

In the application root, there are three database-related files:

  • _db.php - the configuration currently in use
  • _db_sqlite.php - SQLite configuration
  • _db_mysql.php - MySQL configuration

If you want to switch from SQLite (default configuration) to MySQL you'll need to do the following:

1. Edit _db.php and change the require_once() logic as follows:

<?php

// use sqlite
require_once '_db_sqlite.php';

// use MySQL
//require_once '_db_mysql.php';

2 Edit _db_mysql.php and adjust your database connection settings:

<?php
$host = "127.0.0.1";
$port = 3306;
$username = "username";   // change the user name
$password = "password";   // change the password
$database = "doctor";

The script will create the database ('doctor' by default) automatically if it doesn't exist, create the required tables and add some sample data to the 'doctor' table.

MySQL Database Schema

"appointment" table:

CREATE TABLE `appointment` (
	`appointment_id` INT(11) NOT NULL AUTO_INCREMENT,
	`appointment_start` DATETIME NOT NULL,
	`appointment_end` DATETIME NOT NULL,
	`appointment_patient_name` VARCHAR(100) NULL DEFAULT NULL,
	`appointment_status` VARCHAR(100) NOT NULL DEFAULT 'free',
	`appointment_patient_session` VARCHAR(100) NULL DEFAULT NULL,
	`doctor_id` INT(11) NOT NULL,
	PRIMARY KEY (`appointment_id`)
);

"doctor" table:

CREATE TABLE `doctor` (
	`doctor_id` INT(11) NOT NULL AUTO_INCREMENT,
	`doctor_name` VARCHAR(100) NOT NULL,
	PRIMARY KEY (`doctor_id`)
);

History

  • September 12, 2019: DayPilot Pro for JavaScript upgraded to 2019.3.4001, layout improvements. Time slot generation logic explained.
  • February 22, 2017: DayPilot Pro version upgraded to 8.3.2709. Doctor view displaying slots from the past as well. 
  • June 2, 2016: MySQL support added
  • April 20, 2016: Initial release