Features

  • This web application allows 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 shift planning 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.

See Also

This tutorial is also available for ASP.NET Core:

Doctor Appointment Reservation User Interface

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

1. Patients View

HTML5 Doctor Appointment Scheduling PHP JavaScript - Patient Reservation UI

This is the public user interface that 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.

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.

The date picker on the left side displays the current month and two following months. Patients can use the date picker to display a selected week. The date picker help users with finding a free appointment slot - days with free slots are displayed in bold.

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 PHP JavaScript - Doctor Reservation UI

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.

In our app, doctors can't create new appointment slots (define shifts).

3. Managers

HTML5 Doctor Appointment Scheduling PHP JavaScript - Manager Shift UI

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.

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 PHP JavaScript - Patient User Interface with Free Slots

The patient interface is created using DayPilot JavaScript Calendar component.

The HTML page 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", {
    viewType: "Week",
  });
  calendar.init();

</script>

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

The calendar loads 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 = datepicker.visibleStart();
  const end = datepicker.visibleEnd();

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

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


  const options = {
    events: data
  }
  if (day) {
    options.startDate = day;
  }
  calendar.update(options);

  datepicker.update({events: data});
}

The backend_events_free.php script returns slots that were created in advance by the shift administrator. Only the free slots are loaded (appointment_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 {
  public $id;
  public $text;
  public $start;
  public $end;
  public $resource;
  public $tags;
}
class Tags {
  public $status;
  public $doctor;
}

$events = array();

foreach($result as $row) {
  $e = new Event();
  $e->id = (int) $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": "2024-09-16T09:00:00",
    "end": "2024-09-16T10:00:00",
    "tags": {
      "status": "free",
      "doctor": "Doctor 1"
    }
  },
  {
    "id": 122,
    "text": "",
    "start": "2024-09-16T10:00:00",
    "end": "2024-09-16T11:00:00",
    "tags": {
      "status": "free",
      "doctor": "Doctor 1"
    }
  },
  
  // ...

]

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

<script>

  const calendar = new DayPilot.Calendar("calendar", {
    viewType: "Week",
    timeRangeSelectedHandling: "Disabled",
    eventMoveHandling: "Disabled",
    eventResizeHandling: "Disabled",
    // ...
  });
  calendar.init();

</script>

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

<script>

  const calendar = new DayPilot.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 tutorial that explains how to built modal dialogs using DayPilot.Modal.form() method).

HTML5 Doctor Appointment Scheduling PHP JavaScript - Request an Appointment

As soon as the appointment request is saved the slot status changes to "waiting" and it is displayed in orange color. 

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>Public Patient User Interface - HTML5 Doctor Appointment Scheduling (JavaScript/PHP)</title>

  <link type="text/css" rel="stylesheet" href="css/layout.css"/>
  <link type="text/css" rel="stylesheet" href="css/buttons.css"/>
  <link type="text/css" rel="stylesheet" href="css/toolbar.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="datepicker"></div>
    </div>
    <div class="column-main">
      <div class="toolbar">Click on a blue time slot to create a reservation.</div>
      <div id="calendar"></div>
    </div>

  </div>
</div>

<script>
  const app = {
    datepicker: new DayPilot.Navigator("datepicker", {
      selectMode: "Week",
      showMonths: 3,
      skipMonths: 3,
      onTimeRangeSelected: args => {
        app.loadEvents(args.day);
      }
    }),
    calendar: new DayPilot.Calendar("calendar", {
      viewType: "Week",
      timeRangeSelectedHandling: "Disabled",
      eventMoveHandling: "Disabled",
      eventResizeHandling: "Disabled",
      eventArrangement: "SideBySide",
      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;
        }
      },
      onEventClick: async args => {
        if (args.e.tag("status") !== "free") {
          app.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";
        app.calendar.events.update(args.e.data);
      }
    }),
    async loadEvents(day) {
      const start = app.datepicker.visibleStart() > DayPilot.Date.today() ? app.datepicker.visibleStart() : DayPilot.Date.today();

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

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

      const options = {
        events: data
      };
      if (day) {
        options.startDate = day;
      }
      app.calendar.update(options);
      app.datepicker.update({events: data});
    },
    init() {
      app.datepicker.init();
      app.calendar.init();
      app.loadEvents();
    }
  };
  app.init();

</script>

</body>
</html>

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

HTML5 Doctor Appointment Scheduling PHP JavaScript - Appointment Administration UI for Doctors

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:

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

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 loadEvents(day) {
  const start = app.datepicker.visibleStart();
  const end = app.datepicker.visibleEnd();

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

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

  const options = {
    events: data
  };
  if (day) {
    options.startDate = day;
  }
  app.calendar.update(options);
  app.datepicker.update({events: data});
},

When the doctor clicks an appointment slot the application opens a modal dialog with the reservation details:

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: app.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);
  app.calendar.events.update(modal.result);
}

HTML5 Doctor Appointment Scheduling PHP JavaScript - 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>Doctor User Interface - HTML5 Doctor Appointment Scheduling (JavaScript/PHP)</title>

  <link type="text/css" rel="stylesheet" href="css/layout.css"/>
  <link type="text/css" rel="stylesheet" href="css/buttons.css"/>
  <link type="text/css" rel="stylesheet" href="css/toolbar.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="datepicker"></div>
    </div>
    <div class="column-main">
      <div class="toolbar">
        <select id="doctor" name="doctor"></select>
      </div>
      <div id="calendar"></div>
    </div>

  </div>
</div>

<script>

  const app = {
    datepicker: new DayPilot.Navigator("datepicker", {
      selectMode: "Week",
      showMonths: 3,
      skipMonths: 3,
      onTimeRangeSelected: args => {
        app.loadEvents(args.day);
      }
    }),
    calendar: new DayPilot.Calendar("calendar", {
      viewType: "Week",
      timeRangeSelectedHandling: "Disabled",
      eventDeleteHandling: "Update",
      onEventMoved: async args => {
        const {data} = await DayPilot.Http.post("backend_move.php", args);
        app.calendar.message(data.message);
      },
      onEventResized: async args => {
        const {data} = await DayPilot.Http.post("backend_move.php", args);
        app.calendar.message(data.message);
      },
      onEventDeleted: async args => {
        const params = {
          id: args.e.id(),
        };
        await DayPilot.Http.post("backend_delete.php", params);
        app.calendar.message("Deleted.");
      },
      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;
        }
      },
      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: app.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);
        app.calendar.events.update(modal.result);
      }
    }),
    doctors: [],
    elements: {
      doctor: document.querySelector("#doctor")
    },
    async loadEvents(day) {
      const start = app.datepicker.visibleStart();
      const end = app.datepicker.visibleEnd();

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

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

      const options = {
        events: data
      };
      if (day) {
        options.startDate = day;
      }
      app.calendar.update(options);
      app.datepicker.update({events: data});
    },
    addEventHandlers() {
      app.elements.doctor.addEventListener("change", () => {
        app.loadEvents();
      });
    },
    async loadDoctors() {
      const {data} = await DayPilot.Http.get("backend_resources.php");

      app.doctors = data;
      app.doctors.forEach(item => {
        const option = document.createElement("option");
        option.value = item.id;
        option.innerText = item.name;
        app.elements.doctor.appendChild(option);
      });
    },
    async init() {
      app.datepicker.init();
      app.calendar.init();
      app.addEventHandlers();
      await app.loadDoctors();
      await app.loadEvents();
    }
  };
  app.init();

</script>

</body>
</html>

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

HTML5 Doctor Appointment Scheduling PHP JavaScript - Shift Manager User Interface

The manager's view uses DayPilot JavaScript Scheduler component 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", {
    scale: "Manual",
    timeline: getTimeline(),
    timeHeaders: getTimeHeaders(),
    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 PHP JavaScript - Define Slots

onTimeRangeSelected: async args => {
  const scale = app.elements.scale.value;

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

  app.scheduler.clearSelection();

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

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 {
  public $start;
  public $end;
}

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 {
  public $result;
  public $message;
}

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

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 PHP JavaScript - Delete Slots

onEventDeleted: async args => {
  const params = {
    id: args.e.id(),
  };
  await DayPilot.Http.post("backend_delete.php", params);
  app.scheduler.message("Deleted.");
},

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

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 = app.elements.scale.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;
  }
},

manager.php

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

  <link type="text/css" rel="stylesheet" href="css/layout.css"/>
  <link type="text/css" rel="stylesheet" href="css/buttons.css"/>
  <link type="text/css" rel="stylesheet" href="css/toolbar.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="datepicker"></div>
    </div>
    <div class="column-main">

      <div class="toolbar">
        <span class="toolbar-item">Scale:
          <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>
  const app = {
    datepicker: new DayPilot.Navigator("datepicker", {
      selectMode: "Month",
      showMonths: 3,
      skipMonths: 3,
      onTimeRangeSelected: args => {
        if (app.scheduler.visibleStart().getDatePart() <= args.day && args.day < app.scheduler.visibleEnd()) {
          app.scheduler.scrollTo(args.day, "fast");  // just scroll
        } else {
          app.loadEvents(args.day);  // reload and scroll
        }
      }
    }),
    scheduler: new DayPilot.Scheduler("scheduler", {
      scale: "Manual",
      timeline: [],
      timeHeaders: [],
      cellWidth: 60,
      useEventBoxes: "Never",
      eventDeleteHandling: "Update",
      eventClickHandling: "Disabled",
      eventMoveHandling: "Disabled",
      eventResizeHandling: "Disabled",
      allowEventOverlap: false,
      onBeforeTimeHeaderRender: args => {
        args.header.text = args.header.text.replace(" AM", "a").replace(" PM", "p");  // shorten the hour header
      },
      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 = app.elements.scale.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;
        }
      },
      onEventDeleted: async args => {
        const params = {
          id: args.e.id(),
        };
        await DayPilot.Http.post("backend_delete.php", params);
        app.scheduler.message("Deleted.");
      },
      onTimeRangeSelected: async args => {
        const scale = app.elements.scale.value;

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

        app.scheduler.clearSelection();

        const {data} = await DayPilot.Http.post("backend_create.php", params);
        await app.loadEvents();
        app.scheduler.message(data.message);
      },
    }),
    elements: {
      businessOnly: document.querySelector("#business-only"),
      clear: document.querySelector("#clear"),
      scaleAll: Array.from(document.querySelectorAll("input[name=scale]")),
      get scale() {
        return document.querySelector('input[name=scale]:checked');
      }
    },
    async loadResources() {
      const {data} = await DayPilot.Http.get("backend_resources.php");
      app.scheduler.update({
        resources: data
      });
    },
    async loadEvents(day) {
      let from = app.scheduler.visibleStart();
      let to = app.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 = app.getTimeline(day);
        options.scrollTo = day;
      }

      app.scheduler.update(options);
      app.datepicker.update({events: data});
    },
    loadTimeline() {
      app.scheduler.update({
        timeline: app.getTimeline(),
        timeHeaders: app.getTimeHeaders()
      });
    },
    getTimeline(date) {
      date = date || DayPilot.Date.today();
      const start = new DayPilot.Date(date).firstDayOfMonth();
      const days = start.daysInMonth();
      const scale = app.elements.scale.value;
      const businessOnly = app.elements.businessOnly.checked;

      const morningShiftStarts = businessOnly ? 9 : 0;
      const morningShiftEnds = businessOnly ? 13 : 12;
      const afternoonShiftStarts = businessOnly ? 14 : 12;
      const afternoonShiftEnds = businessOnly ? 18 : 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;
    },
    getTimeHeaders() {
      const scale = app.elements.scale.value;
      switch (scale) {
        case "15min":
          return [
            {groupBy: "Month"},
            {groupBy: "Day", format: "dddd d"},
            {groupBy: "Hour", format: "h tt"},
            {groupBy: "Cell", format: "m"}
          ];
        case "hours":
          return [
            {groupBy: "Month"},
            {groupBy: "Day", format: "dddd d"},
            {groupBy: "Hour", format: "h tt"}
          ];
        case "shifts":
          return [
            {groupBy: "Month"},
            {groupBy: "Day", format: "dddd d"},
            {groupBy: "Cell", format: "tt"}
          ];
      }
    },
    addEventHandlers() {
      app.elements.businessOnly.addEventListener("click", () => {
        app.scheduler.update({
          timeline: app.getTimeline(),
        });
      });

      app.elements.scaleAll.forEach(item => {
        item.addEventListener("change", ev => {
          app.scheduler.update({
            timeline: app.getTimeline(),
            timeHeaders: app.getTimeHeaders()
          });
        });
      });

      app.elements.clear.addEventListener("click", async () => {
        const params = {
          start: app.scheduler.visibleStart(),
          end: app.scheduler.visibleEnd()
        };

        const {data} = await DayPilot.Http.post("backend_clear.php", params);
        await app.loadEvents();
        app.scheduler.message(data.message);
      });

    },
    init() {
      app.datepicker.init();
      app.scheduler.init();
      app.addEventHandlers();
      app.loadTimeline();
      app.loadResources();
      app.loadEvents(DayPilot.Date.today());
    }
  };
  app.init();

</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 24, 2024: DayPilot Pro for JavaScript upgraded to 2024.3.5972. The JavaScript code structure improved (a global app object). PHP 8+ compatibility.

  • 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