Features

  • AngularJS Scheduler
  • Displaying one month
  • Switching the month using date navigator control
  • Support for check-in and check-out times (displays overnight hotel reservations)
  • Creating new reservations using drag and drop
  • Status of the reservation is marked using a custom color
  • Status of the room is marked using a custom color ("free", "cleanup", "dirty")
  • PHP backend (based on simple JSON endpoints)

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

See also other versions of this tutorial:

Using the AngularJS Scheduler

angularjs-hotel-room-booking-scheduler.png

We will use the DayPilot AngularJS Scheduler plugin to create the main reservation grid.

The scheduler can be added using <daypilot-scheduler> element. It supports the following three attributes:

  • id (required) - the id that will be used for the main scheduler div; it will be also used for the property name that will store the DayPilot.Scheduler objects in $scope
  • daypilot-config - the configuration object
  • daypilot-events - array with the events (reservations)
<div ng-app="main" ng-controller="DemoCtrl" >
  <!-- ... -->
  <daypilot-scheduler id="scheduler" daypilot-config="schedulerConfig" daypilot-events="events" ></daypilot-scheduler>
</div>

<script>
    var app = angular.module('main', ['daypilot']).controller('DemoCtrl', function($scope, $timeout, $http) {

        $scope.events = [];

        $scope.schedulerConfig = {
            scale: "Manual",
            timeline: getTimeline(),
            timeHeaders: [ { groupBy: "Month", format: "MMMM yyyy" }, { groupBy: "Day", format: "d" } ],
            eventDeleteHandling: "Update",
            allowEventOverlap: false,
            cellWidthSpec: "Auto",
            eventHeight: 50,
            rowHeaderColumns: [
                {title: "Room", width: 80},
                {title: "Capacity", width: 80},
                {title: "Status", width: 80}
            ],
            // ...
        };

        // the DayPilot.Scheduler object will be reachable as $scope.scheduler (see the id attribute)

    });
</script>

Angular will watch all changes in $scope.schedulerConfig and $scope.events objects. If a change is detected it will update the UI.

For introduction to using the Scheduler with AngularJS see also the AngularJS Scheduler Tutorial.

Check-In and Check-Out Time

angularjs-hotel-room-booking-timeline-check-in-check-out.png

The AngularJS Scheduler supports predefined timeline scale units, such as "Hour", "Day", "Week", "Month", "Year". The timeline unit can be set using .scale property.

However, we will use a manually-adjusted timeline that will display time cells from 12:00 to 12:00 (noon-noon) instead of 00:00 to 24:00 (midnight-midnight). That corresponds to the usual Hotel check-in and check-out time. You can adjust the check-in and check-out time in the getTimeline() function.

<script>
  var app = angular.module('main', ['daypilot']).controller('DemoCtrl', function($scope, $timeout, $http) {
  
      // ...
      
      $scope.schedulerConfig = {
          scale: "Manual",
          timeline: getTimeline(),
          timeHeaders: [ { groupBy: "Month", format: "MMMM yyyy" }, { groupBy: "Day", format: "d" } ],
          // ...
      };
      
      // ...

      function getTimeline(date) {
          var date = date || DayPilot.Date.today();
          var start = new DayPilot.Date(date).firstDayOfMonth();
          var days = start.daysInMonth();

          var timeline = [];
          
          var checkin = 12;
          var checkout = 12;

          for (var i = 0; i < days; i++) {
              var day = start.addDays(i);
              timeline.push({start: day.addHours(checkin), end: day.addDays(1).addHours(checkout) });
          }

          return timeline;                    
      }
  });
</script>

Room Details and Status

angularjs-hotel-loading-rooms.png

The Scheduler displays one room per row. By default the row header shows just one column with the resource (room) name.

We will define three columns and display additional room details there:

  • Name/Room Number
  • Capacity
  • Status

The row header columns are defined using rowHeaderColumns property of the scheduler config object:

<div ng-app="main" ng-controller="DemoCtrl" >
  <!-- ... -->
  <daypilot-scheduler id="scheduler" daypilot-config="schedulerConfig" daypilot-events="events" ></daypilot-scheduler>
</div>

<script>
    var app = angular.module('main', ['daypilot']).controller('DemoCtrl', function($scope, $timeout, $http) {

        $scope.schedulerConfig = {
            // ...
            rowHeaderColumns: [
                {title: "Room", width: 80},
                {title: "Capacity", width: 80},
                {title: "Status", width: 80}
            ],
            // ...
        };
    });
</script>

We will load the rooms from the server using Angular HTTP call to "backend_rooms.php" PHP script. The Angular $http.post method automatically wraps the callback method in an $apply() block so we don't have to notify the framework about a property change.

function loadResources() {
    $http.post("backend_rooms.php", params).then(function(response) {
        $scope.schedulerConfig.resources = response.data;
    });
}

backend_rooms.php - sample JSON response

[
  {"id":"1","name":"Room 1","capacity":"2","status":"Dirty"},
  {"id":"2","name":"Room 2","capacity":"2","status":"Cleanup"},
  {"id":"3","name":"Room 3","capacity":"2","status":"Ready"},
  {"id":"4","name":"Room 4","capacity":"4","status":"Ready"},
  {"id":"5","name":"Room 5","capacity":"1","status":"Ready"}
]

backend_rooms.php

<?php
require_once '_db.php';

$json = file_get_contents('php://input');
$params = json_decode($json);

$stmt = $db->prepare("SELECT * FROM rooms WHERE capacity = :capacity OR :capacity = '0' ORDER BY name");
$stmt->bindParam(':capacity', $params->capacity); 
$stmt->execute();
$rooms = $stmt->fetchAll();

class Room {}

$result = array();

foreach($rooms as $room) {
  $r = new Room();
  $r->id = $room['id'];
  $r->name = $room['name'];
  $r->capacity = $room['capacity'];
  $r->status = $room['status'];
  $result[] = $r;
  
}

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

?>

We will use onBeforeResHeaderRender event to set the content of the additional columns  and customize the row header CSS (depending on the room status). It is also possible to load the column data directly from the JSON source (.resources property) but we will use the client-side approach.

onBeforeResHeaderRender: function(args) {
  var beds = function(count) {
      return count + " bed" + (count > 1 ? "s" : "");
  };

  args.resource.columns[0].html = beds(args.resource.capacity);
  args.resource.columns[1].html = args.resource.status;
  switch (args.resource.status) {
      case "Dirty":
          args.resource.cssClass = "status_dirty";
          break;
      case "Cleanup":
          args.resource.cssClass = "status_cleanup";
          break;
  }
},

CSS

.scheduler_default_rowheadercol2
{
    background: #ffffff;
}
.scheduler_default_rowheadercol2 .scheduler_default_rowheader_inner 
{
    top: 2px;
    bottom: 2px;
    left: 2px;
    background-color: transparent;
    border-left: 5px solid #1a9d13; /* status: "free" (default), green color */
    border-right: 0px none;
}
.status_dirty.scheduler_default_rowheadercol2 .scheduler_default_rowheader_inner
{
    border-left: 5px solid #ea3624; /* status: "dirty", red color */
}
.status_cleanup.scheduler_default_rowheadercol2 .scheduler_default_rowheader_inner
{
    border-left: 5px solid #f9ba25; /* status: "cleanup", orange color */
}

Loading Reservations for the Selected Month

angularjs-hotel-room-booking-navigation.png

The Navigator will let users switch the current month. Whenever the user clicks a certain date, onTimeRangeSelected event is fired.

  • If the selected date is within the current month, the Scheduler will scroll to the selected date (we update the scrollbar position using scrollTo property).
  • If the selected date is in another month we will load the events for the new month and update the view.

HTML

<div ng-app="main" ng-controller="DemoCtrl" >

  <div style="float:left; width:160px">
      <daypilot-navigator id="navigator" daypilot-config="navigatorConfig"></daypilot-navigator>
  </div>
  <div style="margin-left: 160px">
      <!-- ...  -->
      <daypilot-scheduler id="scheduler" daypilot-config="schedulerConfig" daypilot-events="events" ></daypilot-scheduler>
  </div>
  
</div>

AngularJS Controller

<script>
  var app = angular.module('main', ['daypilot']).controller('DemoCtrl', function($scope, $timeout, $http) {
      
      $scope.navigatorConfig = {
          selectMode: "month",
          showMonths: 3,
          skipMonths: 3,
          onTimeRangeSelected: function(args) {
              if ($scope.scheduler.visibleStart().getDatePart() <= args.day && args.day < $scope.scheduler.visibleEnd()) {
                  $scope.scheduler.scrollTo(args.day, "fast");  // just scroll
              }
              else {
                  loadEvents(args.day);  // reload and scroll
              }
          }
      };

      $scope.schedulerConfig = {
        // ...
      };

      // loads events; switches the Scheduler visible range if "day" supplied
      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) {
              if (day) {
                $scope.schedulerConfig.timeline = getTimeline(day);
                $scope.schedulerConfig.scrollTo = day;
                $scope.schedulerConfig.scrollToAnimated = "fast";
                $scope.schedulerConfig.scrollToPosition = "left";
              }
              $scope.events = data;
          });   
      }
  });
</script>

backend_events.php - sample JSON response

[
  {"id":"1","text":"Mrs. Jones","start":"2015-11-03T12:00:00","end":"2015-11-10T12:00:00","resource":"1","bubbleHtml":"Reservation details: <br\/>Mrs. Jones","status":"New","paid":"0"},
  {"id":"2","text":"Mr. Lee","start":"2015-11-05T12:00:00","end":"2015-11-12T12:00:00","resource":"2","bubbleHtml":"Reservation details: <br\/>Mr. Lee","status":"Confirmed","paid":"0"},
  {"id":"3","text":"Mr. Garc\u00eda","start":"2015-11-02T12:00:00","end":"2015-11-07T12:00:00","resource":"3","bubbleHtml":"Reservation details: <br\/>Mr. Garc\u00eda","status":"Arrived","paid":"50"}
]

backend_events.php

<?php
require_once '_db.php';

$json = file_get_contents('php://input');
$params = json_decode($json);

$stmt = $db->prepare("SELECT * FROM reservations WHERE NOT ((end <= :start) OR (start >= :end))");
$stmt->bindParam(':start', $params->start);
$stmt->bindParam(':end', $params->end);
$stmt->execute();
$result = $stmt->fetchAll();

class Event {}
$events = array();

$now = new DateTime("now");
$today = $now->setTime(0, 0, 0);

foreach($result as $row) {
    $e = new Event();
    $e->id = $row['id'];
    $e->text = $row['name'];
    $e->start = $row['start'];
    $e->end = $row['end'];
    $e->resource = $row['room_id'];
    $e->bubbleHtml = "Reservation details: <br/>".$e->text;
    
    // additional properties
    $e->status = $row['status'];
    $e->paid = $row['paid'];
    $events[] = $e;
}

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

?>

Reservation Status

angularjs-hotel-room-booking-reservation-status.png

The AngularJS Scheduler can customize the reservations depending on custom reservation properties. We will use this feature to display reservation details:

  • Reservation status text ("New", "Confirmed", "Arrived").
  • Custom bar color highlighting the reservation status (yellow, green, blue)
  • Guest name, check-in a check-out day: "Mr. GarcĂ­a (11/2/2015 - 11/7/2015)"
  • How much of the price has been paid already (text + special bar)

AngularJS Controller

<div ng-app="main" ng-controller="DemoCtrl" >
  <daypilot-scheduler id="scheduler" daypilot-config="schedulerConfig" daypilot-events="events" ></daypilot-scheduler>
</div>

<script>
    var app = angular.module('main', ['daypilot']).controller('DemoCtrl', function($scope, $timeout, $http) {
        
        $scope.schedulerConfig = {

          // ...
          
          onBeforeEventRender: function(args) {
              var start = new DayPilot.Date(args.data.start);
              var end = new DayPilot.Date(args.data.end);

              var now = new DayPilot.Date();
              var today = new DayPilot.Date().getDatePart();
              var status = "";

              // customize the reservation bar color and tooltip depending on status
              switch (args.e.status) {
                  case "New":
                      var in2days = today.addDays(1);

                      if (start < in2days) {
                          args.data.barColor = 'red';
                          status = 'Expired (not confirmed in time)';
                      }
                      else {
                          args.data.barColor = 'orange';
                          status = 'New';
                      }
                      break;
                  case "Confirmed":
                      var arrivalDeadline = today.addHours(18);

                      if (start < today || (start === today && now > arrivalDeadline)) { // must arrive before 6 pm
                          args.data.barColor = "#f41616";  // red
                          status = 'Late arrival';
                      }
                      else {
                          args.data.barColor = "green";
                          status = "Confirmed";
                      }
                      break;
                  case 'Arrived': // arrived
                      var checkoutDeadline = today.addHours(10);

                      if (end < today || (end === today && now > checkoutDeadline)) { // must checkout before 10 am
                          args.data.barColor = "#f41616";  // red
                          status = "Late checkout";
                      }
                      else
                      {
                          args.data.barColor = "#1691f4";  // blue
                          status = "Arrived";
                      }
                      break;
                  case 'CheckedOut': // checked out
                      args.data.barColor = "gray";
                      status = "Checked out";
                      break;
                  default:
                      status = "Unexpected state";
                      break;    
              }

              // customize the reservation HTML: text, start and end dates
              args.data.html = args.data.text + " (" + start.toString("M/d/yyyy") + " - " + end.toString("M/d/yyyy") + ")" + "<br /><span style='color:gray'>" + status + "</span>";
              
              // reservation tooltip that appears on hover - displays the status text
              args.e.toolTip = status;

              // add a bar highlighting how much has been paid already (using an "active area")
              var paid = args.e.paid;
              var paidColor = "#aaaaaa";
              args.data.areas = [
                  { bottom: 10, right: 4, html: "<div style='color:" + paidColor + "; font-size: 8pt;'>Paid: " + paid + "%</div>", v: "Visible"},
                  { left: 4, bottom: 8, right: 4, height: 2, html: "<div style='background-color:" + paidColor + "; height: 100%; width:" + paid + "%'></div>" }
              ];

          },          
          
          // ...

        };
    });
</script>

Room Filter Using Angular

angularjs-hotel-room-filtering.png

The users can filter rooms using a drop down list with room types (single rooms, double rooms, etc.).

The dropdown is a simple <select> control bound to "roomType" variable using ng-model attribute:

HTML

<div>
  Room Filter: 
  <select ng-model="roomType">
      <option value="0">All</option>
      <option value="1">Single</option>
      <option value="2">Double</option>
      <option value="4">Family</option>                            
  </select>
</div>

We will watch changes of the roomType using Angular $watch mechanism. Changing the room type will reload the resources using the specified filter.

AngularJS Controller

<script>
  var app = angular.module('main', ['daypilot']).controller('DemoCtrl', function($scope, $timeout, $http) {
      
      $scope.roomType = 0;
      $scope.$watch("roomType", function() {
          loadResources();
      });                    
      
      // ...

      function loadResources() {
          var params = {
              capacity: $scope.roomType
          };
          $http.post("backend_rooms.php", params).success(function(data) {
              $scope.schedulerConfig.resources = data;
              $scope.schedulerConfig.visible = true;
          });
      }
  });
</script>

AngularJS Date Formatting

angularjs-hotel-room-booking-date-formatting.png

The demo uses a special AngularJS directive to format and parse the date value between model and view.

Format used to store the date value in the model:

"2015-11-25T00:00:00" (ISO 8601 format)

Format used in the view:

"25/11/2015" (specified in the date-format attribute as "d/M/yyyy")

If the value specified by the user in the view can't be parsed (DayPilot.Date.parse() method returns null) an error message is displayed ("Invalid date").

HTML

<div ng-app="main" ng-controller="NewReservationController" style="padding:10px">
  <!-- ... -->
  
  <div>Start:</div>
  <div><input type="text" id="start" name="start" ng-model="reservation.start" date-format="d/M/yyyy" /> <span ng-show="!reservation.start">Invalid date</span></div>
  <div>End:</div>
  <div><input type="text" id="end" name="end" ng-model="reservation.end" date-format="d/M/yyyy" /> <span ng-show="!reservation.end">Invalid date</span></div>
  <!-- ... -->
  
</div>

AngularJS Model

var app = angular.module('main', ['daypilot']).controller('NewReservationController', function($scope, $timeout, $http) {
  $scope.reservation = {
      start: '2015-10-15T12:00:00',    // use ISO 8601 format for the model
      end: '2015-10-15T12:00:00',      // use ISO 8601 format for the model
  };
  // ...

AngularJS "date-format" Directive

app.directive('dateFormat', function() {
    return { restrict: 'A',
      require: 'ngModel',
      link: function(scope, element, attrs, ngModel) {
        if(ngModel) {

            // parse the input value using the format string, pass the normalized ISO8601 value to the model
            // (unparseable value returns null)

            ngModel.$parsers.push(function (value) {
                var d = DayPilot.Date.parse(value, attrs.dateFormat); 
                return d && d.toString();
            });


            // display the date in the specified format
            // (null value will be returned as null)

            ngModel.$formatters.push(function (value) {
                return value && new DayPilot.Date(value).toString(attrs.dateFormat);
            });
        }
      }
    };
});

Room Editing

angularjs-hotel-room-booking-room-editing.png

In order to allow room editing, we will modify onBeforeResHeaderRender event handler to add a row header active area that will open an edit dialog:

$scope.schedulerConfig = {

    // ...
    
    onBeforeResHeaderRender: function(args) {
    
        // ...
        
        args.resource.areas = [{
            top:3,
            right:4,
            height:14,
            width:14,
            action:"JavaScript",
            js: function(r) {
                var modal = new DayPilot.Modal();
                modal.onClosed = function(args) {
                    loadResources();
                };
                modal.showUrl("room_edit.php?id=" + r.id);
            },
            v:"Hover",
            css:"icon icon-edit",
        }];
    },
    
    // ...
};

It uses an "edit" icon from the DayPilot icon font which is included in "icons" directory. The icon font is imported using CSS:

<link type="text/css" rel="stylesheet" href="icons/style.css" />

The icon opens a modal dialog with room details (room_edit.php):

angularjs-hotel-room-booking-room-edit-dialog.png

room_edit.php

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Edit Room</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/angular.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 rooms WHERE id = :id');
            $stmt->bindParam(':id', $_GET['id']);
            $stmt->execute();
            $room = $stmt->fetch();
        ?>
        <div ng-app="main" ng-controller="EditRoomController" style="padding:10px">
            <input type="hidden" name="id" value="<?php print $_GET['id'] ?>" />
            <h1>Edit Room</h1>
            <div>Name: </div>
            <div><input type="text" id="name" name="name" ng-model="room.name" /></div>
            <div>Capacity:</div>
            <div>
                <select id="capacity" name="capacity" ng-model="room.capacity" >
                    <?php
                        $options = array(1, 2, 4);
                        foreach ($options as $option) {
                            $selected = $option == $room['capacity'] ? ' selected="selected"' : '';
                            $id = $option;
                            $name = $option;
                            print "<option value='$id' $selected>$name</option>";
                        }
                    ?>
                </select>
            </div>
            <div>Status:</div>
            <div>
                <select id="status" name="status" ng-model="room.status" >
                    <?php
                        $options = array("Ready", "Cleanup", "Dirty");
                        foreach ($options as $option) {
                            $selected = $option == $room['status'] ? ' selected="selected"' : '';
                            $id = $option;
                            $name = $option;
                            print "<option value='$id' $selected>$name</option>";
                        }
                    ?>
                </select>
            </div>


            <div class="space"><input type="submit" value="Save" ng-click="save()" /> <a href="" ng-click="cancel()">Cancel</a></div>
        </div>

        <script type="text/javascript">
        var app = angular.module('main', ['daypilot']).controller('EditRoomController', function($scope, $timeout, $http) {
            $scope.room = {
                id: <?php print $room['id'] ?>,
                name: '<?php print $room['name'] ?>',
                capacity: <?php print $room['capacity'] ?>,
                status: '<?php print $room['status'] ?>'
            };
            $scope.save = function() {
                $http.post("backend_room_update.php", $scope.room).success(function(data) {
                    DayPilot.Modal.close(data);
                });
            };
            $scope.cancel = function() {
                DayPilot.Modal.close();
            };

            $("#name").focus();
        });

        </script>
    </body>
</html>

Database Schema (SQLite, MySQL)

1. The sample projects uses SQLite backend by default. The database file (daypilot.sqlite) will be created automatically in the root directory if id doesn't exist.

CREATE TABLE reservations (
    id      INTEGER      PRIMARY KEY,
    name    TEXT,
    start   DATETIME,
    [end]   DATETIME,
    room_id INTEGER,
    status  VARCHAR (30),
    paid    INTEGER
);

CREATE TABLE rooms (
    id       INTEGER      PRIMARY KEY,
    name     TEXT,
    capacity INTEGER,
    status   VARCHAR (30) 
);

2. You can also use the MySQL backend by replacing _db.php with _db_mysql.php file. You need to modify the settings at the beginning of _db_mysql.php according to your configuration.

<?php
$host = "127.0.0.1";
$port = 3306;
$username = "username";
$password = "password";
$database = "hotel";   // the database will be created and initialized if it doesn't exist