Welcome to this tutorial, where we bring together two powerful tools: the DHTMLX Scheduler library and the Angular framework, to create a comprehensive hotel room booking application.
In this post our goal is to create an application that will look exactly like this:
Our Angular hotel booking app will be able to display hotel rooms, room types, and room status, reservations made for particular dates and booking status. The app will also allow performing CRUD operations.
If you’re new to configuring DHTMLX Scheduler for room reservation purposes or integrating it into Angular applications, don’t worry. We’ve got you covered with dedicated tutorials for both:
We also guide you through the process of setting up a Node.js server. You can find a separate tutorial devoted to working with Scheduler and Node.js in the documentation.
You can find the full source code of the Angular Hotel Room Reservation demo in our GitHub repository.
And here is the video tutorial with all the steps, which you can follow to get the result faster.
Let’s dive in!
Step 0 – Prerequisites
Before we begin, make sure you have Node.js and Angular CLI.
Step 1 – Preparing App
To create an app skeleton, run the following command:
ng new room-reservation-angular
After the operation finishes, we can go to the app directory and run the application:
cd room-reservation-angular
ng serve
Now if we open http: //127.0.0.1:4200 we should see the initial page. The ng serve
command will watch the source file and, if necessary, will change and rebuild the app.
Step 2 – Create the Data Models
Let’s define the Reservation
, Room
, RoomType
, CleaningStatus
, and BookingStatus
models. Run the following commands:
ng generate interface models/reservation model
ng generate interface models/room model
ng generate interface models/roomType model
ng generate interface models/cleaningStatus model
ng generate interface models/bookingStatus model
In the newly created reservation.model.ts
file inside the models
folder, we will add the following code:
export interface Reservation {
id: number;
start_date: string;
end_date: string;
text: string;
room: string;
booking_status: string;
is_paid: string;
}
In the room.model.ts
, room-type.model.ts
, cleaning-status.model.ts
, booking-status.model.ts
files, add the next lines of code:
export interface Room {
id: number;
value: string;
label: string;
type: string;
cleaning_status: string;
}
export interface RoomType {
id: string;
value: string;
label: string;
}
export interface CleaningStatus {
id: string;
value: string;
label: string;
color: string;
}
export interface BookingStatus {
id: string;
value: string;
label: string;
}
Step 3 – Creating Scheduler Component
Download the latest free 30-day trial of the PRO edition of DHTMLX Scheduler. Extract the downloaded package to your local machine to the root folder of your project. You can read the article “Adding PRO Edition into Project” for more details.
To be able to embed Scheduler into the app, you should get the DHTMLX Scheduler code. Run the following command:
npm install ./scheduler_6.0.5_trial
Create a new component. For this, run the following command:
ng generate component scheduler --skip-tests
The newly created scheduler.component.html
file inside the scheduler
folder will contain the template for the scheduler. Let’s add the next lines of code:
<div #scheduler_here class='dhx_cal_container' style='width:100%; height:100vh'>
<div class='dhx_cal_navline'>
<div style='font-size:16px;padding:4px 20px;'>
Show rooms:
<select id='room_filter' [(ngModel)]='selectedRoomType' (ngModelChange)='filterRoomsByType($event)'></select>
</div>
<div class='dhx_cal_prev_button'> </div>
<div class='dhx_cal_next_button'> </div>
<div class='dhx_cal_today_button'></div>
<div class='dhx_cal_date'></div>
</div>
<div class='dhx_cal_header'></div>
<div class='dhx_cal_data'></div>
</div>
We utilized the ngModel
and ngModelChange
directives to establish interaction between the select
element and data within the component. Please, add the FormsModule
module to the app.module.ts file
.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { SchedulerComponent } from './scheduler/scheduler.component';
import { FormsModule } from '@angular/forms';
@NgModule({
declarations: [
AppComponent,
SchedulerComponent
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
We’ll declare scheduler styles in the separate file named scheduler.component.css
. Styles can look in the following way:
@import '~dhtmlx-scheduler/codebase/dhtmlxscheduler_flat.css';
:host {
display: block;
position: relative;
height: 100%;
width: 100%;
}
html, body {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
}
.dhx_cal_container #room_filter:focus {
outline: 1px solid #52daff;
}
.timeline-cell-inner {
height: 100%;
width: 100%;
table-layout: fixed;
}
.timeline-cell-inner td {
border-left: 1px solid #cecece;
}
.dhx_section_time select {
display: none;
}
.timeline_weekend {
background-color: #FFF9C4;
}
.timeline_item_cell {
width: 32%;
height: 100% !important;
font-size: 14px;
text-align: center;
line-height: 50px;
}
.cleaning_status {
position: relative;
}
.timeline_item_separator {
background-color: #CECECE;
width: 1px;
height: 100% !important;
}
.dhx_cal_event_line {
background-color: #FFB74D !important;
}
.event_1 {
background-color: #FFB74D !important;
}
.event_2 {
background-color: #9CCC65 !important;
}
.event_3 {
background-color: #40C4FF !important;
}
.event_4 {
background-color: #BDBDBD !important;
}
.booking_status,
.booking_paid {
position: absolute;
right: 5px;
}
.booking_status {
top: 2px;
}
.booking_paid {
bottom: 2px;
}
.dhx_cal_event_line:hover .booking-option {
background: none !important;
}
.dhx_cal_header .dhx_scale_bar {
line-height: 26px;
color: black;
}
.dhx_section_time select {
display: none
}
.dhx_mini_calendar .dhx_year_week,
.dhx_mini_calendar .dhx_scale_bar {
height: 30px !important;
}
.dhx_cal_light_wide .dhx_section_time {
text-align: left;
}
.dhx_cal_light_wide .dhx_section_time > input:first-child {
margin-left: 10px;
}
.dhx_cal_light_wide .dhx_section_time input {
border: 1px solid #aeaeae;
padding-left: 5px;
}
.dhx_cal_light_wide .dhx_readonly {
padding: 3px;
}
.collection_label .timeline_item_cell {
line-height: 60px;
}
.dhx_cal_radio label,
.dhx_cal_radio input {
vertical-align: middle;
}
.dhx_cal_radio input {
margin-left: 10px;
margin-right: 2px;
}
.dhx_cal_radio input:first-child {
margin-left: 5px;
}
.dhx_cal_radio {
line-height: 19px;
}
.dhtmlXTooltip.tooltip {
color: #4d4d4d;
font-size: 15px;
line-height: 140%;
}
To make the scheduler container occupy the entire space of the body, you need to add the following styles to the styles.css
file located in the src
folder:
body,
html {
width: 100%;
height: 100%;
margin: unset;
}
To proceed, we need to import the required modules and add the necessary code lines to the scheduler.component.ts
file:
import { Component, ElementRef, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { Scheduler } from 'dhtmlx-scheduler';
import { Reservation } from "../models/reservation.model";
import { Room } from "../models/room.model";
@Component({
encapsulation: ViewEncapsulation.None,
selector: 'scheduler',
styleUrls: ['./scheduler.component.css'],
templateUrl: './scheduler.component.html'
})
export class SchedulerComponent implements OnInit {
@ViewChild('scheduler_here', {static: true}) schedulerContainer!: ElementRef;
private scheduler: any;
rooms: any[] = [];
roomTypes: any[] = [];
cleaningStatuses: any[] = [];
bookingStatuses: any[] = [];
selectedRoomType: string = '';
public filterRoomsByType(value: string): void {
const currentRooms = value === 'all' ? this.rooms.slice() : this.rooms.filter(room => room.type === value);
this.scheduler.updateCollection('currentRooms', currentRooms);
};
ngOnInit() {
const scheduler = Scheduler.getSchedulerInstance();
this.scheduler = scheduler;
this.selectedRoomType = 'all';
scheduler.plugins({
limit: true,
collision: true,
timeline: true,
editors: true,
minical: true,
tooltip: true
});
scheduler.locale.labels['section_text'] = 'Name';
scheduler.locale.labels['section_room'] = 'Room';
scheduler.locale.labels['section_booking_status'] = 'Booking Status';
scheduler.locale.labels['section_is_paid'] = 'Paid';
scheduler.locale.labels.section_time = 'Time';
scheduler.xy.scale_height = 30;
scheduler.config.details_on_create = true;
scheduler.config.details_on_dblclick = true;
scheduler.config.prevent_cache = true;
scheduler.config.show_loading = true;
scheduler.config.date_format = '%Y-%m-%d %H:%i';
this.rooms = scheduler.serverList('rooms');
this.roomTypes = scheduler.serverList('roomTypes');
this.cleaningStatuses = scheduler.serverList('cleaningStatuses');
this.bookingStatuses = scheduler.serverList('bookingStatuses');
scheduler.config.lightbox.sections = [
{ name: 'text', map_to: 'text', type: 'textarea', height: 24 },
{ name: 'room', map_to: 'room', type: 'select', options: scheduler.serverList("currentRooms") },
{ name: 'booking_status', map_to: 'booking_status', type: 'radio', options: scheduler.serverList('bookingStatuses') },
{ name: 'is_paid', map_to: 'is_paid', type: 'checkbox', checked_value: true, unchecked_value: false },
{ name: 'time', map_to: 'time', type: 'calendar_time' }
];
scheduler.locale.labels['timeline_tab'] = 'Timeline';
scheduler.createTimelineView({
name: 'timeline',
y_property: 'room',
render: 'bar',
x_unit: 'day',
x_date: '%d',
dy: 52,
event_dy: 48,
section_autoheight: false,
y_unit: scheduler.serverList('currentRooms'),
second_scale: {
x_unit: 'month',
x_date: '%F %Y'
},
columns: [
{ label: 'Room', width: 70, template: (room: Room) => room.label },
{ label: 'Type', width: 90, template: (room: Room) => this.getRoomType(room.type) },
{ label: 'Status', width: 90, template: this.generateCleaningStatusColumnTemplate.bind(this) }
]
});
this.schedulerContainer.nativeElement.addEventListener('input', (event: any) => {
const target = event.target as HTMLSelectElement;
if (target instanceof HTMLElement && target.closest('.cleaning-status-select')) {
this.handleCleaningStatusChange(target);
}
});
scheduler.date['timeline_start'] = scheduler.date.month_start;
scheduler.date['add_timeline'] = (date: any, step: any) => scheduler.date.add(date, step, 'month');
scheduler.attachEvent('onBeforeViewChange', (old_mode, old_date, mode, date) => {
const year = date.getFullYear();
const month = (date.getMonth() + 1);
const d = new Date(year, month, 0);
const daysInMonth = d.getDate();
scheduler.matrix['timeline'].x_size = daysInMonth;
return true;
}, {});
scheduler.templates.event_class = (start, end, event) => 'event_' + (event.booking_status || '');
function getPaidStatus(isPaid: any) {
return isPaid ? 'paid' : 'not paid';
}
const eventDateFormat = scheduler.date.date_to_str('%d %M %Y');
scheduler.templates.event_bar_text = (start, end, event) => {
const paidStatus = getPaidStatus(event.is_paid);
const startDate = eventDateFormat(event.start_date);
const endDate = eventDateFormat(event.end_date);
return [event.text + '<br />',
startDate + ' - ' + endDate,
`<div class='booking_status booking-option'>${this.getBookingStatus(event.booking_status)}</div>`,
`<div class='booking_paid booking-option'>${paidStatus}</div>`].join('');
};
scheduler.templates.tooltip_text = (start, end, event) => {
const room = this.getRoom(event.room) || {label: ''};
const html = [];
html.push('Booking: <b>' + event.text + '</b>');
html.push('Room: <b>' + room.label + '</b>');
html.push('Check-in: <b>' + eventDateFormat(start) + '</b>');
html.push('Check-out: <b>' + eventDateFormat(end) + '</b>');
html.push(this.getBookingStatus(event.booking_status) + ', ' + getPaidStatus(event.is_paid));
return html.join('<br>')
};
scheduler.templates.lightbox_header = (start, end, ev) => {
const formatFunc = scheduler.date.date_to_str('%d.%m.%Y');
return formatFunc(start) + ' - ' + formatFunc(end);
};
scheduler.attachEvent('onEventCollision', (ev, evs) => {
for (let i = 0; i < evs.length; i++) {
if (ev.room != evs[i].room) {
continue;
}
scheduler.message({
type: 'error',
text: 'This room is already booked for this date.'
});
}
return true;
}, {});
scheduler.attachEvent('onEventCreated', (event_id) => {
const ev = scheduler.getEvent(event_id);
ev.booking_status = 1;
ev.is_paid = false;
ev.text = 'new booking';
}, {});
scheduler.addMarkedTimespan({days: [0, 6], zones: 'fullday', css: 'timeline_weekend'});
function setHourToNoon(event: any) {
event.start_date.setHours(12, 0, 0);
event.end_date.setHours(12, 0, 0);
}
scheduler.attachEvent('onEventLoading', (ev) => {
this.filterRoomsByType('all');
const select = document.getElementById('room_filter') as HTMLSelectElement;
if (select !== null) {
const selectHTML = [`<option value='all'>All</option>`];
for (let i = 1; i < this.roomTypes.length + 1; i++) {
const roomType = this.roomTypes[i-1];
selectHTML.push(`<option value='${roomType.key}'>${this.getRoomType(roomType.key)}</option>`);
}
select.innerHTML = selectHTML.join('');
}
setHourToNoon(ev);
return true;
}, {});
scheduler.attachEvent('onEventSave', (id, ev, is_new) => {
if (!ev.text) {
scheduler.alert('Text must not be empty');
return false;
}
setHourToNoon(ev);
return true;
}, {});
scheduler.attachEvent('onEventChanged', (id, ev) => {
setHourToNoon(ev);
}, {});
scheduler.init(this.schedulerContainer.nativeElement, new Date(), 'timeline');
}
ngOnDestroy() {
const scheduler = this.scheduler;
scheduler && scheduler.destructor();
}
getRoom(key: any) {
return this.rooms.find(room => room.key === key) || null;
}
getRoomType(key: any) {
const roomType = this.roomTypes.find(item => item.key === key);
return roomType ? roomType.label : null;
}
getCleaningStatus(key: any) {
const cleaningStatus = this.cleaningStatuses.find(item => item.key === key);
return cleaningStatus ? cleaningStatus.label : null;
}
getCleaningStatusIndicator(key: any) {
const cleaningStatus = this.cleaningStatuses.find(item => item.key === key);
return cleaningStatus ? cleaningStatus.color : null;
}
getBookingStatus(key: any) {
const bookingStatus = this.bookingStatuses.find(item => item.key === key);
return bookingStatus ? bookingStatus.label : '';
}
handleCleaningStatusChange(target: HTMLSelectElement) {
const roomId = target.getAttribute('room-id');
const selectedCleaningStatus = target.value;
const roomToUpdate = this.rooms.find(room => room.id == roomId);
if (roomToUpdate) {
roomToUpdate.cleaning_status = selectedCleaningStatus;
}
const backgroundColor = this.getCleaningStatusIndicator(selectedCleaningStatus);
target.style.backgroundColor = backgroundColor;
}
generateCleaningStatusColumnTemplate(room: Room) {
const backgroundColor = this.getCleaningStatusIndicator(room.cleaning_status);
const rgbaBackgroundColor = this.hexToRgba(backgroundColor, 0.2);
const selectHTML = [`
<select class='cleaning-status-select'
room-id='${room.id}'
style='width: 100%; height: 52px; background-color: ${rgbaBackgroundColor}; outline: none; border: none;'>
`];
this.cleaningStatuses.forEach(status => {
const optionHTML = `
<option value='${status.key}' style='background-color: ${status.color};' ${room.cleaning_status === status.key ? 'selected' : ''}>
${this.getCleaningStatus(status.key)}
</option>
`;
selectHTML.push(optionHTML);
});
selectHTML.push(`</select>`);
return selectHTML.join('');
}
hexToRgba(hex: any, alpha: any) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
}
Feel free to check out the complete code of scheduler.component.ts
on GitHub.
Now let’s add the new component to the page. For this purpose, open app.component.html
(located in src/app
) and insert the scheduler tag in there:
<scheduler></scheduler>
At the next step, we will proceed to load and save data.
Step 4 – Providing and Saving Data
Data Loading
To add data loading to the Angular Scheduler, you need to add the reservation and collections services. But before that, let’s create and configure an environment file for a project. Run the following command:
ng generate environments
Let’s also create a helper for errors that will notify users by sending error messages to the console when something goes wrong. To do so, create the service-helper.ts
file inside the app/services
folder with the following code:
export function HandleError(error: any): Promise<any>{
console.log(error);
return Promise.reject(error);
}
Now let’s create the reservations and collections services. Run the following commands:
ng generate service services/reservation --flat --skip-tests
ng generate service services/collections --flat --skip-tests
In the newly created reservation.service.ts
file inside the services
folder, we will add the following code:
import { Injectable } from '@angular/core';
import { Reservation } from "../models/reservation";
import { HttpClient } from "@angular/common/http";
import { HandleError } from "./service-helper";
import { firstValueFrom } from 'rxjs';
import { environment } from '../../environments/environment.development';
@Injectable()
export class ReservationService {
private reservationUrl = `${environment.apiBaseUrl}/reservations`;
constructor(private http: HttpClient) { }
get(): Promise<Reservation[]>{
return firstValueFrom(this.http.get(this.reservationUrl))
.catch(HandleError);
}
insert(reservation: Reservation): Promise<Reservation> {
return firstValueFrom(this.http.post(this.reservationUrl, reservation))
.catch(HandleError);
}
update(reservation: Reservation): Promise<void> {
return firstValueFrom(this.http.put(`${this.reservationUrl}/${reservation.id}`, reservation))
.catch(HandleError);
}
remove(id: number): Promise<void> {
return firstValueFrom(this.http.delete(`${this.reservationUrl}/${id}`))
.catch(HandleError);
}
}
In the newly created collections.service.ts
file, add the next lines of code:
import { Injectable } from '@angular/core';
import { Room } from "../models/room.model";
import { RoomType } from "../models/room-type.model";
import { CleaningStatus } from "../models/cleaning-status.model";
import { BookingStatus } from "../models/booking-status.model";
import { HttpClient } from "@angular/common/http";
import { HandleError } from "./service-helper";
import { firstValueFrom } from 'rxjs';
import { environment } from '../../environments/environment.development';
@Injectable()
export class CollectionsService {
private collectionsUrl = `${environment.apiBaseUrl}/collections`;
constructor(private http: HttpClient) { }
getRooms(): Promise<Room[]>{
return firstValueFrom(this.http.get(`${this.collectionsUrl}/rooms`))
.catch(HandleError);
}
updateRoom(room: Room): Promise<void> {
return firstValueFrom(this.http.put(`${this.collectionsUrl}/rooms/${room.id}`, room))
.catch(HandleError);
}
getRoomTypes(): Promise<RoomType[]>{
return firstValueFrom(this.http.get(`${this.collectionsUrl}/roomTypes`))
.catch(HandleError);
}
getCleaningStatuses(): Promise<CleaningStatus[]>{
return firstValueFrom(this.http.get(`${this.collectionsUrl}/cleaningStatuses`))
.catch(HandleError);
}
getBookingStatuses(): Promise<BookingStatus[]>{
return firstValueFrom(this.http.get(`${this.collectionsUrl}/bookingStatuses`))
.catch(HandleError);
}
}
The get()
, getRooms()
, getRoomTypes()
, getCleaningStatuses()
and getBookingStatuses()
methods retrieve data from the server.
The reservationUrl
and collectionsUrl
are private elements of the services. They contain the URL to the REST API. In order to send HTTP requests, an HTTP class has been injected into the service.
To insert a new item, you need to send a POST request to the URL with the new item in its body.
To update an item, you need to send a PUT request to the url/item_id
. This request also contains the updated item in its body. To remove an item, you need to send a delete request to the url/item_id.
CRUD Operations
The services should handle CRUD operations in the scheduler. HTTP communication has been enabled by adding the HttpClient
module in the reservations.service.ts
and collections.service.ts
files:
import { HttpClient } from "@angular/common/http";
This step allows fetching data seamlessly within our Angular application.
To utilize the HttpClient
module, it is also required to include the essential HttpClientModule from the @angular/common/http
package. In the app.module.ts
file, you should update the imports array as follows:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { SchedulerComponent } from './scheduler/scheduler.component';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
@NgModule({
declarations: [
AppComponent,
SchedulerComponent
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
HttpClientModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
The scheduler component is supposed to use the ReservationService
and CollectionsService
to get/insert/update/remove reservations and collections. To enable these options, let’s add the ReservationService
and CollectionsService
to the component. First, import the necessary modules for the service in scheduler.component.ts
:
import { ReservationService } from '../services/reservation.service';
import { CollectionsService } from '../services/collections.service';
You should also specify the EventService
as a provider in the @Component
decorator:
providers: [ ReservationService, CollectionsService ]
Now every time a new SchedulerComponent
is initialized, a fresh instance of the service will be created.
The services should be prepared to be injected into the component. For this purpose, add the following constructor to the SchedulerComponent
class:
constructor(
private reservationService: ReservationService,
private collectionsService: CollectionsService
) { }
Next, we’ll add the updateRoom()
method to save the room cleaning status changes in the database:
handleCleaningStatusChange(target: HTMLSelectElement) {
...
this.collectionsService.updateRoom(roomToUpdate);
}
You need to modify the ngOnInit
function to call the services to get the function and then wait for a response to put the data to the scheduler.
scheduler.init(this.schedulerContainer.nativeElement, new Date(), 'timeline');
const dp = scheduler.createDataProcessor({
event: {
create: (data: Reservation) => this.reservationService.insert(data),
update: (data: Reservation) => this.reservationService.update(data),
delete: (id: number) => this.reservationService.remove(id),
}
});
forkJoin({
reservations: this.reservationService.get(),
rooms: this.collectionsService.getRooms(),
roomTypes: this.collectionsService.getRoomTypes(),
cleaningStatuses: this.collectionsService.getCleaningStatuses(),
bookingStatuses: this.collectionsService.getBookingStatuses()
}).subscribe({
next: ({ reservations, rooms, roomTypes, cleaningStatuses, bookingStatuses }) => {
const data = {
events: reservations,
collections: {
rooms,
roomTypes,
cleaningStatuses,
bookingStatuses,
}
};
scheduler.parse(data);
},
error: error => {
console.error('An error occurred:', error);
}
});
The scheduler.parse
accepts a data object in the JSON format. To efficiently wait for the completion of multiple asynchronous requests and load their data (reservations and collections) into the scheduler, you can utilize the forkJoin
operator from the RxJS library. Please include the import:
import { forkJoin } from 'rxjs';
You can check the complete code for the scheduler.components.ts
file on GitHub.
Step 5 – Server Configuration
Now, let’s move on to setting up the Node.js server for our app.
This tutorial uses the Express framework and MySQL as a data storage.
Adding dependencies and installing modules
You should set up your MySQL server, or you can use another service, e.g. Free MySQL Hosting.
Add express, mysql, and date-format-lite modules:
$ npm install express mysql date-format-lite
The server.js
has been specified as the entry point above. Now let’s create the server folder in the root of the project and add the server.js
file with the code below:
const express = require('express'); // use Express
const app = express(); // create application
const port = 3000; // port for listening
const cors = require('cors');
app.use(cors()); // enable CORS for all routes
// MySQL will be used for db access and util to promisify queries
const util = require('util');
const mysql = require('mysql');
// use your own parameters for database
const mysqlConfig = {
'connectionLimit': 10,
'host': 'localhost',
'user': 'root',
'password': '',
'database': 'room_reservation_node'
};
app.use(express.json()); // Enable JSON body parsing
// return static pages from the './public' directory
app.use(express.static(__dirname + '/public'));
// start server
app.listen(port, () => {
console.log('Server is running on port ' + port + '...');
});
const router = require('./router');
// open connection to mysql
const connectionPool = mysql.createPool(mysqlConfig);
connectionPool.query = util.promisify(connectionPool.query);
// add listeners to basic CRUD requests
const DatabaseHandler = require('./databaseHandler');
const databaseHandler = new DatabaseHandler(connectionPool);
router.setRoutes(app, '/data', databaseHandler);
Then open the package.json
file and replace the start
statement with:
"scripts": {
"ng": "ng",
"start": "concurrently \"node server/server.js\" \"ng serve\"",
…
We’ll utilize the concurrently
package to enable simultaneous launching of both the server and the client application. So, add the concurrently
module:
$ npm install concurrently
Preparing a database
Let’s connect Scheduler to the database and define methods to read and write items in it.
- Creating a database:
First things first, we need a database to work with. You can create a database with your favorite mysql-client or via the console. To create a database with a mysql-client, open it and execute the code below. For creating reservations
tables:
CREATE TABLE `reservations` (
`id` bigint(20) unsigned AUTO_INCREMENT,
`start_date` datetime NOT NULL,
`end_date` datetime NOT NULL,
`text` varchar(255) DEFAULT NULL,
`room` varchar(255) DEFAULT NULL,
`booking_status` varchar(255) DEFAULT NULL,
`is_paid` BOOLEAN DEFAULT NULL CHECK (is_paid IN (0, 1)),
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;
Let’s add some test data:
INSERT INTO `reservations` VALUES (2, '2023-08-01', '2023-08-11', 'RSV2023-08-01ABC124', 3, 4, true);
INSERT INTO `reservations` VALUES (3, '2023-08-07', '2023-08-17', 'RSV2023-08-07ABC126', 5, 3, true);
INSERT INTO `reservations` VALUES (4, '2023-08-04', '2023-08-16', 'RSV2023-08-04ABC125', 7, 4, false);
INSERT INTO `reservations` VALUES (13, '2023-07-28', '2023-08-14', 'RSV2023-07-28ABC123', 1, 4, true);
INSERT INTO `reservations` VALUES (14, '2023-08-14', '2023-08-27', 'RSV2023-08-14ABC129', 1, 3, false);
INSERT INTO `reservations` VALUES (15, '2023-08-19', '2023-08-29', 'new booking', 4, 1, false);
INSERT INTO `reservations` VALUES (16, '2023-08-24', '2023-08-31', 'new booking', 11, 1, false);
INSERT INTO `reservations` VALUES (17, '2023-08-17', '2023-08-26', 'RSV2023-08-17ABC135', 6, 2, false);
INSERT INTO `reservations` VALUES (18, '2023-08-18', '2023-08-31', 'RSV2023-08-18ABC139', 9, 2, false);
INSERT INTO `reservations` VALUES (19, '2023-08-02', '2023-08-12', 'RSV2023-08-02ABC127', 10, 4, true);
INSERT INTO `reservations` VALUES (20, '2023-08-12', '2023-08-21', 'RSV2023-08-12ABC130', 10, 3, false);
For creating rooms
tables:
CREATE TABLE `rooms` (
`id` bigint(20) unsigned AUTO_INCREMENT,
`value` varchar(255) DEFAULT NULL,
`label` varchar(255) DEFAULT NULL,
`type` varchar(255) DEFAULT NULL,
`cleaning_status` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;
Let’s add some test data:
INSERT INTO `rooms` VALUES ('1', '1', '101', '1', '1');
INSERT INTO `rooms` VALUES ('2', '2', '102', '1', '3');
INSERT INTO `rooms` VALUES ('3', '3', '103', '1', '2');
INSERT INTO `rooms` VALUES ('4', '4', '104', '1', '1');
INSERT INTO `rooms` VALUES ('5', '5', '105', '2', '1');
INSERT INTO `rooms` VALUES ('6', '6', '201', '2', '2');
INSERT INTO `rooms` VALUES ('7', '7', '202', '2', '1');
INSERT INTO `rooms` VALUES ('8', '8', '203', '3', '3');
INSERT INTO `rooms` VALUES ('9', '9', '204', '3', '3');
INSERT INTO `rooms` VALUES ('10', '10', '301', '4', '2');
INSERT INTO `rooms` VALUES ('11', '11', '302', '4', '2');
INSERT INTO `rooms` VALUES ('12', '12', '303', '1', '2');
For creating the roomTypes
tables:
CREATE TABLE `roomTypes` (
`id` bigint(20) unsigned AUTO_INCREMENT,
`value` varchar(255) DEFAULT NULL,
`label` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;
Let’s add some test data:
INSERT INTO `roomTypes` VALUES ('1', '1', '1 bed');
INSERT INTO `roomTypes` VALUES ('2', '2', '2 bed');
INSERT INTO `roomTypes` VALUES ('3', '3', '3 bed');
INSERT INTO `roomTypes` VALUES ('4', '4', '4 bed');
For creating cleaningStatuses
tables:
CREATE TABLE `cleaningStatuses` (
`id` bigint(20) unsigned AUTO_INCREMENT,
`value` varchar(255) DEFAULT NULL,
`label` varchar(255) DEFAULT NULL,
`color` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;
Let’s add some test data:
INSERT INTO `cleaningStatuses` VALUES ('1', '1', 'Ready', '#43a047');
INSERT INTO `cleaningStatuses` VALUES ('2', '2', 'Dirty', '#e53935');
INSERT INTO `cleaningStatuses` VALUES ('3', '3', 'Clean up', '#ffb300');
For creating bookingStatuses
tables:
CREATE TABLE `bookingStatuses` (
`id` bigint(20) unsigned AUTO_INCREMENT,
`value` varchar(255) DEFAULT NULL,
`label` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;
Let’s add some test data:
INSERT INTO `bookingStatuses` VALUES ('1', '1', 'New');
INSERT INTO `bookingStatuses` VALUES ('2', '2', 'Confirmed');
INSERT INTO `bookingStatuses` VALUES ('3', '3', 'Arrived');
INSERT INTO `bookingStatuses` VALUES ('4', '4', 'Checked Out');
- Implementing data access:
All the read/write logic will be defined in a separate module called DatabaseHandler
. It’ll take a mysql connection and perform simple CRUD operations in the specified table: read all the items, insert new items, update or delete the existing ones. For this, create the databaseHandler.js
file and add the code below into it:
require('date-format-lite'); // add date format
class DatabaseHandler {
constructor(connection, table) {
this._db = connection;
this.table = 'reservations';
}
/// ↓↓↓ reservations handler ↓↓↓
// get reservations, use dynamic loading if parameters sent
async getAllReservations(params) {
let query = 'SELECT * FROM ??';
let queryParams = [
this.table
];
let result = await this._db.query(query, queryParams);
result.forEach((entry) => {
// format date and time
entry.start_date = entry.start_date.format('YYYY-MM-DD hh:mm');
entry.end_date = entry.end_date.format('YYYY-MM-DD hh:mm');
});
return result;
}
// create new reservation
async insert(data) {
let result = await this._db.query(
'INSERT INTO ?? (`start_date`, `end_date`, `text`, `room`, `booking_status`, `is_paid`) VALUES (?,?,?,?,?,?)',
[this.table, data.start_date, data.end_date, data.text, data.room, data.booking_status, data.is_paid]);
return {
action: 'inserted',
tid: result.insertId
}
}
// update reservation
async update(id, data) {
await this._db.query(
'UPDATE ?? SET `start_date` = ?, `end_date` = ?, `text` = ?, `room` = ?, `booking_status` = ?, `is_paid` = ? WHERE id = ?',
[this.table, data.start_date, data.end_date, data.text, data.room, data.booking_status, data.is_paid, id]);
return {
action: 'updated'
}
}
// delete reservation
async delete(id) {
await this._db.query(
'DELETE FROM ?? WHERE `id`=? ;',
[this.table, id]);
return {
action: 'deleted'
}
}
/// ↑↑↑ reservations handler ↑↑↑
/// ↓↓↓ room cleanup status handler ↓↓↓
// get rooms
async getAllRooms(params) {
let query = 'SELECT * FROM ??';
let queryParams = [
'rooms'
];
let result = await this._db.query(query, queryParams);
return result;
}
// update room cleanup status
async updateRoomCleaningStatus(id, data) {
await this._db.query(
'UPDATE ?? SET `value` = ?, `label` = ?, `type` = ?, `cleaning_status` = ? WHERE id = ?',
['rooms', data.key, data.label, data.type, data.cleaning_status, id]);
return {
action: 'updated'
}
}
/// ↑↑↑ room cleanup status handler ↑↑↑
/// ↓↓↓ get room types ↓↓↓
async getRoomTypes(params) {
let query = 'SELECT * FROM ??';
let queryParams = [
'roomTypes'
];
let result = await this._db.query(query, queryParams);
return result;
}
/// ↑↑↑ get room types ↑↑↑
/// ↓↓↓ get cleaning statuses ↓↓↓
async getCleaningStatuses(params) {
let query = 'SELECT * FROM ??';
let queryParams = [
'cleaningStatuses'
];
let result = await this._db.query(query, queryParams);
return result;
}
/// ↑↑↑ get cleaning statuses ↑↑↑
/// ↓↓↓ get booking statuses ↓↓↓
async getBookingStatuses(params) {
let query = 'SELECT * FROM ??';
let queryParams = [
'bookingStatuses'
];
let result = await this._db.query(query, queryParams);
return result;
}
/// ↑↑↑ get booking statuses ↑↑↑
}
module.exports = DatabaseHandler;
Routing
Then you need to set up routes, so that the storage could be accessed by the scheduler you have placed on the page.
For this, create another helper module and call it router.js
:
function callMethod (method) {
return async (req, res) => {
let result;
try {
result = await method(req, res);
} catch (e) {
result = {
action: 'error',
message: e.message
}
}
res.send(result);
}
};
module.exports = {
setRoutes (app, prefix, databaseHandler) {
/// ↓↓↓ reservations router ↓↓↓
app.get(`${prefix}/reservations`, callMethod((req) => {
return databaseHandler.getAllReservations(req.query);
}));
app.post(`${prefix}/reservations`, callMethod((req) => {
return databaseHandler.insert(req.body);
}));
app.put(`${prefix}/reservations/:id`, callMethod((req) => {
return databaseHandler.update(req.params.id, req.body);
}));
app.delete(`${prefix}/reservations/:id`, callMethod((req) => {
return databaseHandler.delete(req.params.id);
}));
/// ↑↑↑ reservations router ↑↑↑
/// ↓↓↓ rooms router ↓↓↓
app.get(`${prefix}/collections/rooms`, callMethod((req) => {
return databaseHandler.getAllRooms(req.query);
}));
app.put(`${prefix}/collections/rooms/:id`, callMethod((req) => {
return databaseHandler.updateRoomCleaningStatus(req.params.id, req.body);
}));
/// ↑↑↑ rooms router ↑↑↑
/// ↓↓↓ room types router ↓↓↓
app.get(`${prefix}/collections/roomTypes`, callMethod((req) => {
return databaseHandler.getRoomTypes(req.query);
}));
/// ↑↑↑ room types router ↑↑↑
/// ↓↓↓ cleaning statuses router ↓↓↓
app.get(`${prefix}/collections/cleaningStatuses`, callMethod((req) => {
return databaseHandler.getCleaningStatuses(req.query);
}));
/// ↑↑↑ cleaning statuses router ↑↑↑
/// ↓↓↓ booking statuses router ↓↓↓
app.get(`${prefix}/collections/bookingStatuses`, callMethod((req) => {
return databaseHandler.getBookingStatuses(req.query);
}));
/// ↑↑↑ booking statuses router ↑↑↑
}
};
All it does is sets up the application to listen to request URLs that scheduler can send and calls the appropriate methods of the storage. Note that all methods are wrapped into try-catch blocks for you to be able to capture any error and return an appropriate error response to the client. Learn more about error handling.
Also note that the exception message is written directly to the API response. It’s pretty handy during the development, but in the production environment it can be a good idea to hide these messages from the client side, since raw mysql exceptions that get there may contain sensitive data.
Now if you open the app page, you should see a scheduler with reservations. You can create, delete, and modify items within the scheduler. Any changes you make will be retained even if you reload the page.
Conclusions
The Hotel Booking Calendar with Angular is ready! You are welcome to check out the full source code on GitHub. The app can use RESTful API for CRUD operations.
We hope this guide has given you a clear understanding of how to integrate the room booking app based on DHTMLX Scheduler with Angular and connect it to a real backend. We’ll be glad if it helps you achieve your application development goals.
If you have any thoughts and ideas about the next topics/tutorials you would like to discover, don’t hesitate to share them in the comments.
The article is originally published on the DHTMLX blog.