Features

  • Angular single page application for managing hotel reservations
  • Uses Angular 2 Scheduler from DayPilot Pro for JavaScript
  • Full-screen page layout to use all available space
  • Changing the visible month using date navigator
  • Custom check-in/check-out time (noon)
  • Basic room management (creating, updating, deleting)
  • Room status (ready, cleanup, dirty)
  • Reservation management (creating, moving, updating, deleting)
  • Reservation status (new, confirmed, arrived, checked-out)
  • Simple token-based authentication integrated
  • Includes a PHP/MySQL backend (with JSON-based endpoints)
  • Angular 4
  • Uses Angular CLI 1.0
  • Includes a trial version of DayPilot Pro for JavaScript (see License below)

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.

Running the Angular Hotel Room Booking Application

The application is split into two projects:

  • Angular fronted project (angular2-hotel-php-frontend)
  • PHP backend project (angular2-hotel-php-backend)

To run the application you need to run both projects:

1. Runing the PHP project

Start the PHP project using PHP built-in web server on port 8090:

Linux:

php -S 127.0.0.1:8090 -t /home/daypilot/tutorials/angular2-hotel-php-backend

Windows:

php.exe -S 127.0.0.1:8090 -t C:\Users\daypilot\tutorials\angular2-hotel-php-backend

By default, the PHP backend projects uses SQLite database (it automatically creates and initializes a new db file on startup, just make sure the process has write permissions for the web app directory).

You can switch to MySQL by copying _db_mysql.php file over _db.php. Don't forget to edit MySQL connection parameters at the top of the file (server, port, username, password).

Note that PHP 7 is required (backend_login.php script uses random_bytes() function which is only available in PHP 7).

2. Runing the Angular project

You can run the Angular project using npm:

npm run start

The Angular project uses a proxy to redirect relative "/api/*" URLs to the PHP web server running on port 8090.

proxy.conf.json

{
  "/api": {
    "target": "http://localhost:8090",
    "secure": false
  }
}

Full Screen Layout with Sidebar

angular2-hotel-room-booking-full-screen-layout.png

This project integrates the Scheduler in a page with full-screen layout. This is explained in more details in the following tutorial:

Changing Dates

angular2-hotel-room-booking-date-navigation.png

This project uses the Navigator component in a collapsible sidebar on the left side of the page to change the date currently visible in the Scheduler. For more details on using the Navigator please see the following tutorial:

Reservation Creating and Editing

angular2-hotel-room-booking-create-reservation.png

For more details on creating reservations using drag and drop and reservation editing please see the following tutorial:

Loading Rooms

angular2-hotel-room-booking-loading-rooms.png

The rooms are loaded using DataService in ngAfterViewInit() method.

scheduler/scheduler.component.ts

import {Component, ViewChild, AfterViewInit, ChangeDetectorRef} from "@angular/core";
import {DayPilotSchedulerComponent} from "daypilot-pro-angular";
import {
  DataService, ReservationData, CreateReservationParams, CreateRoomParams,
  UpdateReservationParams, UpdateRoomParams
} from "../backend/data.service";
import {RoomEditComponent} from "./room-edit.component";
import {RoomCreateComponent} from "./room-create.component";
import {ReservationCreateComponent} from "./reservation-create.component";
import {ReservationEditComponent} from "./reservation-edit.component";
import {Router} from "@angular/router";

@Component({
  selector: 'scheduler-component',
  template: `
  ...
  <daypilot-scheduler [config]="config" [events]="events" (viewChange)="viewChange($event)" #scheduler></daypilot-scheduler>
  ...
`,
  styles: [``]
})
export class SchedulerComponent implements AfterViewInit {

  @ViewChild("scheduler") scheduler: DayPilotSchedulerComponent;

  events: any[] = [];

  config: any = {
    scale: "Manual",
    // ...
  }

  constructor(private ds: DataService, private router: Router) {
  }

  ngAfterViewInit(): void {
    this.ds.getRooms().subscribe(result => this.config.resources = result);
  }

}

Room Status

angular2-hotel-room-booking-room-status.png

The room status is highlighted using a special bar which is added as an active area using onBeforeRowHeaderRender event handler:

import {Component, ViewChild, AfterViewInit, ChangeDetectorRef} from "@angular/core";
import {DayPilotSchedulerComponent} from "daypilot-pro-angular";
import {
  DataService, ReservationData, CreateReservationParams, CreateRoomParams,
  UpdateReservationParams, UpdateRoomParams
} from "../backend/data.service";
import {RoomEditComponent} from "./room-edit.component";
import {RoomCreateComponent} from "./room-create.component";
import {ReservationCreateComponent} from "./reservation-create.component";
import {ReservationEditComponent} from "./reservation-edit.component";
import {Router} from "@angular/router";

@Component({
  selector: 'scheduler-component',
  template: `
  ...
  <daypilot-scheduler [config]="config" [events]="events" (viewChange)="viewChange($event)" #scheduler></daypilot-scheduler>
  ...
`,
  styles: [``]
})
export class SchedulerComponent implements AfterViewInit {

  @ViewChild("scheduler") scheduler: DayPilotSchedulerComponent;

  events: any[] = [];

  config: any = {
    // ...
    onBeforeRowHeaderRender: args => {
      let beds = function(count) {
        return count + " bed" + (count > 1 ? "s" : "");
      };

      args.row.columns[0].html = beds(args.row.data.capacity);
      args.row.columns[1].html = args.row.data.status;

      var color = "";
      switch (args.row.data.status) {
        case "Ready":
          color = "green"
          break;
        case "Dirty":
          color = "red";
          break;
        case "Cleanup":
          color = "orange";
          break;
      }


      // status
      args.row.columns[1].areas = [];
      args.row.columns[1].areas.push({
        right: 2,
        top: 2,
        bottom: 2,
        width: 3,
        backColor: color
      });

      // context menu icon
      args.row.areas = [];
      args.row.areas.push({
        top:3,
        right:4,
        visibility: "Hover",
        style: "font-size: 12px; background-color: #f9f9f9; border: 1px solid #ccc; padding: 2px 2px 0px 2px; cursor:pointer",
        icon: "icon-triangle-down",
        action: "ContextMenu"
      });
    },
  };


  // ...

}

Room Editing

angular2-hotel-room-booking-room-editing.png

The rooms can be edited by invoking a context menu. The "Edit..." menu item opens a modal dialog which you can use to change room properties (name, size, status):

angular2-hotel-room-booking-room-properties.png

Managing Scheduler rows (creating, editing, deleting, moving) is described in detail in the Resource Management tutorial:

Loading Reservations

angular2-hotel-room-booking-loading-reservations.png

Reservations are loaded using viewChange() event handler. This event is fired whenever the Scheduler is updated because of a configuration change ("config" attribute). The event handler calls DataService.getReservations() and limits the events to the date range that is currently visible in the Scheduler (visibleStart()/visibleEnd() methods).

import {Component, ViewChild, AfterViewInit, ChangeDetectorRef} from "@angular/core";
import {DayPilotSchedulerComponent} from "daypilot-pro-angular";
import {
  DataService, ReservationData, CreateReservationParams, CreateRoomParams,
  UpdateReservationParams, UpdateRoomParams
} from "../backend/data.service";
import {RoomEditComponent} from "./room-edit.component";
import {RoomCreateComponent} from "./room-create.component";
import {ReservationCreateComponent} from "./reservation-create.component";
import {ReservationEditComponent} from "./reservation-edit.component";
import {Router} from "@angular/router";

@Component({
  selector: 'scheduler-component',
  template: `
  ...
  <daypilot-scheduler [config]="config" [events]="events" (viewChange)="viewChange($event)" #scheduler></daypilot-scheduler>
  ...
`,
  styles: [``]
})
export class SchedulerComponent implements AfterViewInit {

  @ViewChild("scheduler") scheduler: DayPilotSchedulerComponent;

  events: any[] = [];

  config: any = {
    // ...
  };

  constructor(private ds: DataService, private router: Router) {
  }


  viewChange(args) {
    // quit if the date range hasn't changed
    if (!args.visibleRangeChanged) {
      return;
    }

    let from = this.scheduler.control.visibleStart();
    let to = this.scheduler.control.visibleEnd();

    this.ds.getReservations(from, to).subscribe(result => {
      this.events = result;
    });
  }


}

Reservation Status

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

The reservation status is saved in the database, in the "status" field. It can have one of the following values:

  • New
  • Confirmed
  • Arrived
  • CheckedOut

We will use the onBeforeEventRender event customization event handler to set the reservation color to highlight the status:

import {Component, ViewChild, AfterViewInit, ChangeDetectorRef} from "@angular/core";
import {DayPilotSchedulerComponent} from "daypilot-pro-angular";
import {
  DataService, ReservationData, CreateReservationParams, CreateRoomParams,
  UpdateReservationParams, UpdateRoomParams
} from "../backend/data.service";
import {RoomEditComponent} from "./room-edit.component";
import {RoomCreateComponent} from "./room-create.component";
import {ReservationCreateComponent} from "./reservation-create.component";
import {ReservationEditComponent} from "./reservation-edit.component";
import {Router} from "@angular/router";

@Component({
  selector: 'scheduler-component',
  template: `
  ...
  <daypilot-scheduler [config]="config" [events]="events" (viewChange)="viewChange($event)" #scheduler></daypilot-scheduler>
  ...
`,
  styles: [``]
})
export class SchedulerComponent implements AfterViewInit {

  @ViewChild("scheduler") scheduler: DayPilotSchedulerComponent;

  events: any[] = [];


  config: any = {
    // ...
    onBeforeEventRender: args => {
      let start = new DayPilot.Date(args.data.start);
      let end = new DayPilot.Date(args.data.end);

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

      // customize the reservation bar color and tooltip depending on status
      switch (args.e.status) {
        case "New":
          let 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":
          let 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
          let 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")
      let paid = args.e.paid;
      let 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>" }
      ];

    },
    // ...
  };

  // ...

}

Some additional status notifications are set depending on the status and the current date/time:

  1. A "New" reservation that hasn't been confirmed two days before arrival is marked as "Expired (not confirmed in time)".
  2. A "Confirmed" reservation will be marked as "Late arrival" after 6pm on the arrival day.
  3. A reservation with "Arrived" status will be marked as "Late checkout" after 10am on the departure day.

User Authentication

angular2-hotel-room-booking-authentication.png

This project includes simple token-based authentication. The root URL (which contains the main Scheduler UI) is protected using a route guard:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import {SchedulerComponent} from "../scheduler/scheduler.component";
import {LoginComponent} from "./login.component";
import {AuthGuard} from "./auth.guard";

const routes: Routes = [
  { path: '', component: SchedulerComponent, canActivate: [AuthGuard] },
  { path: 'login', component: LoginComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
  providers: []
})
export class AppRoutingModule { }

All PHP endpoints are protected and require an authentication token.

A special tutorial that explains the authentication logic in details is in the works and will be published soon.

PHP Backend JSON Endpoints

This is a list of all server-side endpoints:

  • backend_login.php (issues an authentication token upon login using username and password)
  • backend_reservation_create.php (creates a new reservation)
  • backend_reservation_delete.php (deletes a reservation)
  • backend_reservation_move.php (moves a reservation to a new location: date and room)
  • backend_reservation_update.php (updates reservations properties, such as guest name and status)
  • backend_reservations.php (returns all reservations for a given date range)
  • backend_room_create.php (creates a new room)
  • backend_room_delete.php (deletes a room)
  • backend_room_update.php (updates room properties, such as name, size and status)
  • backend_rooms.php (returns all rooms)
  • backend_user.php (verifies an authentication token and returns user details on success)

Data Access Service (Angular)

All HTTP calls to the PHP backend are done using a DataService class that wraps the endpoints.

import {Http, Response, RequestOptions, Headers} from '@angular/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Rx';
import 'rxjs/Rx';
import {DayPilot} from 'daypilot-pro-angular';
import {Router} from "@angular/router";

@Injectable()
export class DataService {

  user: any;

  constructor(private http : Http, private router: Router){
  }

  requestOptions(): RequestOptions {
    let storedUser = localStorage.getItem("user");
    let headers = new Headers();
    if (storedUser) {
      let user = JSON.parse(storedUser);
      if (user) {
        headers.append("X-Auth-Token", user.token);
      }
    }
    return new RequestOptions({headers: headers});
  }

  getUser(): Observable<any> {
    return this.http.post("/api/backend_user.php", {}, this.requestOptions()).map((response:Response) => {
      let responseObject = response.json();
      this.user = responseObject.user;
      return responseObject;
    });
  }

  doLogin(user:{username: string, password: string}): Observable<any> {
    return this.http.post("/api/backend_login.php", user).map((response:Response) => response.json());
  }

  isLoggedIn() {
    return !!localStorage.getItem("user");
  }

  logout() {
    localStorage.removeItem("user");
    this.router.navigate(["/login"]);
  }

  getReservations(from: DayPilot.Date, to: DayPilot.Date): Observable<any[]> {
    return this.http.post("/api/backend_reservations.php", {start: from.toString(), end: to.toString()}, this.requestOptions()).map((response:Response) => response.json());
  }

  createReservation(data: CreateReservationParams): Observable<ReservationData> {
    return this.http.post("/api/backend_reservation_create.php", data, this.requestOptions()).map((response:Response) => {
      return {
        id: response.json().id,
        start: data.start,
        end: data.end,
        resource: data.room,
        text: data.name,
        status: "New",
        paid: "0"
      };
    });
  }

  deleteReservation(id: string): Observable<any> {
    return this.http.post("/api/backend_reservation_delete.php", {id: id}, this.requestOptions()).map((response:Response) => response.json());
  }

  updateReservation(params: UpdateReservationParams): Observable<ReservationData> {
    return this.http.post("/api/backend_reservation_update.php", params, this.requestOptions()).map((response:Response) => {
      return {
        id: params.id,
        start: params.start,
        end: params.end,
        text: params.name,
        resource: params.room,
        status: params.status,
        paid: params.paid
      };
    });
  }

  moveReservation(params: MoveReservationParams): Observable<any> {
    return this.http.post("/api/backend_reservation_move.php", params, this.requestOptions()).map((response:Response) => response.json());
  }

  getRooms(): Observable<any[]> {
    return this.http.post("/api/backend_rooms.php", {capacity: 0}, this.requestOptions()).map((response:Response) => response.json());
  }

  createRoom(params: CreateRoomParams): Observable<RoomData> {
    return this.http.post("/api/backend_room_create.php", params, this.requestOptions()).map((response:Response) => {
      return {
        name: params.name,
        capacity: params.capacity,
        status: "Ready",
        id: response.json().id,
      };
    });
  }

  updateRoom(params: UpdateRoomParams): Observable<RoomData> {
    return this.http.post("/api/backend_room_update.php", params, this.requestOptions()).map((response:Response) => {
      return {
        id: params.id,
        name: params.name,
        capacity: params.capacity,
        status: params.status
      };
    });
  }

  deleteRoom(id: string): Observable<any> {
    return this.http.post("/api/backend_room_delete.php", {id: id}, this.requestOptions()).map((response:Response) => response.json());
  }

}

export interface CreateReservationParams {
  start: string;
  end: string;
  name: string;
  room: string | number;
}

export interface UpdateReservationParams {
  id: string;
  start: DayPilot.Date;
  end: DayPilot.Date;
  room: string;
  name: string;
  status: string;
  paid: number | string;
}

export interface MoveReservationParams {
  id: string;
  start: DayPilot.Date;
  end: DayPilot.Date;
  room: string;
}

export interface CreateRoomParams {
  name: string;
  capacity: number;
}

export interface UpdateRoomParams {
  id: string;
  name: string;
  capacity: number;
  status: string;
}

export interface ReservationData {
  id: string | number;
  start: string | DayPilot.Date;
  end: string | DayPilot.Date;
  text: string;
  resource: string | number;
  status: string;
  paid: number | string;
}

export interface RoomData {
  id: string;
  name: string;
  capacity: number;
  status: string;
}

MySQL Database Schema

If the database doesn't exist yet, it is created and initialized automatically in _db.php using the following schema:

CREATE DATABASE IF NOT EXISTS `hotel`;
USE `hotel`;

CREATE TABLE IF NOT EXISTS `reservation` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` text,
  `start` datetime DEFAULT NULL,
  `end` datetime DEFAULT NULL,
  `room_id` int(11) DEFAULT NULL,
  `status` varchar(30) DEFAULT NULL,
  `paid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
);

CREATE TABLE IF NOT EXISTS `room` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` text,
  `capacity` int(11) DEFAULT NULL,
  `status` varchar(30) DEFAULT NULL,
  PRIMARY KEY (`id`)
);

CREATE TABLE IF NOT EXISTS `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` text,
  `password` text,
  PRIMARY KEY (`id`)
);

CREATE TABLE IF NOT EXISTS `user_token` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) DEFAULT NULL,
  `created` datetime DEFAULT NULL,
  `expires` datetime DEFAULT NULL,
  `token` varchar(64) DEFAULT NULL,
  PRIMARY KEY (`id`)
);