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.
See Also
This tutorial is also available for ASP.NET Core: ASP.NET Core Doctor Appointment Scheduling Tutorial
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:
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:
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:
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
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
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.
Waiting for Confirmation
Slots that were requested by a patient but were not confirmed by the doctor yet are displayed with an orange bar.
Confirmed Appointments
Slots with a confirmed appointment are displayed with a red bar.
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
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)
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
Shifts
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
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: