Features

  • Angular single-page application for managing hotel reservations.

  • Uses Angular Scheduler component from DayPilot Pro for JavaScript.

  • Full-screen page layout to use all available space.

  • User can change the visible month using a date picker.

  • 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 12

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.

Scheduler UI Builder

This project was generated using Scheduler UI Builder. You can use this visual tool to configure the Scheduler appearance and properties and generate a downloadable Angular project.

Running the Angular Hotel Room Booking Application

The application is split into two projects:

  • Angular fronted project (angular-hotel-php-frontend)

  • PHP backend project (angular-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/angular-hotel-php-backend

Windows:

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

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

You can switch to MySQL by updating the _db.php to include _db_mysql.php instead of _db_sqlite.php. Don't forget to edit MySQL connection parameters at the top of the _db_mysql.php ($server, $port, $username, and $password variables).

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

First, it's necessary to download the NPM dependencies:

npm install

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 Scheduler Layout with Integrated Sidebar

angular hotel room booking mysql php full screen layout

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

Changing the Reservation Month

angular hotel room booking mysql php date picker

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:

Modal Dialog: Adding and Editing Reservations

angular hotel room booking mysql php modal dialog editing

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

You can design your own modal dialog using DayPilot Modal Builder.

Loading Hotel Rooms

angular hotel room booking mysql php loading rooms

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

scheduler/scheduler.component.ts

import {Component, ViewChild, AfterViewInit, ChangeDetectorRef} from "@angular/core";
import {DayPilot, 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: DayPilot.EventData[] = [];

  config: DayPilot.SchedulerConfig = {
    scale: "Manual",
    // ...
  }

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

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

}

Hotel Room Status

angular hotel room booking mysql php room status

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

import {Component, ViewChild, AfterViewInit, ChangeDetectorRef} from "@angular/core";
import {DayPilot, 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: DayPilot.EventData[] = [];

  config: DayPilot.SchedulerConfig = {
    // ...
    onBeforeRowHeaderRender: args => {
      const beds = (count) => {
        return count + ' bed' + (count > 1 ? 's' : '');
      };

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

      let 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[2].areas = [];
      args.row.columns[2].areas.push({
        right: 2,
        top: 2,
        bottom: 2,
        width: 3,
        backColor: color
      });

      // context menu icon
      args.row.columns[0].areas = [];
      args.row.columns[0].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'
      });
    }
  };


  // ...

}

Hotel Room Editing

angular hotel booking mysql php room editing

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):

angular hotel booking mysql php room editing modal dialog

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

Loading Room Reservations

angular hotel room booking mysql php loading reservations

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 selects events for the date range that is currently visible in the Scheduler (visibleStart() and visibleEnd() methods).

import {Component, ViewChild, AfterViewInit, ChangeDetectorRef} from "@angular/core";
import {DayPilot, 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: DayPilot.EventData[] = [];

  config: DayPilot.SchedulerConfig = {
    // ...
  };

  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

angular hotel room booking mysql php reservation status

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 {DayPilot, 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: DyPilot.EventData[] = [];


  config: DayPilot.SchedulerConfig = {
    // ...
    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 = DayPilot.Date.today();
      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.

Angular User Authentication

angular2 hotel room booking authentication

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 handled by a DataService class that wraps the endpoints.

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import {DayPilot} from 'daypilot-pro-angular';
import {Router} from "@angular/router";
import {HttpClient} from "@angular/common/http";
import {map} from "rxjs/operators";

@Injectable()
export class DataService {

  user: any;

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

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

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

  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()}) as Observable<any>;
  }

  createReservation(data: CreateReservationParams): Observable<ReservationData> {
    return this.http.post("/api/backend_reservation_create.php", data).pipe(map((response:any) => {
      return {
        id: response.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});
  }

  updateReservation(params: UpdateReservationParams): Observable<ReservationData> {
    return this.http.post("/api/backend_reservation_update.php", params).pipe(map((response:any) => {
      return params;
    }));
  }

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

  getRooms(): Observable<any[]> {
    return this.http.post("/api/backend_rooms.php", {capacity: 0}) as Observable<any>;
  }

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

  updateRoom(params: UpdateRoomParams): Observable<RoomData> {
    return this.http.post("/api/backend_room_update.php", params).pipe(map((response:any) => {
      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});
  }

}

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

export interface UpdateReservationParams {
  id: string;
  start: DayPilot.Date;
  end: DayPilot.Date;
  resource: string;
  text: 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`)
);

History

  • June 17, 2021: Upgraded to Angular 12, DayPilot Pro for JavaScript 2021.2.5007. Using DayPilot.Modal.form() for modal dialogs.

  • November 27, 2020: Upgraded to Angular 11, DayPilot Pro for JavaScript 2020.4.4786.

  • May 25, 2020: Upgraded to Angular 9, DayPilot Pro for JavaScript 2020.2.4470, tabular mode.

  • June 19, 2019: Upgraded to Angular 8, Angular CLI 8., DayPilot Pro for JavaScript 2019.2.3871, styling improvements.

  • June 3, 2018: Upgraded to Angular 6, Angular CLI 6., DayPilot Pro for JavaScript 2018.2.3297.

  • February 20, 2018: Upgraded to Angular 5, Angular CLI 1.5, DayPilot Pro for JavaScript 2018.1.3169.

  • April 6, 2017: Switched to Angular 4, Angular CLI 1.0. Includes AOT compilation support.

  • February 20, 2017: Initial release, based on HTML5 Hotel Room Booking (JavaScript/PHP/MySQL) tutorial