Features

  • Web application for scheduling doctor appointments

  • Public interface for patients: Users can see available appointment slots and request a new appointment at the selected time

  • Doctor's management interface: Doctors can edit and delete appointments, confirm the the requests submitted by patients

  • Manager's managements interface: Managers can schedule shifts and create appointment slots

  • The application uses scheduling calendar components from DayPilot Pro for JavaScript (trial version)

  • The backend is implemented in PHP, it uses MySQL database for 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

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

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

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

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>

  const 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.

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

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

  const {data} = await DayPilot.Http.post("backend_events_doctor.php", params);

  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": "2022-09-16T09:00:00",
    "end": "2022-09-16T10:00:00",
    "tags": {
      "status": "free",
      "doctor": "Doctor 1"
    }
  },
  {
    "id": "122",
    "text": "",
    "start": "2022-09-16T10:00:00",
    "end": "2022-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>

  const 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>

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

  // ...

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

    const 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"},
    ];

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

    const options = {
      focus: "name"
    };

    const modal = await DayPilot.Modal.form(form, data, options);
    if (modal.canceled) {
      return;
    }

    await DayPilot.Http.post("backend_request_save.php", modal.result);

    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

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>
  const nav = new DayPilot.Navigator("nav");
  nav.selectMode = "week";
  nav.showMonths = 3;
  nav.skipMonths = 3;
  nav.onTimeRangeSelected = args => {
    loadEvents(args.start.firstDayOfWeek(DayPilot.Locale.find(nav.locale).weekStarts), args.start.addDays(7));
  };
  nav.init();

  const calendar = new DayPilot.Calendar("calendar");
  calendar.viewType = "Week";
  calendar.timeRangeSelectedHandling = "Disabled";
  calendar.eventMoveHandling = "Disabled";
  calendar.eventResizeHandling = "Disabled";
  calendar.eventArrangement = "SideBySide";
  calendar.onBeforeEventRender = 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 = async args => {
    if (args.e.tag("status") !== "free") {
      calendar.message("You can only request a new appointment in a free slot.");
      return;
    }

    const 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"},
    ];

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

    const options = {
      focus: "name"
    };

    const modal = await DayPilot.Modal.form(form, data, options);
    if (modal.canceled) {
      return;
    }

    await DayPilot.Http.post("backend_request_save.php", modal.result);

    args.e.data.tags.status = "waiting";
    calendar.events.update(args.e.data);

  };
  calendar.init();

  loadEvents();

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

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

    const {data} = await DayPilot.Http.post("backend_events_free.php", params);

    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

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 = async args => {
  const {data} = await DayPilot.Http.post("backend_move.php", args);
  calendar.message(data.message);
};
calendar.onEventResized = async args => {
  const {data} = await DayPilot.Http.post("backend_move.php", args);
  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 = 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):

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

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

  const {data} = await DayPilot.Http.post("backend_events_doctor.php", params);

  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 a modal dialog with the reservation details:

calendar.onEventClick = async args => {

  const 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},
  ];

  const data = args.e.data;

  const options = {
    focus: "text"
  };

  const modal = await DayPilot.Modal.form(form, data, options);
  if (modal.canceled) {
    return;
  }

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

  await DayPilot.Http.post("backend_update.php", params);
  calendar.events.update(modal.result);

};

html5 doctor appointment scheduling javascript php edit time slot

This modal dialog allows changing the status and the patient name. To accept the appointment request, the doctor can change the status from “Waiting” to “Confirmed”.

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>
  const elements = {
    doctor: document.querySelector("#doctor")
  };

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

  const calendar = new DayPilot.Calendar("calendar");
  calendar.viewType = "Week";
  calendar.timeRangeSelectedHandling = "Disabled";
  calendar.eventDeleteHandling = "Update";

  calendar.onEventMoved = async args => {
    const {data} = await DayPilot.Http.post("backend_move.php", args);
    calendar.message(data.message);
  };
  calendar.onEventResized = async args => {
    const {data} = await DayPilot.Http.post("backend_move.php", args);
    calendar.message(data.message);
  };
  calendar.onEventDeleted = async args => {
    const params = {
      id: args.e.id(),
    };
    await DayPilot.Http.post("backend_delete.php", params);
    calendar.message("Deleted.");
  };
  calendar.onBeforeEventRender = 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 = async args => {

    const 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},
    ];

    const data = args.e.data;

    const options = {
      focus: "text"
    };

    const modal = await DayPilot.Modal.form(form, data, options);
    if (modal.canceled) {
      return;
    }

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

    await DayPilot.Http.post("backend_update.php", params);
    calendar.events.update(modal.result);

  };
  calendar.init();

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

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

    const {data} = await DayPilot.Http.post("backend_events_doctor.php", params);

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

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

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

  let doctors = [];

  async function init() {
    const {data} = await DayPilot.Http.get("backend_resources.php");

    doctors = data;

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

  init();
  loadEvents();

</script>

</body>
</html>

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

html5 doctor appointment scheduling javascript php shift manager user interface

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>
  const 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

<script>
  const scheduler = new DayPilot.Scheduler("scheduler");
  
  // ...
  
  scheduler.onTimeRangeSelected = async args => {
    const dp = scheduler;
    const scale = document.querySelector("input[name=scale]:checked").value;

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

    dp.clearSelection();

    const {data} = await DayPilot.Http.post("backend_create.php", params);
    loadEvents();
    dp.message(data.message);

  };

  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

<script>
  const scheduler = new DayPilot.Scheduler("scheduler");
  
  // ...
  
  scheduler.onEventDeleted = async args => {
    var params = {
      id: args.e.id(),
    };
    const {data: result} = await DayPilot.Http.post("backend_delete.php", params);
    scheduler.message("Deleted.");
  };

  scheduler.init();
</script>

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

<script>
  const scheduler = new DayPilot.Scheduler("scheduler");
  
  // ...
  
  scheduler.onBeforeEventRender = 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>
  const nav = new DayPilot.Navigator("nav");
  nav.selectMode = "month";
  nav.showMonths = 3;
  nav.skipMonths = 3;
  nav.onTimeRangeSelected = 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();

  const 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 = args => {
    args.header.text = args.header.text.replace(" AM", "a").replace(" PM", "p");  // shorten the hour header
  };
  scheduler.onBeforeEventRender = 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 = async args => {
    var params = {
      id: args.e.id(),
    };
    const {data: result} = await DayPilot.Http.post("backend_delete.php", params);
    scheduler.message("Deleted.");
  };

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

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

    dp.clearSelection();

    const {data} = await DayPilot.Http.post("backend_create.php", params);
    loadEvents();
    dp.message(data.message);

  };
  scheduler.init();


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

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

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

    const {data} = await DayPilot.Http.post("backend_events.php", params);

    const options = {
      events: data
    };

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

    scheduler.update(options);

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

  }

  async function loadResources() {
    const {data} = await DayPilot.Http.get("backend_resources.php");
    scheduler.update({
      resources: data,
      visible: true
    });
  }

  function getTimeline(date) {
    date = date || DayPilot.Date.today();
    const start = new DayPilot.Date(date).firstDayOfMonth();
    const days = start.daysInMonth();
    const scale = document.querySelector("input[name=scale]:checked").value;
    const businessOnly = document.querySelector("#business-only").checked;

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

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

    const timeline = [];

    let increaseMorning;  // in hours
    let 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 (let i = 0; i < days; i++) {
      const day = start.addDays(i);

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

    return timeline;
  }

  function getTimeHeaders() {
    const 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", () => {
    scheduler.timeline = getTimeline();
    scheduler.update();
  });

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

  document.querySelector("#clear").addEventListener("click", async () => {
    const dp = scheduler;
    const params = {
      start: dp.visibleStart(),
      end: dp.visibleEnd()
    };

    const {data} = await DayPilot.Http.post("backend_clear.php", params);
    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

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

  • June 1, 2021: DayPilot Pro for JavaScript upgraded to 2021.2.4999. Using promise-based DayPilot.Http.get() and DayPilot.Http.post(). Upgraded to ES6.

  • November 22, 2020: DayPilot Pro for JavaScript upgraded to 2020.4.4736

  • 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