Features

  • Appointment reservation for multiple doctors
  • Management overview of all doctors, shifts and appointment slots
  • Defining appointment slots using drag and drop
  • Public interface with an option to request an appointment in one of the available slots
  • Uses DayPilot AngularJS Scheduler and Event Calendar controls
  • Sample project with PHP backend, storing data in a SQLite database

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.

See Also

This tutorial is also available for ASP.NET WebForms: ASP.NET Doctor Appointment Scheduling (C#, VB.NET)

Architecture/Overview

This application uses three different views:

  • Manager (manager.php)
  • Doctor (doctor.php)
  • Patient (index.php)

It allows the patients to request an appointment in one of the time slots defined by the manager or doctor. The appointment slots are defined in advance. The manager can create the appointment slots using drag and drop - it will automatically generate the slots with the defined size (1 hour) in the selected range (shift).

Defining Available Appointment Slots (Manager)

The manager.php script defines the manager's view:

angularjs-doctor-appointment-scheduling-php-manager.png

This view uses the AngularJS Scheduler control to show a timeline for multiple doctors. For an introduction on using the Scheduler with AngularJS see the the AngularJS Scheduler Tutorial.

  • Creating multiple appointment slots at once using drag and drop (for one or more shifts).
  • The timeline shows morning shift and afternoon shift (daytime only).

Scheduling Appointments (Doctor)

The doctor.php script defines the doctor's view:

angularjs-doctor-appointment-scheduling-php-doctor.png

This view uses the AngularJS Event Calendar control to manage appointments in a week view. For an introduction on using the Event Calendar in AngularJS see the AngularJS Event Calendar Tutorial.

  • The calendar shows full 24 hours a day.
  • Creating new slots using drag and drop.
  • Moving appointment slots using drag and drop.
  • Deleting appointment slots.
  • Scheduling appointments in existing slots.
  • Confirm appointments requested directly by patients using the public interface.
  • Edit existing appointments using a modal dialog.

Public Interface for Requesting an Appointment (Patients)

The index.php script defines the patient's (public) view:

angularjs-doctor-appointment-scheduling-php-patient.png

  • It displays the slots defined in the doctor's or manager's view.
  • Only the available slots are displayed.
  • Slots in the past are hidden.

Loading Doctors (Y Axis) using AngularJS

angularjs-doctor-appointment-scheduling-php-loading-resources.png

manager.php

function loadResources() {
  $http.post("backend_resources.php").success(function(data) {
      $scope.schedulerConfig.resources = data;
      $scope.schedulerConfig.visible = true;
  });
}

backend_resources.php

<?php
require_once '_db.php';
    
$scheduler_doctors = $db->query('SELECT * FROM [doctor] ORDER BY [doctor_name]');

class Resource {}

$result = array();

foreach($scheduler_doctors as $doctor) {
  $r = new Resource();
  $r->id = $doctor['doctor_id'];
  $r->name = $doctor['doctor_name'];
  $result[] = $r;
}

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

?>

Sample JSON response:

[
  {"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"}
]

Loading and Displaying Appointment Slots using AngularJS

angularjs-doctor-appointment-scheduling-php-loading-events.png

The appointment slots are loaded using a special AngularJS POST request:

manager.php

function loadEvents(day) {
  var from = $scope.scheduler.visibleStart();
  var to = $scope.scheduler.visibleEnd();
  if (day) {
      from = new DayPilot.Date(day).firstDayOfMonth();
      to = from.addMonths(1);
  }
  
  var params = {
      start: from.toString(),
      end: to.toString()
  };

  $http.post("backend_events.php", params).success(function(data) {
      $scope.schedulerConfig.timeline = getTimeline(day);
      $scope.schedulerConfig.scrollTo = day;
      $scope.schedulerConfig.scrollToAnimated = "fast";
      $scope.schedulerConfig.scrollToPosition = "left";
      $scope.events = data;
  });   
}

backend_events.php

<?php
require_once '_db.php';

$json = file_get_contents('php://input');
$params = json_decode($json);
    
$stmt = $db->prepare('SELECT * FROM [appointment] WHERE NOT ((appointment_end <= :start) OR (appointment_start >= :end))');
$stmt->bindParam(':start', $params->start);
$stmt->bindParam(':end', $params->end);
$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 = $row['appointment_patient_name'] ?: "";
  $e->start = $row['appointment_start'];
  $e->end = $row['appointment_end'];
  $e->resource = $row['doctor_id'];
  $e->tags = new Tags();
  $e->tags->status = $row['appointment_status'];
  $events[] = $e;
}

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

?>

Sample JSON response:

[
  {"id":"1","text":"","start":"2016-01-28T10:00:00","end":"2016-01-28T11:00:00","resource":"1","tags":{"status":"free"}},
  {"id":"2","text":"","start":"2016-01-28T11:00:00","end":"2016-01-28T12:00:00","resource":"1","tags":{"status":"free"}},
  {"id":"3","text":"","start":"2016-01-28T12:00:00","end":"2016-01-28T13:00:00","resource":"1","tags":{"status":"free"}},
  {"id":"4","text":"","start":"2016-01-28T14:00:00","end":"2016-01-28T15:00:00","resource":"1","tags":{"status":"free"}},
  {"id":"5","text":"","start":"2016-01-28T15:00:00","end":"2016-01-28T16:00:00","resource":"1","tags":{"status":"free"}},
  {"id":"6","text":"","start":"2016-01-28T16:00:00","end":"2016-01-28T17:00:00","resource":"1","tags":{"status":"free"}},
  {"id":"7","text":"","start":"2016-01-28T17:00:00","end":"2016-01-28T18:00:00","resource":"1","tags":{"status":"free"}},
  {"id":"8","text":"","start":"2016-01-29T09:00:00","end":"2016-01-29T10:00:00","resource":"1","tags":{"status":"free"}},
  {"id":"9","text":"","start":"2016-01-29T10:00:00","end":"2016-01-29T11:00:00","resource":"1","tags":{"status":"free"}},
  {"id":"10","text":"","start":"2016-01-29T11:00:00","end":"2016-01-29T12:00:00","resource":"1","tags":{"status":"free"}}
]

Appointment Slot Status

Free Slots

Free slots are displayed with a green bar. No patient details are entered.

angularjs-doctor-appointment-scheduling-php-slot-free.png

Waiting for Confirmation

Slots that were requested by a patient but were not confirmed by the doctor yet are displayed with an orange bar.

angularjs-doctor-appointment-scheduling-php-slot-waiting.png

Confirmed Appointments

Slots with a confirmed appointment are displayed with a red bar.

angularjs-doctor-appointment-scheduling-php-slot-confirmed.png

The calendar onBeforeEventRender event handler to customize the events.

doctor.php

onBeforeEventRender: function(args) {
    switch (args.data.tags.status) {
        case "free":
            args.data.barColor = "green";
            args.data.deleteDisabled = $scope.scale === "shifts";  // only allow deleting in the more detailed hour scale mode
            break;
        case "waiting":
            args.data.barColor = "orange";
            args.data.deleteDisabled = true;
            break;
        case "confirmed":
            args.data.barColor = "#f41616";  // red            
            args.data.deleteDisabled = true;
            break;                            
    }
},

Deleting an Appointment Slot

angularjs-doctor-appointment-scheduling-php-deleting.png

The manager's view allows deleting the slots using the built-in event deleting feature.

manager.php

$scope.schedulerConfig = {
    eventDeleteHandling: "Update",
    onEventDeleted: function(args) {
        var params = {
            id: args.e.id(),
        };
        $http.post("backend_delete.php", params).success(function() {
            $scope.scheduler.message("Deleted.");
        });
        
    },
    // ...
};

The server-side backend is notified using an AngularJS AJAX call ($http.post() method).

Defining Shifts (Creating Appointment Slots)

angularjs-doctor-appointment-scheduling-php-creating-slots.png

The scheduler supports drag and drop time range selecting. We will use this feature to add multiple appointment slots at once.

manager.php

onTimeRangeSelected: function(args) {
    var dp = $scope.scheduler;
    
    var params = {
        start: args.start.toString(),
        end: args.end.toString(),
        resource: args.resource,
        scale: $scope.scale
    };
    
    $http.post("backend_create.php", params).success(function(data) {
        loadEvents();
        dp.message(data.message);
    });   
    
    dp.clearSelection();

},

Custom Timeline (Hours/Shifts)

The Scheduler supports a custom timeline (defined using individual time cells). We will use this feature to display only the business hours. The user will be able to switch the "zoom" level:

Hours

angularjs-doctor-appointment-scheduling-php-scale-hours.png

Shifts

angularjs-doctor-appointment-scheduling-php-scale-shifts.png

The Hours/Shift radio buttons switch the scale (by generating an updated timeline), adjusts the time headers and scrolls to the original position.

manager.php

HTML

<div>
  Scale: 
  <label for='scale-hours'><input type="radio" ng-model="scale" value="hours" id='scale-hours'> Hours</label>
  <label for='scale-shifts'><input type="radio" ng-model="scale" value="shifts" id='scale-shifts'> Shifts</label>
</div>

We will watch the model changes using the Angular $watch method:

JavaScript

$scope.$watch("scale", function() {
    $scope.schedulerConfig.timeline = getTimeline($scope.scheduler.visibleStart());                    
    $scope.schedulerConfig.timeHeaders = getTimeHeaders();
    $scope.schedulerConfig.scrollToAnimated = "fast";
    $scope.schedulerConfig.scrollTo = $scope.scheduler.getViewPort().start;  // keep the scrollbar position/by date
}); 

The timeline is generated from the pre-defined morning and afternoon shift times:

function getTimeline(date) {
    var date = date || DayPilot.Date.today();
    var start = new DayPilot.Date(date).firstDayOfMonth();
    var days = start.daysInMonth();
    
    var morningShiftStarts = 9;
    var morningShiftEnds = 13;
    var afternoonShiftStarts = 14;
    var afternoonShiftEnds = 18;
    
    if (!$scope.businessOnly) {
        var morningShiftStarts = 0;
        var morningShiftEnds = 12;
        var afternoonShiftStarts = 12;
        var afternoonShiftEnds = 24;                        
    }
    
    var timeline = [];
    
    var increaseMorning;  // in hours
    var increaseAfternoon;  // in hours
    switch ($scope.scale) {
        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;                    
}

The time header rows show Months/Days/Hours for the "Hours" scale and Months/Days/Shifts for the "Shifts" scale.

function getTimeHeaders() {
    switch ($scope.scale) {
        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;
    }
}

Requesting an Appointment

angularjs-doctor-appointment-scheduling-php-request.png

The patient's view allows the visitors to request an appointment in one of the free slots. After clicking a free slot the reservation dialog box is displayed.

index.php

onEventClick: function(args) {
    
    if (args.e.tag("status") !== "free") {
        $scope.calendar.message("You can only request a new appointment in a free slot.");
        return;
    }
    
    var modal = new DayPilot.Modal({
        onClosed: function(args) {
            if (args.result) {  // args.result is empty when modal is closed without submitting
                loadEvents();
            }
        }
    });

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

The appointment request dialog is displayed using a DayPilot.Modal helper - it displays a standalone page (appointment_request.php).

appointment_request.php

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Request an Appointment</title>
    	<link type="text/css" rel="stylesheet" href="media/layout.css" />    
        <script src="js/jquery-1.11.2.min.js" type="text/javascript"></script>
        <script src="js/daypilot/daypilot-all.min.js" type="text/javascript"></script>
    </head>
    <body>
        <?php
            // check the input
            is_numeric($_GET['id']) or die("invalid URL");
            
            require_once '_db.php';
            
            $stmt = $db->prepare('SELECT * FROM appointment WHERE appointment_id = :id');
            $stmt->bindParam(':id', $_GET['id']);
            $stmt->execute();
            $event = $stmt->fetch();

        ?>
        <form id="f" action="backend_request_save.php" style="padding:20px;">
            <input type="hidden" name="id" id="id" value="<?php print $_GET['id'] ?>" />
            <h1>Request an Appointment</h1>

            <div>Start:</div>
            <div><input type="text" id="start" name="start" value="<?php print (new DateTime($event['appointment_start']))->format('d/M/y g:i A') ?>" disabled /></div>

            <div>End:</div>
            <div><input type="text" id="end" name="end" value="<?php print (new DateTime($event['appointment_end']))->format('d/M/y g:i A') ?>" disabled /></div>

            <div>Your Name: </div>
            <div><input type="text" id="name" name="name" value="" /></div>
            
            <div class="space"><input type="submit" value="Save" /> <a href="#" id="cancel">Cancel</a></div>
        </form>
        
        <script type="text/javascript">

        $("#f").submit(function () {
            var f = $("#f");
            $.post(f.attr("action"), f.serialize(), function (result) {
                DayPilot.Modal.close(result);
            });
            return false;
        });
        
        $("#cancel").click(function() {
            DayPilot.Modal.close();
            return false;
        });

        $(document).ready(function () {
            $("#name").focus();
        });
    
        </script>
    </body>
</html>

The request is stored in the database and the slot status is changed to "waiting". For demonstration purposes the requested slot is linked to the user using the session id. In a standard application it would be replaced with an id of the logged user.

backend_request_save.php

<?php
require_once '_db.php';

class Result {}

$session = session_id();

$stmt = $db->prepare("UPDATE appointment SET appointment_patient_name = :name, appointment_patient_session = :session, appointment_status = 'waiting' WHERE appointment_id = :id");
$stmt->bindParam(':id', $_POST["id"]);
$stmt->bindParam(':name', $_POST["name"]);
$stmt->bindParam(':session', $session);
$stmt->execute();

$response = new Result();
$response->result = 'OK';
$response->message = 'Update successful';

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

?>

As soon as the request is saved it will be displayed in the doctor's and manager's view in orange color:

angularjs-doctor-appointment-scheduling-php-requested-slot.png