Development of OTRS features

As you know, we are developing the SPA. Therefore, once the application loads, you can perform all the operations without page refresh. All interactions with the server are performed using AJAX calls. Now, we'll make use of the AngularJS concepts that we have covered in the first section. We'll cover the following scenarios:

For the home page, we will create index.html and a template that will contain the restaurant listing in the middle section or the content area.

The home page is the main page of any web application. To design the home page, we are going to use the Angular-UI bootstrap rather than the actual bootstrap. Angular-UI is an Angular version of the bootstrap. The home page will be divided into three sections:

You must be interested in viewing the home page before designing or implementing it. Therefore, let us first see how it will look like once we have our content ready:

Now, to design our home page, we need to add following four files:

First, we'll add the ./app/index.html in our project workspace. The contents of index.html will be as explained here onwards.

index.html is divided into many parts. We'll discuss a few of the key parts here. First, we will see how to address old Internet Explorer versions. If you want to target the Internet Explorer browser versions greater than 8 or IE version 9 onwards, then we need to add following block that will prevent JavaScript rendering and give the no-js output to the end-user.

Then, after adding a few meta tags and the title of the application, we'll also define the important meta tag viewport. The viewport is used for responsive UI designs.

The width property defined in the content attribute controls the size of the viewport. It can be set to a specific number of pixels like width = 600 or to the special value device-width value which is the width of the screen in CSS pixels at a scale of 100%.

The initial-scale property controls the zoom level when the page is first loaded. The maximum-scale, minimum-scale, and user-scalable properties control how users are allowed to zoom the page in or out.

In the next few lines, we'll define the style sheets of our application. We are adding normalize.css and main.css from HTML5 boilerplate code. We are also adding our application's customer CSS app.css. Finally, we are adding the bootstrap 3 CSS. Apart from the customer app.css, other CSS are referenced in it. There is no change in these CSS files.

Then we'll define the scripts using the script tag. We are adding the modernizer, Angular, Angular-route, and our own developed custom JavaScript file app.js. We have already discussed Angular and Angular-UI. app.js will be discussed in the next section.

Modernizer allows web developers to use new CSS3 and HTML5 features while maintaining a fine level of control over browsers that don't support them. Basically, modernizer performs the next generation feature detection (checking the availability of those features) while the page loads in the browser and reports the results. Based on these results you can detect what are the latest features available in the browser and based on that you can provide an interface to the end user. If the browser does not support a few of the features then an alternate flow or UI is provided to the end user.

We are also adding the bootstrap templates which are written in JavaScript using the ui-bootstrap-tpls javascript file.

We can also add style to the head tag as shown in the following. This style allows drop-down menus to work.

In the body tag we are defining the controller of the application using the ng-controller attribute. While the page loads, it tells the controller the name of the application to Angular.

Then, we define the header section of the home page. In the header section, we'll define the application title, Online Table Reservation System. Also, we'll define the search form that will search the restaurants.

Then, in the next section, the middle section, includes where we actually bind the different views, marked with actual content comments. The ui-view attribute in div gets its content dynamically from Angular such as restaurant details, restaurant list, and so on. We have also added a warning dialog and spinner to the middle section that will be visible as and when required.

The final section of the index.html is the footer. Here, we are just adding the static content and copyright text. You can add whatever content you want here.

app.js is our main application file. Because we have defined it in index.html, it gets loaded as soon as our index.html is called.

As we are using the Edge Server (Proxy Server), everything will be accessible from it including our REST endpoints. External applications including the UI will use the Edge Server host to access the application. You can configure it in some global constants file and then use it wherever it is required. This will allow you to configure the REST host at a single place and use it at other places.

'use strict';
/*
This call initializes our application and registers all the modules, which are passed as an array in the second argument.
*/
var otrsApp = angular.module('otrsApp', [
    'ui.router',
    'templates',
    'ui.bootstrap',
    'ngStorage',
    'otrsApp.httperror',
    'otrsApp.login',
    'otrsApp.restaurants'
])
/*
  Then we have defined the default route /restaurants
*/
        .config([
            '$stateProvider', '$urlRouterProvider',
            function ($stateProvider, $urlRouterProvider) {
                $urlRouterProvider.otherwise('/restaurants');
            }])
/*
	This functions controls the flow of the application and handles the events.
*/
        .controller('otrsAppCtrl', function ($scope, $injector, restaurantService) {
            var controller = this;

            var AjaxHandler = $injector.get('AjaxHandler');
            var $rootScope = $injector.get('$rootScope');
            var log = $injector.get('$log');
            var sessionStorage = $injector.get('$sessionStorage');
            $scope.showSpinner = false;
/*
	This function gets called when the user searches any restaurant. It uses the Angular restaurant service that we'll define in the next section to search the given search string.
*/
            $scope.search = function () {
                $scope.restaurantService = restaurantService;
                restaurantService.async().then(function () {
                    $scope.restaurants = restaurantService.search($scope.searchedValue);
                });
            }
/*
	When the state is changed, the new controller controls the flows based on the view and configuration and the existing controller is destroyed. This function gets a call on the destroy event.
*/
            $scope.$on('$destroy', function destroyed() {
                log.debug('otrsAppCtrl destroyed');
                controller = null;
                $scope = null;
            });

            $rootScope.fromState;
            $rootScope.fromStateParams;
            $rootScope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState, fromStateParams) {
                $rootScope.fromState = fromState;
                $rootScope.fromStateParams = fromStateParams;
            });

            // utility method
            $scope.isLoggedIn = function () {
                if (sessionStorage.session) {
                    return true;
                } else {
                    return false;
                }
            };

            /* spinner status */
            $scope.isSpinnerShown = function () {
                return AjaxHandler.getSpinnerStatus();
            };

        })
/*
	This function gets executed when this object loads. Here we are setting the user object which is defined for the root scope.
*/
        .run(['$rootScope', '$injector', '$state', function ($rootScope, $injector, $state) {
                $rootScope.restaurants = null;
                // self reference
                var controller = this;
                // inject external references
                var log = $injector.get('$log');
                var $sessionStorage = $injector.get('$sessionStorage');
                var AjaxHandler = $injector.get('AjaxHandler');

                if (sessionStorage.currentUser) {
                    $rootScope.currentUser = $sessionStorage.currentUser;
                } else {
                    $rootScope.currentUser = "Guest";
                    $sessionStorage.currentUser = ""
                }
            }])

restaurants.js represents an Angular service for our app which we'll use for the restaurants. We know that there are two common uses of services – organizing code and sharing code across apps. Therefore, we have created a restaurants service which will be used among different modules like search, list, details, and so on.

The following section initializes the restaurant service module and loads the required dependencies.

In the configuration, we are defining the routes and state of the otrsApp.restaurants module using UI-Router.

First we define the restaurants state by passing the JSON object containing the URL that points the router URI, the template URL that points to the HTML template that display the restaurants state, and the controller that will handle the events on the restaurants view.

On top of the restaurants view (route - /restaurants), a nested state restaurants.profile is also defined that will represent the specific restaurant. For example, /restaurant/1 would open and display the restaurant profile (details) page of a restaurant which is represented by Id 1. This state is called when a link is clicked in the restaurants template. In this ui-sref="restaurants.profile({id: rest.id})" rest represents the restaurant object retrieved from the restaurants view.

Notice that the state name is 'restaurants.profile' which tells the AngularJS UI Router that the profile is a nested state of the restaurants state.

In the next code section, we are defining the restaurant service using the Angular factory service type. This restaurant service on load fetches the list of restaurants from the server using a REST call. It provides a list and searches restaurant operations and restaurant data.

        .factory('restaurantService', function ($injector, $q) {
            var log = $injector.get('$log');
            var ajaxHandler = $injector.get('AjaxHandler');
            var deffered = $q.defer();
            var restaurantService = {};
            restaurantService.restaurants = [];
            restaurantService.orignalRestaurants = [];
            restaurantService.async = function () {
                ajaxHandler.startSpinner();
                if (restaurantService.restaurants.length === 0) {
                    ajaxHandler.get('/api/restaurant')
                            .success(function (data, status, headers, config) {
                                log.debug('Getting restaurants');
                                sessionStorage.apiActive = true;
                                log.debug("if Restaurants --> " + restaurantService.restaurants.length);
                                restaurantService.restaurants = data;
                                ajaxHandler.stopSpinner();
                                deffered.resolve();
                            })
                            .error(function (error, status, headers, config) {
                                restaurantService.restaurants = mockdata;
                                ajaxHandler.stopSpinner();
                                deffered.resolve();
                            });
                    return deffered.promise;
                } else {
                    deffered.resolve();
                    ajaxHandler.stopSpinner();
                    return deffered.promise;
                }
            };
            restaurantService.list = function () {
                return restaurantService.restaurants;
            };
            restaurantService.add = function () {
                console.log("called add");
                restaurantService.restaurants.push(
                        {
                            id: 103,
                            name: 'Chi Cha\'s Noodles',
                            address: '13 W. St., Eastern Park, New County, Paris',
                        });
            };
            restaurantService.search = function (searchedValue) {
                ajaxHandler.startSpinner();
                if (!searchedValue) {
                    if (restaurantService.orignalRestaurants.length > 0) {
                        restaurantService.restaurants = restaurantService.orignalRestaurants;
                    }
                    deffered.resolve();
                    ajaxHandler.stopSpinner();
                    return deffered.promise;
                } else {
                    ajaxHandler.get('/api/restaurant?name=' + searchedValue)
                            .success(function (data, status, headers, config) {
                                log.debug('Getting restaurants');
                                sessionStorage.apiActive = true;
                                log.debug("if Restaurants --> " + restaurantService.restaurants.length);
                                if (restaurantService.orignalRestaurants.length < 1) {
                                    restaurantService.orignalRestaurants = restaurantService.restaurants;
                                }
                                restaurantService.restaurants = data;
                                ajaxHandler.stopSpinner();
                                deffered.resolve();
                            })
                            .error(function (error, status, headers, config) {
                                if (restaurantService.orignalRestaurants.length < 1) {
                                    restaurantService.orignalRestaurants = restaurantService.restaurants;
                                }
                                restaurantService.restaurants = [];
                                restaurantService.restaurants.push(
                                        {
                                            id: 104,
                                            name: 'Gibsons - Chicago Rush St.',
                                            address: '1028 N. Rush St., Rush & Division, Cook County, Paris'
                                        });
                                restaurantService.restaurants.push(
                                        {
                                            id: 105,
                                            name: 'Harry Caray\'s Italian Steakhouse',
                                            address: '33 W. Kinzie St., River North, Cook County, Paris',
                                        });
                                ajaxHandler.stopSpinner();
                                deffered.resolve();
                            });
                    return deffered.promise;
                }
            };
            return restaurantService;
        })

In the next section of the restaurants.js module, we'll add two controllers that we defined for the restaurants and restaurants.profile states in the routing configuration. These two controllers are RestaurantsCtrl and RestaurantCtrl that handle the restaurants state and the restaurants.profiles states respectively.

RestaurantsCtrl is pretty simple in that it loads the restaurants data using the restaurants service list method.

RestaurantCtrl is responsible for showing the restaurant details of a given ID. This is also responsible for performing the reservation operations on the displayed restaurant. This control will be used when we design the restaurant details page with reservation options.

        .controller('RestaurantCtrl', function ($scope, $state, $stateParams, $injector, restaurantService) {
            var $sessionStorage = $injector.get('$sessionStorage');
            $scope.format = 'dd MMMM yyyy';
            $scope.today = $scope.dt = new Date();
            $scope.dateOptions = {
                formatYear: 'yy',
                maxDate: new Date().setDate($scope.today.getDate() + 180),
                minDate: $scope.today.getDate(),
                startingDay: 1
            };

            $scope.popup1 = {
                opened: false
            };
            $scope.altInputFormats = ['M!/d!/yyyy'];
            $scope.open1 = function () {
                $scope.popup1.opened = true;
            };
            $scope.hstep = 1;
            $scope.mstep = 30;

            if ($sessionStorage.reservationData) {
                $scope.restaurant = $sessionStorage.reservationData.restaurant;
                $scope.dt = new Date($sessionStorage.reservationData.tm);
                $scope.tm = $scope.dt;
            } else {
                $scope.dt.setDate($scope.today.getDate() + 1);
                $scope.tm = $scope.dt;
                $scope.tm.setHours(19);
                $scope.tm.setMinutes(30);
                restaurantService.async().then(function () {
                    angular.forEach(restaurantService.list(), function (value, key) {
                        if (value.id === parseInt($stateParams.id)) {
                            $scope.restaurant = value;
                        }
                    });
                });
            }
            $scope.book = function () {
                var tempHour = $scope.tm.getHours();
                var tempMinute = $scope.tm.getMinutes();
                $scope.tm = $scope.dt;
                $scope.tm.setHours(tempHour);
                $scope.tm.setMinutes(tempMinute);
                if ($sessionStorage.currentUser) {
                    console.log("$scope.tm --> " + $scope.tm);
                    alert("Booking Confirmed!!!");
                    $sessionStorage.reservationData = null;
                    $state.go("restaurants");
                } else {
                    $sessionStorage.reservationData = {};
                    $sessionStorage.reservationData.restaurant = $scope.restaurant;
                    $sessionStorage.reservationData.tm = $scope.tm;
                    $state.go("login");
                }
            }
        })

We have also added a few of the filters in the restaurants.js module to format the date and time. These filters perform the following formatting on the input data:

In the following code snippet we've applied these three filters:

On the home page index.html we have added the search form in the header section that allows us to search restaurants. The Search Restaurants functionality will use the same files as described earlier. It makes use of the app.js (search form handler), restaurants.js (restaurant service), and restaurants.html to display the searched records.

Restaurant details with reservation option will be the part of the content area (middle section of the page). This will contain a breadcrumb at the top with restaurants as a link to the restaurant listing page, followed by the name and address of the restaurant. The last section will contain the reservation section containing date time selection boxes and reserve button.

This page will look like the following screenshot:

Here, we will make use of the same restaurant service declared in restaurants.js. The only change will be the template as described for the state restaurants.profile. This template will be defined using the restaurant.html.

As you can see, the breadcrumb is using the restaurants route, which is defined using the ui-sref attribute. The reservation form designed in this template calls the book() function defined in the controller RestaurantCtrl using the directive ng-submit on the form submit.

When a user clicks on the Reserve button on the Restaurant Detail page after selecting the date and time of the reservation, the Restaurant Detail page checks whether the user is already logged in or not. If the user is not logged in, then the Login page displays. It looks like the following screenshot:

Once the user logs in, the user is redirected back to same booking page with the persisted state. Then the user can proceed with the reservation. The Login page uses basically two files: login.html and login.js.

Once the user is logged in and has clicked on the Reservation button, the restaurant controller shows the alert box with confirmation as shown in the following screenshot.