Skip to content

Hotel Management Module - Frontend Implementation Specification

Overview

This document provides detailed technical documentation for the Hotel Management frontend implementation, including the HTML page structure, JavaScript module, state management, and UI interactions.

Files: - tqweb-adm/hotelmgmt.html (1452 lines) - HTML structure and dialogs - tqweb-adm/js/modules/hotelmgmt.js (1428 lines) - JavaScript ES6 module

Technology Stack

Core Libraries

Library Version Purpose
jQuery 3.7.1 DOM manipulation, AJAX calls
Foundation 6.8.1 CSS framework, UI components
Foundation Datepicker Custom Date input widgets
Notify.js Custom Toast notifications

JavaScript Features Used

  • ES6 Modules: import/export syntax
  • Arrow Functions: Concise callback definitions
  • Template Literals: String interpolation
  • Map Objects: Efficient key-value lookups
  • Destructuring: Object and array destructuring
  • Promises: Asynchronous API calls via glb.tlinq()

Page Architecture

Layout Structure

┌─────────────────────────────────────────────────────────┐
│                      HEADER                              │
│  (Loaded from header.html template)                     │
└─────────────────────────────────────────────────────────┘
┌──────────────┬──────────────────────────────────────────┐
│              │                                          │
│   LEFT       │          MAIN CONTENT AREA              │
│   SIDEBAR    │                                          │
│              │  ┌───────────────────────────────────┐  │
│  Hotel       │  │  Hotel Search Panel               │  │
│  Search      │  │  (Initially visible)              │  │
│  Accordion   │  └───────────────────────────────────┘  │
│              │                                          │
│  [Area 1]    │  ┌───────────────────────────────────┐  │
│    Hotel A   │  │  Hotel Data Tabs                  │  │
│    Hotel B   │  │  (Shown after hotel selection)    │  │
│              │  │                                    │  │
│  [Area 2]    │  │  [Hotel Info] [Rooms] [Promos]   │  │
│    Hotel C   │  │                                    │  │
│              │  │  Content panels with forms...     │  │
│              │  └───────────────────────────────────┘  │
│              │                                          │
│              │  ┌───────────────────────────────────┐  │
│              │  │  Search Results                   │  │
│              │  │  (Shown after accommodation       │  │
│              │  │   search)                         │  │
│              │  └───────────────────────────────────┘  │
└──────────────┴──────────────────────────────────────────┘

The page uses Foundation Reveal modals for: 1. confirm_dialog - Confirmation prompts 2. hotelmeals-dialog - Meal plan configuration 3. breakdown-dialog - Price breakdown display 4. dialog_small - Room info tooltip 5. newroom-dialog - Add new room form 6. quickcal_dialog - Quick rate periods edit 7. newcal-dialog - Create calendar entries (large form) 8. editcal-dialog - Edit calendar range 9. copycal-dialog - Copy calendar between rooms 10. bookroom-dialog - Create booking request


Module State Management

Module-Level State (hotelmgmt.js:7-20)

// Entity Maps
let roomMap = new Map();              // roomId → CHotelRoom object
let mealSuppMap = new Map();          // mealPlan → {adlRate:[], chldRate:[]}
let hotelMealPlans = new Map();       // mealPlan → CMealPlan object
let allMealPlans = new Map();         // mealPlan → CMealPlan (all system plans)
let searchResultMap = new Map();      // "hotelId:roomId" → search result

// Current Selections
let selectedHotel;                    // Currently selected CHotel
let selectedRoom;                     // Currently selected CHotelRoom
let bookedRoom;                       // Room selected for booking

// Calendar Data
let roomCalendar = [];                // Array of CRoomCalendarEntry
let calStayFromDt;                    // Calendar editing: stay from date
let calStayToDt;                      // Calendar editing: stay to date
let calBookFromDt;                    // Calendar editing: booking from
let calBookToDt;                      // Calendar editing: booking to

// Lookup Data
let areaList = [];                    // List of areas (DXB, ADB, etc.)

// UI State
let creatingCalendar = true;          // true=creating, false=editing calendar
let hotelSearchParams;                // Last hotel search parameters

// Constants
let monthDays = [31,28,31,30,31,30,31,31,30,31,30,31];
let months = ['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC'];

State Flow

Page Load
listHotels() → Build accordion menu
User clicks hotel
selectHotel() → selectedHotel = hotel
showHotelData() → Load tabs
showHotelRooms() → roomMap populated
updateRoomData() → selectedRoom = room

Core Functions

1. Hotel Management Functions

listHotels(namepart, area) - hotelmgmt.js:205

Purpose: Load and display hotel list in left sidebar accordion

Flow:

export function listHotels(namepart, area) {
    const sessId = sessionStorage.getItem("ss_user_uid");
    const apiParams = {
        "session": sessId,
        "name": (namepart ? namepart : null),
        "area": (area ? area : null)
    };

    glb.tlinq("hotel/listHotels", apiParams).then((hotels) => {
        if((hotels) && (hotels.length > 0)) {
            // Group hotels by area
            let hotelMap = new Map();
            hotels.forEach((hotel) => {
                let areaHotels = hotelMap.has(hotel.area) ?
                    hotelMap.get(hotel.area) : [];
                areaHotels.push(hotel);
                hotelMap.set(hotel.area, areaHotels);
            });

            // Build accordion UI
            buildHotelMenu(hotelMap);
        } else {
            // Show "no hotels" message with "Add First Hotel" link
            showMessageSection('warning', msgTxt, '#div-msg-req');
        }

        // Load all meal plans for later use
        glb.tlinq("hotel/listMealPlans", {"session": sessId}).then((plans) => {
            plans.forEach((mplan) => {
                allMealPlans.set(mplan.plan, mplan);
                // Build meal plan checkboxes for meal config dialog
            });
        });
    });
}

UI Result: Accordion menu grouped by area (DXB, ADB, SHJ, etc.), each area containing hotel buttons


selectHotel(event) - hotelmgmt.js:147

Purpose: Handle hotel selection from accordion menu

Implementation:

function selectHotel(event) {
    const idStr = $(event.currentTarget).attr('data-row-id');
    const id = parseInt(idStr);
    const sess = sessionStorage.getItem("ss_user_uid");

    glb.tlinq("hotel/getHotel", {"session": sess, "id": id}).then((htl) => {
        selectedHotel = htl;
        showHotelData();
    });
}

Triggered By: Click on hotel button in accordion Updates: selectedHotel state Next Action: Calls showHotelData()


showHotelData() - hotelmgmt.js:130

Purpose: Display hotel details in tabbed interface

Implementation:

function showHotelData() {
    // Bind hotel data to Hotel Info tab
    let entities = {"hotel": selectedHotel};
    setViewEntities($('#hotel-panel'), entities);

    // Load rooms for Rooms tab
    showHotelRooms();

    // Load meal plans for this hotel
    const sessId = sessionStorage.getItem("ss_user_uid");
    glb.tlinq("hotel/listMealPlans",
        {"session": sessId, "hotelId": selectedHotel.hotelId}).then((mls) => {
        updateMealPlans(mls);
    });

    // Hide search results, show hotel data tabs
    $('#searchres_div').addClass('hide');
    $('#hoteldata_div').removeClass('hide');
}

UI Changes: - Hides search results panel - Shows tabbed interface (Hotel Info | Rooms | Promotions) - Populates Hotel Info tab with form fields bound to hotel entity


updateHotel(event) - hotelmgmt.js:300

Purpose: Save hotel changes (create or update)

Implementation:

export function updateHotel(event) {
    hideMessageSection('#div-msg-req');

    // Get form data using data-entity-name/field attributes
    let htlEnt = getViewEntities($('#hotel-panel'))['hotel'];
    let htlUpdEnt = selectedHotel;
    const isNew = !!!selectedHotel['hotelId'];

    // Merge form changes into selectedHotel
    for(const cc in htlEnt)
        htlUpdEnt[cc] = htlEnt[cc];

    const apiParam = {
        'session': sessionStorage.getItem("ss_user_uid"),
        'hotel': htlUpdEnt
    };

    glb.tlinq("hotel/saveHotel", apiParam).then((res) => {
        selectedHotel = res;
        if(isNew)
            listHotels();  // Refresh hotel list

        $('#updatehotel_btn').notify("Update successful.", "success");
    });
}

Data Binding: Uses data-entity-name="hotel" and data-entity-field="fieldName" attributes on form inputs

Notification: Shows toast message at button location


addNewHotel(event) - hotelmgmt.js:325

Purpose: Initialize UI for creating new hotel

Implementation:

export function addNewHotel(event) {
    hideMessageSection('#div-msg-req');
    selectedHotel = {};  // Empty object
    $('#hoteldata_div').removeClass('hide');
    clearViewEntities($('#hotel-panel'));  // Clear all form fields
    $('#f_hotelcountry').val("UAE");  // Set default country
}

Note: Clearing selectedHotel.hotelId triggers "create" mode in updateHotel()


2. Room Management Functions

showHotelRooms(selRoomId) - hotelmgmt.js:82

Purpose: Load room list for selected hotel

Implementation:

function showHotelRooms(selRoomId) {
    clearViewEntities($('#rooms-panel'));
    const sess = sessionStorage.getItem("ss_user_uid");
    roomCalendar = [];  // Reset calendar data

    glb.tlinq('hotel/listHotelRooms',
        {"session": sess, "hotelId": selectedHotel.hotelId}).then((rms) => {

        if(rms.length > 0) {
            rms.sort((a, b) => a.sortorder - b.sortorder);
            setupRoomData(rms, selRoomId);
        } else {
            $('#f_roomselect').empty()
                .notify("No rooms configured for this hotel - Add rooms!", "error");
        }
    });
}

Flow: API call → setupRoomData()updateRoomData()


setupRoomData(rooms, selRoomId) - hotelmgmt.js:64

Purpose: Populate room dropdown and room map

Implementation:

function setupRoomData(rooms, selRoomId) {
    roomMap.clear();
    hideMessageSection('#div-msg-room');
    const rmSelect = $('#f_roomselect');
    rmSelect.empty();

    rooms.forEach((room) => {
        rmSelect.append(optElem(room.roomId, room.name));
        roomMap.set(room.roomId, room);
    });

    if(selRoomId)
        rmSelect.val(selRoomId);

    $('#roomcalendar_div').addClass('hide');
    updateRoomData();

    // Attach change handler
    rmSelect.on("change", updateRoomData);
}

Key Feature: roomMap enables fast room lookups by ID


updateRoomData() - hotelmgmt.js:28

Purpose: Update UI when room selection changes

Implementation:

function updateRoomData() {
    const selRoomId = $('#f_roomselect').val();
    const id = parseInt(selRoomId);
    const theRoom = roomMap.get(id);
    selectedRoom = theRoom;

    // Bind room entity to form fields
    setViewEntities($('#rooms-panel'), {"room": theRoom});

    // Refresh calendar if data exists
    if(roomCalendar && (roomCalendar.length > 0)) {
        showRoomCalendarClick();
    }

    // Populate "Copy From" dropdown (all rooms except selected)
    const copySelect = $('#f_copyfrom').empty();
    roomMap.forEach((rm) => {
        if(rm.roomId !== id) {
            copySelect.append(optElem(rm.roomId, rm.name));
        }
    });
}

Triggered By: - Room dropdown change event - After creating new room - After loading rooms


saveRoom(event) - hotelmgmt.js:374

Purpose: Update existing room

Implementation:

export function saveRoom(event) {
    let updRoom = getViewEntities($('#rooms-panel'))['room'];
    for(const fld in updRoom)
        selectedRoom[fld] = updRoom[fld];

    const sess = sessionStorage.getItem("ss_user_uid");
    const apiParam = {"session": sess, "room": selectedRoom};

    glb.tlinq("hotel/saveRoom", apiParam).then((res) => {
        console.log("Updated room " + res.roomId);
        $('#saveroom_btn').notify("Update successful.", "success");
    });
}


addNewRoom(event) / saveNewRoom(event) - hotelmgmt.js:369, 349

Purpose: Create new room via dialog

Flow:

// Open dialog
export function addNewRoom(event) {
    clearViewEntities($('#newroom-panel'));
    $('#newroom-dialog').foundation('open');
}

// Save from dialog
export function saveNewRoom(event) {
    let newRm = getViewEntities($('#newroom-panel'))['room'];
    newRm['hotelId'] = selectedHotel.hotelId;

    const sess = sessionStorage.getItem("ss_user_uid");
    const apiParam = {"session": sess, "room": newRm};

    $('#newroom-dialog').foundation('close');

    glb.tlinq("hotel/saveRoom", apiParam).then((res) => {
        console.log("Added hotel room " + res.roomId);
        showHotelRooms(res.roomId);  // Refresh rooms, select new room
        $('#savenewroom_btn').notify("Update successful.", "success");
        $('#roomcal_placeholder').empty();
        $('#roomcalendar_div').addClass('hide');
    });
}

Dialog Structure (hotelmgmt.html:77-141): - Room name (text) - Description (text) - Max occupancy (number) - Max adults (number) - Extra bed available (checkbox) - Extra bed required (checkbox) - Room notes (textarea) - Bedding description (textarea)


3. Calendar Management Functions

addPricing(event) - hotelmgmt.js:390

Purpose: Open calendar creation dialog

Implementation:

export function addPricing(event) {
    $('#f_calmealsupp').val('');
    $('#f_calmsg').empty();
    creatingCalendar = true;  // Flag for create mode
    $('#wait-div-cal').addClass('hide');

    // Clean up meal supplements map (remove obsolete plans)
    if((mealSuppMap) && (mealSuppMap.size > 0)) {
        let newSm = new Map();
        mealSuppMap.forEach((val, key) => {
            if(hotelMealPlans.has(key))
                newSm.set(key, val);
        });
        mealSuppMap = newSm;
    } else {
        mealSuppMap = new Map();
    }

    $('#newcal-dialog').foundation('open');
}

UI State: Sets creatingCalendar = true to differentiate from editing mode


saveRoomCalendar(event) - hotelmgmt.js:455

Purpose: Create calendar entries for date range

Implementation (simplified):

export function saveRoomCalendar(event) {
    const calEntity = getViewEntities($('#newcal-panel'))['calendar'];

    // Extract dates from date pickers
    const fromDate = $('#f_newcalfrom').fdatepicker().data("datepicker").getDate();
    const toDate = $('#f_newcalto').fdatepicker().data("datepicker").getDate();
    const bookFrom = $('#f_newbkfrom').fdatepicker().data("datepicker").getDate();
    const bookTo = $('#f_newbkto').fdatepicker().data("datepicker").getDate();

    // Build calendar entry template
    let calEnt = {
        roomId: selectedRoom.roomId,
        market: calEntity.market,
        stayDate: glb.webDate(fromDate),
        bookFrom: glb.webDate(bookFrom),
        bookTo: glb.webDate(bookTo),
        baseRate: parseFloat(calEntity.baseRate),
        specDayRate: parseFloat(calEntity.specDayRate),
        sdRateType: calEntity.sdRateType,
        adultRate: parseFloat(calEntity.adultRate),
        adultRateType: calEntity.adultRateType,
        childRate: parseFloat(calEntity.childRate),
        childRateType: calEntity.childRateType,
        extraBedRate: parseFloat(calEntity.extraBedRate),
        rateBase: calEntity.rateBase,
        mealBase: parseInt(calEntity.mealBase),
        mealSupplements: buildMealSupplementsJSON(),  // From meal supp map
        stopSale: roomStatus === 'S',
        available: roomStatus === 'A',
        onRequest: roomStatus === 'R',
        mlos: parseInt(calEntity.mlos),
        sdmlos: parseInt(calEntity.sdmlos)
    };

    const apiParam = {
        "session": sessionStorage.getItem("ss_user_uid"),
        "template": calEnt,
        "stayTo": glb.webDate(toDate),
        "specDays": specDaySetting  // "WE"/"ALL"/"NO"
    };

    glb.tlinq("hotel/createRoomCalendar", apiParam).then((calRes) => {
        const numRecs = calRes.length;
        $('#addcal_btn').notify("Created " + numRecs + " calendar records", "success");
        $('#newcal-dialog').foundation('close');
        refreshRoomCalendar();  // Reload calendar view
    });
}

Key Logic: - buildMealSupplementsJSON(): Serializes mealSuppMap to JSON array - Special day setting: "WE" (weekends), "ALL" (all days), "NO" (none) - Date format conversion: JavaScript Date → "dd-mm-yyyy" string


refreshHotelCalendar(event) - hotelmgmt.js:1161

Purpose: Display monthly calendar grid for all rooms

Implementation:

export function refreshHotelCalendar(event) {
    const mthStr = $('#f_calmonth').val();
    const yrStr = $('#f_calyear').val();
    let mth = parseInt(mthStr);
    let yr = parseInt(yrStr);

    // Calculate date range for selected month
    let fromDate = new Date(yr, mth, 1);
    let mth2 = 1 + mth;
    let yr2 = yr;
    if(mth2 > 11) {
        yr2++;
        mth2 = 0;
    }
    let toDate = new Date(yr2, mth2, 1);

    const fromDateStr = glb.webDate(fromDate);
    const toDateStr = glb.webDate(toDate);
    const session = sessionStorage.getItem("ss_user_uid");
    const mkt = $('#f_hcal_market').val();

    const apiParams = {
        "session": session,
        "hotelId": selectedHotel.hotelId,
        "market": mkt,
        "fromDate": fromDateStr,
        "toDate": toDateStr
    };

    showWaitDialog('small', '#wait-div-htlcal');

    glb.tlinq("hotel/listRoomCalendar", apiParams).then((cal) => {
        // Group calendar entries by room
        let calRoomMap = new Map();
        cal.forEach((cd) => {
            let roomCal = calRoomMap.get(cd.roomId);
            if(roomCal)
                roomCal.entries.push(cd);
            else {
                roomCal = {
                    "roomId": cd.roomId,
                    "entries": [cd]
                };
                calRoomMap.set(cd.roomId, roomCal);
            }
        });

        // Build calendar table
        buildCalendarTable(calRoomMap);
        hideWaitDialog();
    });
}

Calendar Visualization: - One row per room - Columns 1-31 for each day of month - Color coding: - Green (#3adb76): Normal day available - Yellow (#fff3d9): Special day available - Red (#ff8378): Stop sale - Blue (#6bd7f2): On request - Hover tooltip shows full rate details


buildCalendarTable(calRoomMap) - Referenced in code

Purpose: Generate HTML table showing calendar grid

Logic (hotelmgmt.js:1095-1159):

function buildHotelCalCell(aDay, aDate) {
    let res = ['', '', ''];

    // Determine background color based on status
    let bgcol = aDay.specialDay ? '#fff3d9' : '#3adb76';
    if(aDay.stopSale)
        bgcol = '#ff8378';
    else if(aDay.onRequest)
        bgcol = '#6bd7f2';

    // Get display price
    let thePrice = aDay.baseRate.toFixed();
    if(aDay.specialDay)
        if(aDay.specDayRate)
            thePrice = aDay.specDayRate.toFixed();

    // Build hover text with all rate details
    let hoverText = glb.displayDate(aDate);
    hoverText += '\nMLOS:' + aDay.mlos + ' nights';
    hoverText += '\n---- Supplements ----';
    hoverText += '\nExtra Adlt: ' + (aDay.adultRate ? aDay.adultRate : '0') +
        '/' + (aDay.adultSdRate ? aDay.adultSdRate : '0') + ' (' + aDay.adultRateType + ')';
    hoverText += '\nExtra Chld: ' + (aDay.childRate ? aDay.childRate : '0') +
        '/' + (aDay.childSdRate ? aDay.childSdRate : '0') + ' (' + aDay.childRateType + ')';

    // Parse meal supplements JSON
    const mealSuppArray = JSON.parse(aDay.mealSupplements);
    const msDescArray = [];
    mealSuppArray.forEach((ms) => {
        const msDesc = ms.mealPlan + ': AD ' + ms.adlRate[0] + '/' + ms.adlRate[1] +
            ', CH ' + ms.chldRate[0] + '/' + ms.chldRate[1];
        msDescArray.push(msDesc);
    });
    const msDesc = msDescArray.join('\n');
    hoverText += '\n' + msDesc;

    // Create tooltip span
    let rateText = '<span data-tooltip class="top" title="' + hoverText + '">' + thePrice + '</span>';

    res[0] = bgcol;    // Background color
    res[1] = '#0425a8'; // Foreground color
    res[2] = rateText; // Cell content HTML

    return res;
}

Result: Rich calendar grid with tooltips showing complete pricing details on hover


4. Meal Plan Management

editMeals(event) - hotelmgmt.js:251

Purpose: Open meal plan configuration dialog

Implementation:

export function editMeals(event) {
    // Reset all checkboxes and inputs
    $('#td-allmeals').find("[type=checkbox]").prop("checked", false);
    $('#td-allmeals').find("[type=number]").val("0");

    // Pre-fill with hotel's current meal plans
    hotelMealPlans.forEach((mpl) => {
        const cbid = '#ckbx-' + mpl.plan;
        const adcId = '#adc-' + mpl.plan;
        const chcId = '#chc-' + mpl.plan;
        $(cbid).prop("checked", true);
        $(adcId).val(mpl.adultCost ? mpl.adultCost.toString() : "0");
        $(chcId).val(mpl.childCost ? mpl.childCost.toString() : "0");
    });

    $('#hotelmeals-dialog').foundation("open");
}

Dialog Content (hotelmgmt.html:42-59): - Table with rows for each meal plan (RO, BB, HB, FB, AI, etc.) - Checkbox to enable/disable plan - Adult cost input - Child cost input


saveMeals(event) - hotelmgmt.js:265

Purpose: Save meal plan configuration for hotel

Implementation:

export function saveMeals(event) {
    let planIds = [];

    // Collect checked meal plans with costs
    $('#td-allmeals').find("[type=checkbox]:checked").each(function() {
        const idStr = $(this).attr("id");
        const pl = idStr.split('-')[1];  // Extract plan code (e.g., "BB")
        const adcid = '#adc-' + pl;
        const chcid = '#chc-' + pl;
        const plid = allMealPlans.get(pl).planId;

        let vs = $(adcid).val();
        const adc = parseFloat(vs ? vs : "0.0");
        vs = $(chcid).val();
        const chc = parseFloat(vs ? vs : "0.0");

        const thePlan = {
            mealPlanId: plid,
            adultSupplement: adc,
            childSupplement: chc
        };
        planIds.push(thePlan);
    });

    const apiParams = {
        "session": sessionStorage.getItem("ss_user_uid"),
        "hotelId": selectedHotel.hotelId,
        "plans": planIds
    };

    glb.tlinq('hotel/updateMealPlans', apiParams).then((newPlans) => {
        updateMealPlans(newPlans);  // Refresh dropdown
        $('#editmeals_btn').notify("Update successful", "success");
    });

    $('#hotelmeals-dialog').foundation("close");
}


5. Search and Booking Functions

searchAllHotels() - hotelmgmt.js:1038

Purpose: Search for available rooms across all hotels

Implementation:

export function searchAllHotels() {
    const adl = parseInt($('#f_srch_adlts').val());
    const chl = parseInt($('#f_srch_chld').val());
    const ckin = $('#f_srch_chkin').val();
    const ckout = $('#f_srch_chkout').val();
    const bgt = $('#f_srch_budget').val();
    const mkt = $('#f_srch_market').val();
    const sess = sessionStorage.getItem("ss_user_uid");

    const apiParam = {
        "session": sess,
        "adults": adl,
        "children": chl,
        "checkin": ckin,
        "checkout": ckout,
        "maxBudget": bgt,
        "market": mkt,
        "meals": null,
        "area": null,
        "context": "config"  // Employee pricing view
    };

    // Add optional filters
    const harea = $('#f_srch_area').val();
    if(harea !== 'ANY')
        apiParam.area = harea;

    const mls = $('#f_srch_meal').val();
    if("NA" !== mls)
        apiParam.meals = mls;

    hotelSearchParams = apiParam;  // Save for booking later

    hideMessageSection('#div-msg-req');
    showWaitDialog();

    glb.tlinq('hotel/searchAccommodation', apiParam).then((hres) => {
        if(hres.results && (hres.results.length > 0)) {
            // Sort rooms by price within each hotel
            hres.results.forEach((res) => {
                res.rooms.sort((a, b) => a.price - b.price);
            });

            // Sort hotels by cheapest room price
            hres.results.sort((a, b) => a.rooms[0].price - b.rooms[0].price);

            showSearchResultList2(hres);
            hideWaitDialog();
        } else {
            hideWaitDialog();
            $.notify("No results found. Please change your search criteria.", "warn");
        }
    });
}

Search Form (in hotelmgmt.html): - Check-in / check-out dates - Adults / children counts - Area filter (dropdown) - Market (UAE/GCC/ROW) - Meal plan filter - Max budget


showSearchResultList2(sres) - hotelmgmt.js:898

Purpose: Display search results as accordion with room tables

Implementation (simplified):

function showSearchResultList2(sres) {
    const srdiv = $('#searchres_div');
    srdiv.empty();
    searchResultMap.clear();

    const sracc = $('<ul id="searchres_acc" class="accordion" data-accordion data-allow-all-closed="true"></ul>');
    const mgn = 1 + parseInt($('#f_srch_margin').val()) / 100;  // Margin multiplier

    sres.results.forEach((hotel) => {
        const firstRmCost = hotel.rooms[0].price;
        const firstRoomPrice = firstRmCost * mgn;

        // Create accordion item for hotel
        const hotelacc_str = '<li class="accordion-item" data-accordion-item>' +
            '<a href="#" class="accordion-title" style="font-size: 110%">' +
            hotel.hotel.name + ' <b>(from AED ' + firstRoomPrice.toFixed(2) + ')</b></a>';
        const hotelacc = $(hotelacc_str);

        // Create table for rooms
        const hotelTbl = $('<table></table>');
        const hdr = $('<thead><tr><th><p><b>Room type</b></p></th>' +
            '<th>Meal</th><th>Cost</th><th>Price</th><th>MLS</th><th>Actions</th>' +
            '</tr></thead>');
        const body = $('<tbody></tbody>');

        hotel.rooms.forEach((room) => {
            const rmSellPrc = room.price * mgn;

            // Build room description with status indicators
            let rmDesc = room.roomName;
            if(room.notes)
                rmDesc += '<br><span style="font-size: 75%">*' + room.notes + '</span>';
            if(room.stopSale)
                rmDesc += '<br><span style="font-size: 75%; color: darkred">*Room on stop sale within this period</span>';
            else if(room.onRequest)
                rmDesc += '<br><span style="font-size: 75%; color: #1779ba">*Room available on request only</span>';

            // Create action links
            const lnkdata = '' + hotel.hotel.hotelId + ':' + room.roomId;
            const bkdlink = '<a class="button perun-button-small" href="#" data-result-id="' + lnkdata + '">Breakdown</a>' +
                '&nbsp;&nbsp;<a class="button perun-button-small" href="#" data-book-id="' + lnkdata + '">Book</a>';

            // MLOS warning if search is shorter than minimum
            const mls = (room.calendar.length < room.mlos ?
                '<span style="color:red"><b>' + room.mlos + '</b></span>' : '' + room.mlos);

            const rmRowS = '<tr><td>' + rmDesc + '</td><td>' + room.mealPlanName + '</td>' +
                '<td>' + room.price.toFixed(2) + '</td>' +
                '<td>' + rmSellPrc.toFixed(2) + '</td>' +
                '<td>' + mls + '</td>' +
                '<td>' + bkdlink + '</td>' +
                '</tr>';

            body.append($(rmRowS));
            searchResultMap.set(lnkdata, room);  // Save for breakdown/booking
        });

        hotelTbl.append(hdr);
        hotelTbl.append(body);

        const hotelBody = $('<div class="accordion-content" data-tab-content></div>');
        hotelBody.append(hotelTbl);
        hotelacc.append(hotelBody);
        sracc.append(hotelacc);
    });

    srdiv.append(sracc);
    srdiv.foundation();  // Initialize Foundation accordion
    $('#hoteldata_div').addClass('hide');
    srdiv.removeClass('hide');

    // Attach event handlers
    $('a[data-result-id]').on("click", showPriceBreakdown);
    $('a[data-book-id]').on("click", showBookDialog);
}

Result: Accordion with hotel panels, each containing a table of available rooms with cost, price, and action buttons


showPriceBreakdown(event) - hotelmgmt.js:733

Purpose: Display detailed nightly cost breakdown

Implementation:

export function showPriceBreakdown(event) {
    const idStr = $(event.currentTarget).attr('data-result-id');
    const room = searchResultMap.get(idStr);
    const htl = room.hotel;

    $('#para_bkdownheader').html('<b>Room: </b>' + room.roomName + ' at ' + htl.hotel.name);

    const bkdownBody = $('#breakdown-body');
    bkdownBody.empty();

    // Display each night's breakdown
    room.calendar.forEach((night, idx) => {
        const stayDt = new Date();
        stayDt.setTime(night.stayDate);
        const dtStr = glb.displayDate(stayDt);

        const bkdnTxt = night.breakdown;  // Array of breakdown strings from API
        const bkdnHtml = bkdnTxt.join('<br>');

        const rowStr = '<tr><td>' + dtStr + '</td><td>' + bkdnHtml + '</td></tr>';
        bkdownBody.append($(rowStr));
    });

    $('#breakdown-dialog').foundation('open');
}

Breakdown Display: - Date - Base rate - Extra adult charges - Extra child charges - Meal supplements - Extra bed charges - Night total


createBooking(event) / createLead(bookedRoom) - hotelmgmt.js:865, 772

Purpose: Create booking request (lead) for selected room

Implementation:

export function createBooking(event) {
    createLead(bookedRoom);
}

function createLead(aRoom) {
    const fromDate = $('#fl_bk_ckin').fdatepicker().data("datepicker").getDate();
    const toDate = $('#fl_bk_ckout').fdatepicker().data("datepicker").getDate();
    const qty = parseInt($('#fl_bk_qty').val());
    const mgn = 1 + parseInt($('#f_srch_margin').val()) / 100;
    const total = qty * (aRoom.price * mgn);
    const custName = $('#fl_bk_name').val();

    // Build description text
    const descr = 'Guest name: ' + custName + '\n' +
        'Room type: ' + aRoom.roomName + ', ' +
        'Meal plan: ' + allMealPlans.get(aRoom.mealPlanName).name + '\n' +
        hotelSearchParams.adults + ' adults\n' +
        hotelSearchParams.children + ' children (' + $('#fl_bk_chldages').val() + ')\n' +
        'Check-In: ' + glb.displayDate(fromDate) + '\n' +
        'Check-Out: ' + glb.displayDate(toDate) + '\n' +
        'Number of rooms: ' + qty + '\n\n' +
        'Other requests: \n' + $('#fl_bk_otherinfo').val();

    const leadReq = {
        "session": sessionStorage.getItem("ss_user_uid"),
        "tripRequest": {
            "customerName": custName,
            "tripName": custName + "," + dest,
            "description": descr,
            "requestCode": glb.createId("TR"),
            "origin": "UAE",
            "destination": dest,
            "startDate": fromDate,
            "endDate": toDate,
            "numAdults": hotelSearchParams.adults,
            "numChildren": hotelSearchParams.children,
            "childAges": $('#fl_bk_chldages').val(),
            "contactName": custName,
            "contactEmail": $('#fl_bk_email').val(),
            "contactType": "EMAIL",
            "quantity": qty,
            "plannedRevenue": total
        }
    };

    $('#wait_bookreq').removeClass('hide');

    glb.tlinq('hotel/createBookingRequest', leadReq).then((newTrip) => {
        hideMessageSection('#div-msg-req');
        sessionStorage.setItem("ss_triprequest", JSON.stringify(newTrip));
        $('#wait_bookreq').addClass('hide');
        $('#bookroom-dialog').foundation('close');
        $.notify('New reservation request created.', "success", {position: "top center"});
    });
}

Booking Dialog (hotelmgmt.html): - Hotel name (display only) - Room type (display only) - Check-in / check-out (editable dates) - Number of rooms - Customer name - Customer email - Child ages - Other requests (textarea)


Data Binding System

Entity-Field Binding

The application uses a custom data binding system using HTML attributes:

<input type="text"
    data-entity-name="hotel"
    data-entity-field="name"
    id="f_hotelname" />

setViewEntities(container, entities)

Purpose: Populate form fields from entity objects

Usage:

setViewEntities($('#hotel-panel'), {"hotel": selectedHotel});

Logic: 1. Find all elements with data-entity-name attribute within container 2. For each element, read data-entity-field attribute 3. Get value from entity object: entity[fieldName] 4. Set input value based on type: - Text/number inputs: .val(value) - Checkboxes: .prop("checked", value) - Selects: .val(value)

getViewEntities(container)

Purpose: Extract form field values into entity objects

Usage:

let entities = getViewEntities($('#hotel-panel'));
let hotelData = entities['hotel'];

Logic: 1. Find all elements with data-entity-name attribute 2. Group by entity name 3. For each element, extract value: - Text/number inputs: .val() - Checkboxes: .is(":checked") - Selects: .val() 4. Build object: {entityName: {field1: value1, field2: value2, ...}}

Return: Object with entity names as keys


UI Component Patterns

1. Foundation Accordion

Usage: Hotel list grouped by area

HTML Structure:

<ul class="accordion" data-accordion>
    <li class="accordion-item" data-accordion-item>
        <a href="#" class="accordion-title">Dubai (DXB)</a>
        <div class="accordion-content" data-tab-content>
            <button>Atlantis The Palm</button>
            <button>Burj Al Arab</button>
        </div>
    </li>
    <li class="accordion-item" data-accordion-item>
        <a href="#" class="accordion-title">Abu Dhabi (ADB)</a>
        <div class="accordion-content" data-tab-content>
            <button>Emirates Palace</button>
        </div>
    </li>
</ul>

Initialization:

Foundation.reInit('accordion');
$('#hotelmenu_div').foundation();


2. Foundation Tabs

Usage: Hotel Info | Rooms | Promotions tabs

HTML Structure:

<ul class="tabs" data-tabs id="hotel-tabs">
    <li class="tabs-title is-active">
        <a href="#panel-hotel">Hotel Info</a>
    </li>
    <li class="tabs-title">
        <a href="#panel-rooms">Rooms</a>
    </li>
    <li class="tabs-title">
        <a href="#panel-promos">Promotions</a>
    </li>
</ul>

<div class="tabs-content" data-tabs-content="hotel-tabs">
    <div class="tabs-panel is-active" id="panel-hotel">
        <!-- Hotel form fields -->
    </div>
    <div class="tabs-panel" id="panel-rooms">
        <!-- Rooms content -->
    </div>
    <div class="tabs-panel" id="panel-promos">
        <!-- Promotions content -->
    </div>
</div>


3. Foundation Reveal (Modal Dialogs)

Usage: All popup forms

HTML Structure:

<div class="large reveal" id="newcal-dialog" data-reveal>
    <!-- Dialog content -->
    <button class="close-button" data-close aria-label="Close">
        <span aria-hidden="true">&times;</span>
    </button>
</div>

Opening:

$('#newcal-dialog').foundation('open');

Closing:

$('#newcal-dialog').foundation('close');

Programmatic Close:

<button data-close>Cancel</button>


4. Foundation Datepicker

Usage: All date input fields

HTML:

<input id="f_newcalfrom"
    data-date-format="dd-mm-yyyy"
    type="text"
    class="fdatepicker"
    placeholder="Stay From Date" />

Initialization:

$('#f_newcalfrom').fdatepicker({
    format: 'dd-mm-yyyy'
});

Getting Date Object:

const fromDate = $('#f_newcalfrom')
    .fdatepicker({ format: 'dd-mm-yyyy' })
    .data("datepicker")
    .getDate();

Setting Date:

$('#fl_bk_ckin').val(hotelSearchParams.checkin);


5. Notify.js (Toast Notifications)

Usage: Success/error messages

Success Notification (at element):

$('#updatehotel_btn').notify("Update successful.", "success");

Global Notification:

$.notify("No results found.", "warn", {position: "top center"});

Error Notification:

$.notify("Error - contact admin!", "error");

Parameters: - Message text - Type: "success", "warn", "error" - Options: {position: "top center"} etc.


Helper Functions

Date Formatting

// Convert JavaScript Date to "dd-mm-yyyy" string
glb.webDate(dateObj);

// Convert "dd-mm-yyyy" string to display format
glb.displayDate(dateObj);

// Example:
const today = new Date();
const webFormat = glb.webDate(today);  // "26-11-2025"

Confirmation Dialog

function ask(theQuestion, whenYes, whenNo) {
    $('#confirm_text').html(theQuestion);

    $('#dlg_btn_ok').one('click', function(event) {
        $('#confirm_dialog').foundation('close');
        if(whenYes)
            whenYes(event);
    });

    $('#dlg_btn_cancel').one('click', function(event) {
        $('#confirm_dialog').foundation('close');
        if(whenNo)
            whenNo(event);
    });

    $('#confirm_dialog').foundation('open');
}

// Usage:
ask("Are you sure you want to delete this room?",
    () => { deleteRoom(); },
    () => { console.log("Cancelled"); }
);

Wait Dialog

// Show loading spinner
showWaitDialog();
showWaitDialog('small', '#wait-div-htlcal');  // With custom selector

// Hide loading spinner
hideWaitDialog();

Message Sections

// Show inline message
showMessageSection('warning', 'Please fill all required fields', '#div-msg-req', '#var-msg-req');

// Hide inline message
hideMessageSection('#div-msg-req');

Event Handling Patterns

Click Events

// Method 1: Direct handler assignment
$('#updatehotel_btn').on("click", updateHotel);

// Method 2: Delegated event (for dynamically created elements)
$('a[data-result-id]').on("click", showPriceBreakdown);

// Method 3: One-time event
$('#dlg_btn_ok').one('click', function(event) {
    // Handler runs only once
});

Change Events

// Dropdown change
$('#f_roomselect').on("change", updateRoomData);

// Checkbox change
$('#f_calmlsupp').on("change", populateSelectedMealPlan);

Form Submission

Note: No form submit events used; all actions triggered by button clicks


AJAX Call Pattern

All API calls use the glb.tlinq() wrapper:

glb.tlinq('hotel/saveHotel', apiParam).then(
    (result) => {
        // Success handler
        console.log("Success:", result);
        $.notify("Update successful", "success");
    },
    (error) => {
        // Error handler
        console.error("Error:", error);
        $.notify("Error: " + error.errorMessage, "error");
    }
);

glb.tlinq() Implementation (in globals.js, not shown): - Builds full API URL - Adds session token if not in params - Makes jQuery AJAX POST request - Returns Promise - Handles common errors


Performance Optimizations

1. Map-Based Lookups

Instead of array searches:

// Fast: O(1) lookup
roomMap.set(room.roomId, room);
const room = roomMap.get(roomId);

// Slow: O(n) search
rooms.find(r => r.roomId === roomId);

2. Event Delegation

For dynamically created search results:

// Attach once to parent
$('a[data-result-id]').on("click", showPriceBreakdown);

// Better than attaching to each link individually

3. Conditional Loading

Only load calendar when needed:

if(roomCalendar && (roomCalendar.length > 0)) {
    showRoomCalendarClick();
}

4. Batch DOM Updates

Build complete HTML string before appending:

// Good: Single append
const table = $('<table></table>');
rows.forEach(row => {
    table.append(buildRow(row));
});
$('#container').append(table);

// Bad: Multiple appends to live DOM
rows.forEach(row => {
    $('#container').append(buildRow(row));
});


Error Handling

API Error Pattern

glb.tlinq('hotel/saveHotel', apiParam).then(
    (result) => {
        // Success
    },
    (error) => {
        // API returned error response
        console.error("API Error: " + error.errorCode + " - " + error.errorMessage);
        $.notify("Error: " + error.errorMessage, "error");
    }
);

Client-Side Validation

Example: Validate dates before API call:

const fromDate = $('#f_newcalfrom').fdatepicker().data("datepicker").getDate();
const toDate = $('#f_newcalto').fdatepicker().data("datepicker").getDate();

if(fromDate >= toDate) {
    $.notify("End date must be after start date!", "error");
    return;  // Prevent API call
}

Error Notification Locations

  1. At Button: For button-specific actions

    $('#updatehotel_btn').notify("Update failed", "error");
    

  2. Global: For general operations

    $.notify("Session expired", "error", {position: "top center"});
    

  3. Console Log: For debugging

    console.error("Cannot load rooms:", err);
    


Session Management

Session Token

Stored in browser's sessionStorage:

// Get session token
const session = sessionStorage.getItem("ss_user_uid");

// All API calls include session
glb.tlinq('hotel/listHotels', {"session": session, ...});

Session Expiry Handling

Handled by glb.tlinq() wrapper: - Detects SESSION_ERROR response - Redirects to login page - Clears sessionStorage


Browser Compatibility

Requirements

  • JavaScript: ES6 (2015) support required
  • Modules
  • Arrow functions
  • Template literals
  • Map/Set objects
  • Promises

  • CSS: Foundation 6.x requirements

  • Flexbox
  • CSS Grid (optional)

  • Supported Browsers:

  • Chrome 51+
  • Firefox 54+
  • Safari 10+
  • Edge 15+

Not Supported

  • Internet Explorer (any version)
  • Older mobile browsers

Accessibility Considerations

ARIA Attributes

Foundation components include built-in ARIA support:

<button class="close-button" data-close aria-label="Close modal">
    <span aria-hidden="true">&times;</span>
</button>

Keyboard Navigation

  • Tab through form fields
  • Enter to submit/confirm
  • Escape to close dialogs (Foundation default)
  • Arrow keys in dropdowns

Screen Reader Support

  • Form labels properly associated with inputs
  • Required fields indicated
  • Error messages announced
  • Dialog titles announced

Future Frontend Enhancements

1. Validation Library Integration

Replace manual validation with library like: - jQuery Validation Plugin - Parsley.js - Vuelidate (if migrating to Vue)

2. State Management Framework

For complex state, consider: - Redux (with React) - Vuex (with Vue) - MobX

3. Module Bundler

Use Webpack or Rollup to: - Bundle modules - Minify code - Tree-shake unused code - Enable hot module replacement

4. TypeScript Migration

Add type safety:

interface Hotel {
    hotelId: number;
    name: string;
    area: string;
    stars: number;
    // ...
}

function selectHotel(hotel: Hotel): void {
    selectedHotel = hotel;
    showHotelData();
}

5. Component Framework

Consider migrating to: - React - Vue.js - Angular

Benefits: - Reactive data binding - Component reusability - Virtual DOM performance - Better testing support

6. Progressive Web App (PWA)

Add offline capabilities: - Service worker for caching - IndexedDB for local data - Background sync - Push notifications


Testing Considerations

Manual Testing Checklist

Hotel Management: - [ ] Create new hotel - [ ] Update existing hotel - [ ] Search hotels by name - [ ] Search hotels by area - [ ] Hotel list accordion navigation

Room Management: - [ ] Add new room to hotel - [ ] Update room details - [ ] Room dropdown selection - [ ] Occupancy validation

Calendar Management: - [ ] Create calendar entries (90 days) - [ ] Weekend vs weekday rates - [ ] Update existing entries - [ ] Copy rates between rooms - [ ] Monthly calendar view - [ ] Hover tooltips on calendar

Search & Booking: - [ ] Search all hotels - [ ] Filter by area/meal plan - [ ] Sort by price - [ ] View price breakdown - [ ] Create booking request - [ ] MLOS validation warning

Meal Plans: - [ ] Configure hotel meal plans - [ ] Meal supplement pricing - [ ] Normal vs special day costs

Automated Testing Opportunities

Unit Tests (with Jest or Mocha): - Date formatting functions - Price calculation logic - Data binding helpers

Integration Tests (with Cypress or Playwright): - Full user workflows - API integration - Dialog interactions

End-to-End Tests: - Complete booking flow - Calendar creation flow - Multi-hotel search


Summary

The Hotel Management frontend is a sophisticated single-page application built with:

Architecture: - ✅ ES6 module structure - ✅ State management with Maps - ✅ Custom data binding system - ✅ Event-driven interactions

UI Components: - ✅ Foundation accordion for navigation - ✅ Foundation tabs for organization - ✅ Foundation modals for forms - ✅ Custom calendar grid visualization - ✅ Toast notifications for feedback

Key Features: - ✅ Dynamic hotel/room loading - ✅ Complex rate calendar management - ✅ Multi-market pricing support - ✅ Real-time search with price calculation - ✅ Detailed price breakdown display - ✅ Flexible meal plan configuration - ✅ Booking request creation

Performance: - ✅ Map-based lookups for speed - ✅ Batch DOM updates - ✅ Lazy loading of data - ✅ Event delegation

User Experience: - ✅ Clear visual feedback (notifications) - ✅ Loading indicators for async operations - ✅ Confirmation dialogs for destructive actions - ✅ Inline validation - ✅ Responsive layout (Foundation grid) - ✅ Tooltips with detailed information

This implementation provides a production-ready hotel contract management interface suitable for travel agency operations.