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)

  • MySQL database storage

  • SQLite database for easy testing

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(day) {
  var start = nav.visibleStart() > new DayPilot.Date() ? nav.visibleStart() : new DayPilot.Date();

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

  DayPilot.Http.ajax({
    url: "backend_events_free.php",
    data: params,
    success: function(ajax) {
      var data = ajax.data;

      if (day) {
        calendar.startDate = day;
      }
      calendar.events.list = data;
      calendar.update();

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

    }
  });
}

The backend_events_free.php script returns 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": "2020-09-16T09:00:00",
    "end": "2020-09-16T10:00:00",
    "tags": {
      "status": "free",
      "doctor": "Doctor 1"
    }
  },
  {
    "id": "122",
    "text": "",
    "start": "2020-09-16T10:00:00",
    "end": "2020-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 form = [
      {name: "Request an Appointment"},
      {name: "From", id: "start", dateFormat: "MMMM d, yyyy h:mm tt", disabled: true},
      {name: "To", id: "end", dateFormat: "MMMM d, yyyy h:mm tt", disabled: true},
      {name: "Name", id: "name"},
    ];

    var data = {
      id: args.e.id(),
      start: args.e.start(),
      end: args.e.end(),
    };

    var options = {
      focus: "name"
    };

    DayPilot.Modal.form(form, data, options).then(function(modal) {
        if (modal.canceled) {
          return;
        }

        DayPilot.Http.ajax({
          url: "backend_request_save.php",
          data: modal.result,
          success: function(ajax) {
            args.e.data.tags.status = "waiting";
            calendar.events.update(args.e.data);
          }
        })
    });

  };


  calendar.init();

</script>

It opens a modal dialog where the patient can request an appointment. The modal dialog is built dynamically using JavaScript (see also JavaScript Scheduler: How to Edit Multiple Fields using a Modal Dialog that explains how to built modal dialogs using DayPilot.Modal.form() method).

html5-doctor-appointment-scheduling-javascript-php-request-time-slot.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/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.eventArrangement = "SideBySide";
  calendar.onBeforeEventRender = function (args) {
    if (!args.data.tags) {
      return;
    }
    switch (args.data.tags.status) {
      case "free":
        args.data.backColor = "#3d85c6";  // blue
        args.data.barHidden = true;
        args.data.borderColor = "darker";
        args.data.fontColor = "white";
        args.data.html = "Available<br/>" + args.data.tags.doctor;
        args.data.toolTip = "Click to request this time slot";
        break;
      case "waiting":
        args.data.backColor = "#e69138";  // orange
        args.data.barHidden = true;
        args.data.borderColor = "darker";
        args.data.fontColor = "white";
        args.data.html = "Your appointment, waiting for confirmation";
        break;
      case "confirmed":
        args.data.backColor = "#6aa84f";  // green
        args.data.barHidden = true;
        args.data.borderColor = "darker";
        args.data.fontColor = "white";
        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 form = [
      {name: "Request an Appointment"},
      {name: "From", id: "start", dateFormat: "MMMM d, yyyy h:mm tt", disabled: true},
      {name: "To", id: "end", dateFormat: "MMMM d, yyyy h:mm tt", disabled: true},
      {name: "Name", id: "name"},
    ];

    var data = {
      id: args.e.id(),
      start: args.e.start(),
      end: args.e.end(),
    };

    var options = {
      focus: "name"
    };

    DayPilot.Modal.form(form, data, options).then(function(modal) {
        if (modal.canceled) {
          return;
        }

        DayPilot.Http.ajax({
          url: "backend_request_save.php",
          data: modal.result,
          success: function(ajax) {
            args.e.data.tags.status = "waiting";
            calendar.events.update(args.e.data);
          }
        })
    });

  };
  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()
    };

    DayPilot.Http.ajax({
      url: "backend_events_free.php",
      data: params,
      success: function(ajax) {
        var data = ajax.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) {
  DayPilot.Http.ajax({
    url: "backend_move.php",
    data: args,
    success: function(ajax) {
      calendar.message(ajax.data.message);
    }
  });
};
calendar.onEventResized = function (args) {
  DayPilot.Http.ajax({
    url: "backend_move.php",
    data: args,
    success: function(ajax) {
      calendar.message(ajax.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.backColor = "#3d85c6";  // blue
      args.data.barHidden = true;
      args.data.borderColor = "darker";
      args.data.fontColor = "white";
      break;
    case "waiting":
      args.data.backColor = "#e69138";  // orange
      args.data.barHidden = true;
      args.data.borderColor = "darker";
      args.data.fontColor = "white";
      break;
    case "confirmed":
      args.data.backColor = "#6aa84f";  // green
      args.data.barHidden = true;
      args.data.borderColor = "darker";
      args.data.fontColor = "white";
      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 start = nav.visibleStart();
  var end = nav.visibleEnd();

  var params = {
    doctor: elements.doctor.value,
    start: start.toString(),
    end: end.toString()
  };

  DayPilot.Http.ajax({
    url: "backend_events_doctor.php",
    data: params,
    success: function(ajax) {
      var data = ajax.data;
      if (day) {
        calendar.startDate = day;
      }
      calendar.events.list = data;
      calendar.update();

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

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

calendar.onEventClick = function (args) {

  var form = [
    {name: "Edit Appointment"},
    {name: "Name", id: "text"},
    {name: "Status", id: "tags.status", options: [
        {name: "Free", id: "free"},
        {name: "Waiting", id: "waiting"},
        {name: "Confirmed", id: "confirmed"},
      ]},
    {name: "From", id: "start", dateFormat: "MMMM d, yyyy h:mm tt", disabled: true},
    {name: "To", id: "end", dateFormat: "MMMM d, yyyy h:mm tt", disabled: true},
    {name: "Doctor", id: "resource", disabled: true, options: doctors},
  ];

  var data = args.e.data;

  var options = {
    focus: "text"
  };

  DayPilot.Modal.form(form, data, options).then(function(modal) {
    if (modal.canceled) {
      return;
    }

    var params = {
      id: modal.result.id,
      name: modal.result.text,
      status: modal.result.tags.status
    };

    DayPilot.Http.ajax({
      url: "backend_update.php",
      data: params,
      success: function(ajax) {
        calendar.events.update(modal.result);
      }
    });
  });


};

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

This edit page allows changing the status and the patient name.

doctor.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"></select>
      </div>
      <div id="calendar"></div>
    </div>

  </div>
</div>

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

<script>
  var elements = {
    doctor: document.querySelector("#doctor")
  };

  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.eventDeleteHandling = "Update";

  calendar.onEventMoved = function (args) {
    DayPilot.Http.ajax({
      url: "backend_move.php",
      data: args,
      success: function(ajax) {
        calendar.message(ajax.data.message);
      }
    });
  };
  calendar.onEventResized = function (args) {
    DayPilot.Http.ajax({
      url: "backend_move.php",
      data: args,
      success: function(ajax) {
        calendar.message(ajax.data.message);
      }
    });
  };
  calendar.onEventDeleted = function (args) {
    var params = {
      id: args.e.id(),
    };
    DayPilot.Http.ajax({
      url: "backend_delete.php",
      data: params,
      success: function (ajax) {
        calendar.message("Deleted.");
      }
    })
  };
  calendar.onBeforeEventRender = function (args) {
    if (!args.data.tags) {
      return;
    }
    switch (args.data.tags.status) {
      case "free":
        args.data.backColor = "#3d85c6";  // blue
        args.data.barHidden = true;
        args.data.borderColor = "darker";
        args.data.fontColor = "white";
        break;
      case "waiting":
        args.data.backColor = "#e69138";  // orange
        args.data.barHidden = true;
        args.data.borderColor = "darker";
        args.data.fontColor = "white";
        break;
      case "confirmed":
        args.data.backColor = "#6aa84f";  // green
        args.data.barHidden = true;
        args.data.borderColor = "darker";
        args.data.fontColor = "white";
        break;
    }

  };

  calendar.onEventClick = function (args) {

    var form = [
      {name: "Edit Appointment"},
      {name: "Name", id: "text"},
      {name: "Status", id: "tags.status", options: [
          {name: "Free", id: "free"},
          {name: "Waiting", id: "waiting"},
          {name: "Confirmed", id: "confirmed"},
        ]},
      {name: "From", id: "start", dateFormat: "MMMM d, yyyy h:mm tt", disabled: true},
      {name: "To", id: "end", dateFormat: "MMMM d, yyyy h:mm tt", disabled: true},
      {name: "Doctor", id: "resource", disabled: true, options: doctors},
    ];

    var data = args.e.data;

    var options = {
      focus: "text"
    };

    DayPilot.Modal.form(form, data, options).then(function(modal) {
      if (modal.canceled) {
        return;
      }

      var params = {
        id: modal.result.id,
        name: modal.result.text,
        status: modal.result.tags.status
      };

      DayPilot.Http.ajax({
        url: "backend_update.php",
        data: params,
        success: function(ajax) {
          calendar.events.update(modal.result);
        }
      });
    });


  };
  calendar.init();

  function loadEvents(day) {
    var start = nav.visibleStart();
    var end = nav.visibleEnd();

    var params = {
      doctor: elements.doctor.value,
      start: start.toString(),
      end: end.toString()
    };

    DayPilot.Http.ajax({
      url: "backend_events_doctor.php",
      data: params,
      success: function(ajax) {
        var data = ajax.data;
        if (day) {
          calendar.startDate = day;
        }
        calendar.events.list = data;
        calendar.update();

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

  elements.doctor.addEventListener("change", function() {
    loadEvents();
  });

  var doctors = [];
  DayPilot.Http.ajax({
    url: "backend_resources.php",
    success: function(ajax) {
      doctors = ajax.data;

      doctors.forEach(function(item) {
        var option = document.createElement("option");
        option.value = item.id;
        option.innerText = item.name;
        elements.doctor.appendChild(option);
      });

      loadEvents();

    }
  })

</script>

</body>
</html>

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

html5-doctor-appointment-scheduling-javascript-php-shift-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>

A 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 = document.querySelector("input[name=scale]:checked").value;

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

    DayPilot.Http.ajax({
      url: "backend_create.php",
      data: params,
      success: function (ajax) {
        loadEvents();
        dp.message(ajax.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(),
    };
    DayPilot.Http.ajax({
      url: "backend_delete.php",
      data: params,
      success: function (ajax) {
        scheduler.message("Deleted.");
      }
    })
  };

  scheduler.init();
</script>

The appointment status is highlighted using a custom background color (blue/orange/green):

<script>
  var scheduler = new DayPilot.Scheduler("scheduler");
  
  // ...
  
  scheduler.onBeforeEventRender = function (args) {
    switch (args.data.tags.status) {
      case "free":
        args.data.backColor = "#3d85c6";  // blue
        args.data.barHidden = true;
        args.data.borderColor = "darker";
        args.data.fontColor = "white";
        args.data.deleteDisabled = document.querySelector("input[name=scale]:checked").value === "shifts"; // only allow deleting in the more detailed hour scale mode
        break;
      case "waiting":
        args.data.backColor = "#e69138";  // orange
        args.data.barHidden = true;
        args.data.borderColor = "darker";
        args.data.fontColor = "white";
        break;
      case "confirmed":
        args.data.backColor = "#6aa84f";  // green
        args.data.barHidden = true;
        args.data.borderColor = "darker";
        args.data.fontColor = "white";
        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/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) {
    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.backColor = "#3d85c6";  // blue
        args.data.barHidden = true;
        args.data.borderColor = "darker";
        args.data.fontColor = "white";
        args.data.deleteDisabled = document.querySelector("input[name=scale]:checked").value === "shifts"; // only allow deleting in the more detailed hour scale mode
        break;
      case "waiting":
        args.data.backColor = "#e69138";  // orange
        args.data.barHidden = true;
        args.data.borderColor = "darker";
        args.data.fontColor = "white";
        break;
      case "confirmed":
        args.data.backColor = "#6aa84f";  // green
        args.data.barHidden = true;
        args.data.borderColor = "darker";
        args.data.fontColor = "white";
        break;
    }

  };
  scheduler.onEventDeleted = function (args) {
    var params = {
      id: args.e.id(),
    };
    DayPilot.Http.ajax({
      url: "backend_delete.php",
      data: params,
      success: function (ajax) {
        scheduler.message("Deleted.");
      }
    })
  };

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

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

    DayPilot.Http.ajax({
      url: "backend_create.php",
      data: params,
      success: function (ajax) {
        loadEvents();
        dp.message(ajax.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()
    };

    DayPilot.Http.ajax({
      url: "backend_events.php",
      data: params,
      success: function (ajax) {
        var data = ajax.data;

        var options = {
          events: data
        };

        if (day) {
          options.timeline = getTimeline(day);
          options.scrollTo = day;
        }

        scheduler.update(options);

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

  }

  function loadResources() {
    DayPilot.Http.ajax({
      url: "backend_resources.php",
      success: function (ajax) {
        scheduler.resources = ajax.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 = document.querySelector("input[name=scale]:checked").value;
    var businessOnly = document.querySelector("#business-only").checked;

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

    if (!businessOnly) {
      morningShiftStarts = 0;
      morningShiftEnds = 12;
      afternoonShiftStarts = 12;
      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 = document.querySelector('input[name=scale]:checked').value;
    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.querySelector("#business-only").addEventListener("click", function () {
    scheduler.timeline = getTimeline();
    scheduler.update();
  });

  var radios = Array.apply(null, document.querySelectorAll("input[name=scale]")).forEach(function(item) {
    item.addEventListener("change", function (ev) {
      scheduler.timeline = getTimeline();
      scheduler.timeHeaders = getTimeHeaders();
      scheduler.update();
    });
  });

  document.querySelector("#clear").addEventListener("click", function () {
    var dp = scheduler;
    var params = {
      start: dp.visibleStart(),
      end: dp.visibleEnd()
    };
    DayPilot.Http.ajax({
      url: "backend_clear.php",
      data: params,
      success: function (ajax) {
        dp.message(ajax.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

Schema of the 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`)
);

Schema of the doctor table:

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

History

  • July 15, 2020: DayPilot Pro for JavaScript upgraded to 2020.3.4594. Using DayPilot Modal for modal dialogs. Styling updated. jQuery dependency removed.

  • 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