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
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
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
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
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
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
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):
Managing Scheduler rows (creating, editing, deleting, moving) is described in detail in the Resource Management tutorial:
Loading Room 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
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:
A "New" reservation that hasn't been confirmed two days before arrival is marked as "Expired (not confirmed in time)".
A "Confirmed" reservation will be marked as "Late arrival" after 6pm on the arrival day.
A reservation with "Arrived" status will be marked as "Late checkout" after 10am on the departure day.
Angular User 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