DEV Community

Cover image for How to Make Hotel Booking Calendar Using DHTMLX Scheduler and Angular
Pavel Lazarev
Pavel Lazarev

Posted on

How to Make Hotel Booking Calendar Using DHTMLX Scheduler and Angular

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:

angular-scheduler

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
Enter fullscreen mode Exit fullscreen mode

After the operation finishes, we can go to the app directory and run the application:

cd room-reservation-angular
ng serve
Enter fullscreen mode Exit fullscreen mode

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.

Angular-Booking-App-Step-1

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Create a new component. For this, run the following command:

ng generate component scheduler --skip-tests
Enter fullscreen mode Exit fullscreen mode

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'>&nbsp;</div>
        <div class='dhx_cal_next_button'>&nbsp;</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>
Enter fullscreen mode Exit fullscreen mode

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 { }
Enter fullscreen mode Exit fullscreen mode

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%;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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})`;
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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 { }
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

You should also specify the EventService as a provider in the @Component decorator:

providers: [ ReservationService, CollectionsService ]
Enter fullscreen mode Exit fullscreen mode

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
) { }
Enter fullscreen mode Exit fullscreen mode

Next, we’ll add the updateRoom() method to save the room cleaning status changes in the database:

handleCleaningStatusChange(target: HTMLSelectElement) {
    ...
    this.collectionsService.updateRoom(roomToUpdate);
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
});
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Then open the package.json file and replace the start statement with:

"scripts": {
    "ng": "ng",
    "start": "concurrently \"node server/server.js\" \"ng serve\"",
    …
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode

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 ↑↑↑
    }
};
Enter fullscreen mode Exit fullscreen mode

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.

angular-js-scheduler

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.

Top comments (0)