File "actions.php"

Full Path: /home/romayxjt/public_html/wp-content/plugins/vikbooking/admin/layouts/overview/actions.php
File size: 133.19 KB
MIME-type: text/x-php
Charset: utf-8

<?php
/** 
 * @package     VikBooking
 * @subpackage  core
 * @author      E4J s.r.l.
 * @copyright   Copyright (C) 2025 E4J s.r.l. All Rights Reserved.
 * @license     http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
 * @link        https://vikwp.com
 */

// No direct access
defined('ABSPATH') or die('No script kiddies please!');

/**
 * Obtain vars from arguments received in the layout file.
 * 
 * @var string  $caller     The identifier of who's calling this layout file.
 * @var array   $rooms      List of room records involved.
 * @var array   $tmfilters  Optional list of task manager filters.
 */
extract($displayData);

// access the application
$app = JFactory::getApplication();
$vbo_app = VikBooking::getVboApplication();

// load context menu assets
$vbo_app->loadContextMenuAssets();

// preload chat assets
VBOFactory::getChatMediator()->useAssets();

// gather permissions
$vbo_auth_pricing = JFactory::getUser()->authorise('core.vbo.pricing', 'com_vikbooking');
$vbo_auth_pms = JFactory::getUser()->authorise('core.vbo.pms', 'com_vikbooking');

// load all the existing task manager areas/projects
$taskAreas = VBOTaskModelArea::getInstance()->getItems();

// currency symbol and formatting options
$currencysymb = VikBooking::getCurrencySymb();
list($currency_digits, $currency_decimals, $currency_thousands) = explode(':', VikBooking::getNumberFormatData());

// check whether VCM is available
$vcm_enabled = VikBooking::vcmAutoUpdate();

// build room-ota relations for pricing alterations, if any
$room_ota_relations = [];
foreach (array_column(($rooms ?? []), 'id') as $rid) {
    // always get a new instance of the VikChannelManagerLogos class
    $vcm_logos = VikBooking::getVcmChannelsLogo('', true);
    // load channels (firsr) and accounts (after) for this listing
    $room_ota_channels = is_object($vcm_logos) && method_exists($vcm_logos, 'getVboRoomLogosMapped') ? $vcm_logos->getVboRoomLogosMapped($rid) : [];
    $room_ota_accounts = is_object($vcm_logos) && method_exists($vcm_logos, 'getRoomOtaAccounts') ? $vcm_logos->getRoomOtaAccounts() : [];
    // filter channels not available as accounts (i.e. iCal)
    if (count($room_ota_channels) != count(($room_ota_accounts[$rid] ?? []))) {
        $ota_account_names = array_map('strtolower', array_column(($room_ota_accounts[$rid] ?? []), 'channel'));
        $room_ota_channels = array_filter($room_ota_channels, function($chid) use ($ota_account_names) {
            return in_array(strtolower($chid), $ota_account_names);
        }, ARRAY_FILTER_USE_KEY);
    }
    if ($room_ota_channels && ($room_ota_accounts[$rid] ?? [])) {
        $room_ota_relations[$rid] = [
            'channels' => $room_ota_channels,
            'accounts' => $room_ota_accounts[$rid],
        ];
    }
}

// build room names map
$room_names_map = array_combine(array_column(($rooms ?? []), 'id'), array_column(($rooms ?? []), 'name'));

// access the current task manager filters
$tmfilters = (array) (($tmfilters ?? []) ?: $app->input->get('tmfilters', [], 'array'));

// check if the tasks should be automatically loaded for certain area/project IDs
$activeAreas = array_values(array_filter(array_map('intval', $tmfilters['area_ids'] ?? [])));

?>

<button type="button" class="btn vbo-context-menu-btn vbo-context-menu-btn-raw vbo-context-menu-overview-actions">
    <span class="vbo-context-menu-lbl"><?php echo JText::translate('VBCRONACTIONS'); ?></span>
    <span class="vbo-context-menu-ico"><?php VikBookingIcons::e('sort-down'); ?></span>
</button>

<div class="vbo-overview-action-raterestr-helper" style="display: none;">

    <div class="vbo-overview-action-raterestr-wrap">
        <div class="vbo-overview-action-raterestr-info">
            <div class="vbo-overview-action-raterestr-listings-info">
                <?php VikBookingIcons::e('bed'); ?>
                <span class="vbo-overview-action-raterestr-listings"></span>
            </div>
            <div class="vbo-overview-action-raterestr-dates"></div>
        </div>
        <div class="vbo-roverw-setnewrate">
            <div class="vbo-roverw-setnewrate-title">
                <h4><?php VikBookingIcons::e('calculator'); ?> <?php echo JText::translate('VBO_RATES_AND_RESTR'); ?></h4>
            </div>
            <div class="vbo-roverw-flexnew">
                <div class="vbo-roverw-newrwrap">
                    <h4><?php VikBookingIcons::e('edit'); ?> <?php echo JText::translate('VBRATESOVWSETNEWRATE'); ?></h4>
                    <div class="vbo-roverw-newrcont">
                        <label for="roverw-newrate" class="vbo-roverw-setnewrate-currency"><?php echo $currencysymb; ?></label>
                        <input type="number" step="any" min="0" id="roverw-newrate" value="" placeholder="" size="7" />
                    </div>
                </div>
                <div class="vbo-roverw-newrestr-wrap" style="display: none;">
                    <div class="vbo-roverw-newrestrcont">
                        <h4><?php VikBookingIcons::e('ban'); ?> <?php echo JText::translate('VBOMINIMUMSTAYSET'); ?></h4>
                        <div class="vbo-roverw-newrestrcont-inner">
                            <label for="roverw-newrestr" class="vbo-roverw-setnewrestr-lbl"><?php echo JText::translate('VBDAYS'); ?></label>
                            <input type="number" step="1" min="0" id="roverw-newrestr" value="" size="7" />
                        </div>
                    </div>
                </div>
            </div>
            <div class="vbo-roverw-setnewrate-inner">
                <div class="vbo-roverw-setnewrate-vcm">
                    <div class="vbo-roverw-setnewrate-vcm-head">
                        <span class="<?php echo $vcm_enabled < 0 ? 'vbo-vcm-notinstalled' : 'vbo-vcm-installed'; ?>">
                            <?php echo $vbo_app->createPopover(array('title' => JText::translate('VBOUPDRATESONCHANNELS'), 'content' => ($vcm_enabled < 0 ? JText::translate('VBCONFIGVCMAUTOUPDMISS') : JText::translate('VBOUPDRATESONCHANNELSHELP')), 'icon_class' => VikBookingIcons::i('rocket'))); ?>
                            <?php echo JText::translate('VBOUPDRATESONCHANNELS'); ?>
                        </span>
                    </div>
                    <div class="vbo-roverw-setnewrate-vcm-body vbo-toggle-small">
                        <?php
                        echo $vbo_app->printYesNoButtons('roverw-newrate-vcm', JText::translate('VBYES'), JText::translate('VBNO'), ($vcm_enabled > 0 ? 1 : 0), 1, 0, 'vboActionVcmRestrictionsSupported();', ['blue']);

                        if ($vcm_enabled < 0) {
                            // disable the toggle button when VCM is not available
                            ?>
                        <script type="text/javascript">
                            jQuery(function() {
                                jQuery('input[name="roverw-newrate-vcm"]').prop('disabled', true);
                            });
                        </script>
                            <?php
                        }
                        ?>
                    </div>
                    <div class="vbo-roverw-setnewrate-vcm-otas"></div>
                </div>
            </div>
        </div>
    </div>

    <div class="vbo-roverw-setnewrate-vcm-ota-pricing-alteration">
        <div class="vbo-roverw-setnewrate-vcm-ota-alteration-elem">
            <select data-alter-rule="rmodsop">
                <option value="1">+</option>
                <option value="0">-</option>
            </select>
        </div>
        <div class="vbo-roverw-setnewrate-vcm-ota-alteration-elem">
            <input type="number" value="" step="any" min="0" data-alter-rule="rmodsamount" />
        </div>
        <div class="vbo-roverw-setnewrate-vcm-ota-alteration-elem">
            <select data-alter-rule="rmodsval">
                <option value="1">%</option>
                <option value="0"><?php echo $currencysymb; ?></option>
            </select>
        </div>
    </div>

</div>

<script type="text/javascript">
    /**
     * Register room-ota relations map.
     */
    const vboActionRoomOtaRels = <?php echo json_encode($room_ota_relations); ?>;

    /**
     * Register room namings map.
     */
    const vboActionRoomNames = <?php echo json_encode($room_names_map); ?>;

    /**
     * Register room rate plans map.
     */
    const vboActionRoomRplansMap = {};

    /**
     * Register function for loading the room rates.
     */
    const vboActionLoadRoomRates = (from_date, to_date, room_ids) => {
        let ctx_elem = document
            .querySelector('.vbo-context-menu-overview-actions');

        let lbl_elem = ctx_elem
            .querySelector('.vbo-context-menu-lbl');

        let orig_lbl = lbl_elem.innerText;

        // start loading animation
        ctx_elem.loading = 1;
        lbl_elem.innerHTML = '<?php VikBookingIcons::e('circle-notch', 'fa-spin fa-fw'); ?>';

        // make the request
        VBOCore.doAjax(
            "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=pricing.loadRoomRates'); ?>",
            {
                room_ids: room_ids,
                from_date: from_date,
                to_date: to_date,
                restrictions: true,
            },
            (resp) => {
                try {
                    // decode the response (if needed), and append the content to the modal body
                    resp = typeof resp === 'string' ? JSON.parse(resp) : resp;

                    // hide any previously loaded room rate row, if any
                    vboActionHideRoomRates();

                    // get the date object for today at midnight as limit for past dates
                    let todayMidnight = new Date().setHours(0, 0, 0, 0);

                    // populate rates for all listings by scanning all month tables
                    document
                        .querySelectorAll('table.vboverviewtable[data-month-from]')
                        // iterate months
                        .forEach((month) => {
                            month
                                .querySelectorAll('.roomname[data-roomid]')
                                // iterate month listings
                                .forEach((roomRowCell) => {
                                    if (roomRowCell.matches('.subroomname')) {
                                        // skip sub-unit rows
                                        return;
                                    }

                                    // get current listing ID
                                    let listingId = roomRowCell.getAttribute('data-roomid');

                                    if (!resp.hasOwnProperty(listingId)) {
                                        // no rates returned for this room id
                                        return;
                                    }

                                    // access the "parent row"
                                    let parentRow = roomRowCell.closest('tr');

                                    // check whether the listing is a multi-unit room-type
                                    let isMultiUnit = (roomRowCell.getAttribute('data-units') || 1) > 1;

                                    // build room-rates row for the current listing
                                    let ratesRow = document.createElement('tr');
                                    ratesRow.classList.add('vbo-roomrates-row');
                                    ratesRow.setAttribute('data-roomid', listingId);

                                    // declare flag for rate plan id
                                    let ratePlanId = '';

                                    // build first row-cell and append it to the row
                                    let ratesCell = document.createElement('td');
                                    ratesCell.classList.add('vbo-roomrates-cell-first');
                                    ratesCell.innerHTML = '<?php VikBookingIcons::e('calculator'); ?>';
                                    let cellNameEl = document.createElement('span');
                                    cellNameEl.classList.add('vbo-roomrates-cell-name');
                                    cellNameEl.innerText = <?php echo json_encode(JText::translate('VBO_RATES_AND_RESTR')); ?>;
                                    ratesCell.append(cellNameEl);
                                    ratesRow.append(ratesCell);

                                    // iterate over the "parent row" day cells of the current listing and month
                                    parentRow
                                        .querySelectorAll('td.vbo-grid-avcell[data-day]')
                                        // iterate month listing days
                                        .forEach((roomRowDay) => {
                                            // get the day Y-m-d value
                                            let ymd = roomRowDay.getAttribute('data-day');

                                            // check whether the day is in the past
                                            let isPast = todayMidnight > new Date(ymd);

                                            // build month-day cell with pricing details
                                            let rateDayCell = document.createElement(isMultiUnit ? 'td' : 'span');
                                            // differentiate a multi-unit room row with independent cell from a single-unit cell on main availability row
                                            rateDayCell.classList.add(isMultiUnit ? 'vbo-roomrates-cell-day' : 'vbo-grid-cell-rate');
                                            if (isMultiUnit) {
                                                // set data attribute to independent cell
                                                rateDayCell.setAttribute('data-day', ymd);
                                                // add the same class to independent cell to identify a rate-cell
                                                rateDayCell.classList.add('vbo-grid-avcell-rates');
                                                // check if it's a past date
                                                if (isPast) {
                                                    rateDayCell.setAttribute('data-ispast', '1');
                                                }
                                            } else {
                                                // add class to parent cell that will be containing the rates
                                                roomRowDay.classList.add('vbo-grid-avcell-rates');
                                                // check if it's a past date
                                                if (isPast) {
                                                    roomRowDay.setAttribute('data-ispast', '1');
                                                }
                                            }

                                            // access the pricing information for this day
                                            if (resp[listingId].hasOwnProperty(ymd)) {
                                                if (!ratePlanId) {
                                                    // set rate plan id value-flag for data attribute on main rate row
                                                    ratePlanId = resp[listingId][ymd].idprice;
                                                    ratesRow.setAttribute('data-rateid', ratePlanId);
                                                    // update global rate plans map
                                                    vboActionRoomRplansMap[listingId] = resp[listingId][ymd].idprice;
                                                }

                                                // build rate amount element
                                                let rateAmountEl = document.createElement('span');
                                                rateAmountEl.classList.add('vbo-roomrates-cell-rate-amount');
                                                rateAmountEl.innerHTML = VBOCore.getCurrency().format(resp[listingId][ymd].cost);

                                                // append rate amount element to cell
                                                rateDayCell.append(rateAmountEl);

                                                if ((resp[listingId][ymd]?.restrictions?.minlos || 0) > 0) {
                                                    // build min-los element
                                                    let minLosEl = document.createElement('span');
                                                    minLosEl.classList.add('vbo-roomrates-cell-minlos');
                                                    minLosEl.innerHTML = '<?php VikBookingIcons::e('moon'); ?> ' + resp[listingId][ymd].restrictions.minlos;

                                                    // append min-los element to cell
                                                    rateDayCell.append(minLosEl);
                                                }
                                            }

                                            if (isMultiUnit) {
                                                // append rate cell to main rate row
                                                ratesRow.append(rateDayCell);
                                            } else if (roomRowDay.matches('.notbusy')) {
                                                // single-unit listing with free cell, append the rate information to the parent row, under the current cell
                                                roomRowDay.insertAdjacentElement('beforeend', rateDayCell);
                                            }
                                        });

                                    if (isMultiUnit) {
                                        // append the main rate row to the DOM
                                        parentRow.insertAdjacentElement('afterend', ratesRow);
                                    }
                                });
                        });

                    // iterating completed, stop loading
                    lbl_elem.innerHTML = '';
                    lbl_elem.innerText = orig_lbl;
                    ctx_elem.loading = 0;

                    // register events for the room-rate cells
                    vboActionRegisterRoomRateEvents();

                    // restore any previous temporary data for setting new rates within a queue
                    let previousQueue = VBOCore.getAdminDock().loadTemporaryData(
                        {
                            id: '_tmp',
                            persist_id: 'setnewrates',
                        },
                        (queueData) => {
                            // temporary data restored from dock
                            vboActionDisplayRatesQueue(queueData);
                        },
                        (queueData) => {
                            // temporary data removed from dock
                            document.querySelectorAll('.vbo-cell-pending-update').forEach((pending_cell) => {
                                pending_cell.classList.remove('vbo-cell-pending-update');
                            });
                        }
                    );

                    if (previousQueue && previousQueue?.data && Array.isArray(previousQueue?.data)) {
                        // ensure the cells of the previous queue will get the pending update class
                        let previousQueueData = previousQueue.data;
                        let todayMidnight = new Date().setHours(0, 0, 0, 0);

                        // get all month tables to find the requested listing-day cells
                        let monthTables = document
                            .querySelectorAll('table.vboverviewtable[data-month-from]');

                        // scan the previous request data objects
                        previousQueueData.forEach((prevData) => {
                            if (!prevData?.id_room || !prevData?.fromdate || !prevData?.todate) {
                                // invalid rates data format
                                return;
                            }

                            // pre-flight check to identify the row for the current listing
                            let listingRateRow = document.querySelector('.vbo-roomrates-row[data-roomid="' + prevData.id_room + '"]');
                            if (!listingRateRow) {
                                // may be a single-unit listing
                                listingRateRow = document.querySelector('.vboverviewtablerow[data-roomid="' + prevData.id_room + '"]');
                            }

                            if (!listingRateRow) {
                                // listing no longer in the page, regardless of the month
                                return;
                            }

                            // build the date objects
                            let iterDate = new Date(prevData.fromdate);
                            let end = new Date(prevData.todate);

                            // loop through the dates interval
                            while (iterDate <= end) {
                                if (iterDate < todayMidnight) {
                                    // go to next day by cloning the date object
                                    iterDate = new Date(iterDate.setDate(iterDate.getDate() + 1));

                                    // do not proceed with a date in the past
                                    continue;
                                }

                                // build the current day key in Y-m-d format
                                let day_key = VBOCore.formatDate(iterDate, 'Y-m-d');

                                // iterate over all months to identify the requested listing-day rate cell
                                let listingDayRateCell = null;
                                monthTables.forEach((monthTable) => {
                                    if (listingDayRateCell) {
                                        // desired cell was found already
                                        return;
                                    }

                                    // identify, again, the row for the current listing, but in the current month
                                    let listingRateRow = monthTable.querySelector('.vbo-roomrates-row[data-roomid="' + prevData.id_room + '"]');
                                    if (!listingRateRow) {
                                        // may be a single-unit listing
                                        listingRateRow = monthTable.querySelector('.vboverviewtablerow[data-roomid="' + prevData.id_room + '"]');
                                    }

                                    if (!listingRateRow) {
                                        // should not happen as it was found above during the pre-flight check
                                        return;
                                    }

                                    // query the desired cell
                                    listingDayRateCell = listingRateRow
                                        .querySelector('.vbo-grid-avcell-rates[data-day="' + day_key + '"]');
                                });

                                if (!listingDayRateCell) {
                                    // go to next day by cloning the date object
                                    iterDate = new Date(iterDate.setDate(iterDate.getDate() + 1));

                                    // listing cell date no longer in the document
                                    continue;
                                }

                                // find rate cell for single-unit listing
                                let dayRateCellAmount = listingDayRateCell.querySelector('.vbo-grid-cell-rate');
                                if (!dayRateCellAmount) {
                                    // must be a multi-unit listing with a dedicated cell for the rate
                                    dayRateCellAmount = listingDayRateCell;
                                }

                                if (dayRateCellAmount) {
                                    // restore the pending update class
                                    listingDayRateCell.classList.add('vbo-cell-pending-update');

                                    let rateAmountEl = listingDayRateCell.querySelector('.vbo-roomrates-cell-rate-amount');
                                    if (rateAmountEl) {
                                        // cell is not occupied by a reservation, update the rate from the previous queue
                                        rateAmountEl.innerHTML = VBOCore.getCurrency().format(prevData.rate);

                                        if (prevData.minlos) {
                                            // update minimum stay from the previous queue
                                            let minlosEl = listingDayRateCell.querySelector('.vbo-roomrates-cell-minlos');
                                            minlosEl.innerHTML = '<?php VikBookingIcons::e('moon'); ?> ' + prevData.minlos;
                                        }
                                    }
                                }

                                // go to next day by cloning the date object
                                iterDate = new Date(iterDate.setDate(iterDate.getDate() + 1));
                            }
                        });
                    }
                } catch (err) {
                    console.error('Error decoding the response', err, resp);
                }
            },
            (error) => {
                // display error message
                alert(error.responseText);

                // stop loading
                lbl_elem.innerHTML = '';
                lbl_elem.innerText = orig_lbl;
                ctx_elem.loading = 0;
            }
        );
    };

    /**
     * Register function to hide the room rates.
     */
    const vboActionHideRoomRates = () => {
        // hide rows for multi-unit room-types
        document
            .querySelectorAll('.vbo-roomrates-row')
            .forEach((row) => {
                row.remove();
            });

        // hide rate cells for single-unit listings
        document
            .querySelectorAll('.vbo-grid-cell-rate')
            .forEach((cell) => {
                cell.closest('td').classList.remove('vbo-grid-avcell-rates');
                cell.remove();
            });
    };

    /**
     * Build default object to handle the selection of the room rates.
     */
    const vboActionRoomRateData = {
        start: null,
        end: null,
        listingIdStart: null,
        listingIdEnd: null,
        traverseDir: null,
        listingIds: [],
    };

    /**
     * Register function to gather the list of involved room rate cells matrix for selection.
     * 
     * @param   Date    start           Date object for the first selection.
     * @param   Date    end             Date object for the last selection.
     * @param   number  listingStart    Listing ID for the first selection.
     * @param   number  listingEnd      Listing ID for the last selection.
     * 
     * @return  Array   Linear array of rows (listings) and list of dates.
     */
    const vboActionRoomRateGetCellsMatrix = (start, end, listingStart, listingEnd) => {
        let matrix = [];

        if (!start instanceof Date || !end instanceof Date) {
            // abort for invalid arguments
            return matrix;
        }

        // start the pool of listings involved in the selection
        let listingsInvolved = [listingStart];

        // start container for the first day-cell clicked on the initial listing
        let initialCell;

        // build the start day key in Y-m-d format
        let start_day_key = VBOCore.formatDate(start, 'Y-m-d');

        // identify the initial cell clicked
        initialCell = document
            .querySelector('.vbo-roomrates-row[data-roomid="' + listingStart + '"] .vbo-grid-avcell-rates[data-day="' + start_day_key + '"]');

        if (!initialCell) {
            // single-unit listings have a different row class
            initialCell = document
                .querySelector('.vboverviewtablerow[data-roomid="' + listingStart + '"] .vbo-grid-avcell-rates[data-day="' + start_day_key + '"]');
        }

        // check whether the selection affects multiple listings and eventually gather their IDs
        if (listingStart != listingEnd) {
            // traverse row elements heading toward the document end (down) to identify the landing listing ID row and all listings involved
            let nextSiblingRow = initialCell.closest('tr').nextElementSibling;
            while (nextSiblingRow != null) {
                if ((!nextSiblingRow.matches('tr.vbo-roomrates-row') && !nextSiblingRow.matches('tr.vboverviewtablerow')) || nextSiblingRow.matches('.vboverviewtablerow-subunit')) {
                    // invalid or sub-unit row
                    // go to the next listing row downwards
                    nextSiblingRow = nextSiblingRow.nextElementSibling;
                    continue;
                }

                let currentListing = nextSiblingRow.getAttribute('data-roomid');

                if (!currentListing || isNaN(currentListing)) {
                    // invalid row
                    // go to the next listing row downwards
                    nextSiblingRow = nextSiblingRow.nextElementSibling;
                    continue;
                }

                // push listing ID involved
                listingsInvolved.push(currentListing);

                if (currentListing == listingEnd) {
                    // all rows found
                    vboActionRoomRateData.traverseDir = 'down';
                    break;
                }

                // go to the next listing row downwards
                nextSiblingRow = nextSiblingRow.nextElementSibling;
            }

            // check if the end of the selection was made upwards
            if (!listingsInvolved.includes(listingEnd)) {
                // reset pool of listings involved
                listingsInvolved = [listingStart];

                // traverse row elements heading toward the document root (up) to identify the landing listing ID row and all listings involved
                let prevSiblingRow = initialCell.closest('tr').previousElementSibling;
                while (prevSiblingRow != null) {
                    if ((!prevSiblingRow.matches('tr.vbo-roomrates-row') && !prevSiblingRow.matches('tr.vboverviewtablerow')) || prevSiblingRow.matches('.vboverviewtablerow-subunit')) {
                        // invalid or sub-unit row
                        // go to the previous listing row upwards
                        prevSiblingRow = prevSiblingRow.previousElementSibling;
                        continue;
                    }

                    let currentListing = prevSiblingRow.getAttribute('data-roomid');

                    if (!currentListing || isNaN(currentListing)) {
                        // invalid row
                        // go to the previous listing row upwards
                        prevSiblingRow = prevSiblingRow.previousElementSibling;
                        continue;
                    }

                    // push listing ID involved
                    listingsInvolved.push(currentListing);

                    if (currentListing == listingEnd) {
                        // all rows found
                        vboActionRoomRateData.traverseDir = 'up';
                        break;
                    }

                    // go to the previous listing row upwards
                    prevSiblingRow = prevSiblingRow.previousElementSibling;
                }
            }

            if (!listingsInvolved.includes(listingEnd)) {
                // could not identify the landing row, neither by traversing the document downwards, nor upwards
                // reset the pool of listings involved
                listingsInvolved = [listingStart];
            }
        }

        // update listing IDs involved
        vboActionRoomRateData.listingIds = listingsInvolved;

        // iterate all listing rows (IDs) involved to build the matrix of rows and cells selected
        listingsInvolved.forEach((listingId) => {
            // start matrix row for the current listing
            let listingCells = [];

            // clone the start date object
            let iterDate = new Date(start);

            // loop through the dates interval
            while (iterDate <= end) {
                // build the current day key in Y-m-d format
                let day_key = VBOCore.formatDate(iterDate, 'Y-m-d');

                // query the desired cell
                let listingDayRateCell = document
                    .querySelector('.vbo-roomrates-row[data-roomid="' + listingId + '"] .vbo-grid-avcell-rates[data-day="' + day_key + '"]');

                if (!listingDayRateCell) {
                    // single-unit listings have a different row class
                    listingDayRateCell = document
                        .querySelector('.vboverviewtablerow[data-roomid="' + listingId + '"] .vbo-grid-avcell-rates[data-day="' + day_key + '"]');
                }

                if (listingDayRateCell) {
                    // push listing cell
                    listingCells.push(listingDayRateCell);
                }

                // go to next day by cloning again the date object
                iterDate = new Date(iterDate.setDate(iterDate.getDate() + 1));
            }

            if (listingCells.length) {
                // push row to matrix for the current listing
                matrix.push(listingCells);
            }
        });

        return matrix;
    };

    /**
     * Register function to handle the click event on a room rate cell.
     */
    const vboActionRoomRateHandleClick = (cell, event) => {
        if (vboActionRoomRateData.end) {
            // selection terminated
            return;
        }

        let day = cell.getAttribute('data-day'), listingId;

        if (!day) {
            return;
        }

        if (cell.matches('.vbo-roomrates-cell-day')) {
            // multi-unit room row cell
            listingId = cell.closest('tr.vbo-roomrates-row').getAttribute('data-roomid');
        } else {
            // single-unit room row cell
            listingId = cell.closest('tr').querySelector('td.roomname').getAttribute('data-roomid');
        }

        if (!listingId) {
            return;
        }

        if (!vboActionRoomRateData.start) {
            // start selection
            let startDate = new Date(day);
            let todayMidnight = new Date().setHours(0, 0, 0, 0);

            if (todayMidnight > startDate) {
                // starting a selection on a past date is forbidden
                return;
            }

            // populate values to start the selection
            vboActionRoomRateData.start = startDate;
            vboActionRoomRateData.listingIdStart = listingId;

            // set cell class
            cell.classList.add('vbo-cell-selected');
            cell.classList.add('vbo-cell-selected-first');

            // abort
            return;
        }

        let endDate = new Date(day);

        if (endDate < vboActionRoomRateData.start) {
            // date in the past clicked, reset selection
            vboActionRoomRateHandleReset();

            // start selection
            vboActionRoomRateData.start = endDate;

            // set cell class
            cell.classList.add('vbo-cell-selected');
            cell.classList.add('vbo-cell-selected-first');

            // abort
            return;
        }

        // stop event propagation
        if (event) {
            event.preventDefault();
            event.stopPropagation();
        }

        // terminate the selection
        vboActionRoomRateData.end = endDate;
        vboActionRoomRateData.listingIdEnd = listingId;
        cell.classList.add('vbo-cell-selected');
        cell.classList.add('vbo-cell-selected-last');

        // obtain the listings involved
        let listingsInvolved = [];
        let involvedCellsMatrix = vboActionRoomRateGetCellsMatrix(vboActionRoomRateData.start, vboActionRoomRateData.end, vboActionRoomRateData.listingIdStart, vboActionRoomRateData.listingIdEnd);
        // iterate over all the involved cells to apply the selected class
        involvedCellsMatrix.forEach((listingCells, rowIndex) => {
            // push listing ID involved
            listingsInvolved.push(listingCells[0].closest('tr').getAttribute('data-roomid'));
        });

        // define the modal cancel button
        let cancel_btn = jQuery('<button></button>')
            .attr('type', 'button')
            .addClass('btn')
            .text(<?php echo json_encode(JText::translate('VBANNULLA')); ?>)
            .on('click', () => {
                VBOCore.emitEvent('vbo-overv-setnewrates-dismiss');
            });

        // define the modal apply buttons content
        let apply_btn = jQuery('<button></button>')
            .attr('type', 'button')
            .addClass('btn')
            .addClass('btn-primary')
            .text(<?php echo json_encode(JText::translate('VBAPPLY')); ?>)
            .on('click', () => {
                // apply new rates
                vboActionApplyNewRates();
            });

        let actions_btn = jQuery('<button></button>')
            .attr('type', 'button')
            .addClass('btn')
            .addClass('btn-primary')
            .html('<?php VikBookingIcons::e('ellipsis-h'); ?>');

        // define the context menu for the actions button
        jQuery(actions_btn).vboContextMenu({
            placement: 'top-left',
            buttons: [
                {
                    class: 'vbo-context-menu-entry-secondary',
                    text: <?php echo json_encode(JText::translate('VBAPPLY')); ?>,
                    icon: '<?php echo VikBookingIcons::i('rocket'); ?>',
                    separator: true,
                    action: (root, event) => {
                        // apply new rates
                        vboActionApplyNewRates();
                    },
                },
                {
                    class: 'vbo-context-menu-entry-secondary',
                    text: <?php echo json_encode(JText::translate('VBO_ADD_TO_QUEUE')); ?>,
                    icon: '<?php echo VikBookingIcons::i('stopwatch'); ?>',
                    action: (root, event) => {
                        // queue the rates update request
                        vboActionQueueRatesUpdate();
                    },
                },
            ],
        });

        let apply_btn_content = jQuery('<div></div>')
            .addClass('btn-group')
            .addClass('vbo-context-menu-btn-group')
            .append(apply_btn)
            .append(actions_btn);

        // populate OTA relations and operation details
        vboActionOvervSetAllRoomRelations(listingsInvolved);

        // check if restrictions can be managed
        vboActionVcmRestrictionsSupported();

        // handle modal display with a small delay
        setTimeout(() => {
            // display modal
            let modalBody = VBOCore.displayModal({
                suffix:         'overv_setnewrates_modal',
                title:          <?php echo json_encode(JText::translate('VBRATESOVWSETNEWRATE')); ?>,
                extra_class:    'vbo-modal-rounded vbo-modal-tall',
                body_prepend:   true,
                lock_scroll:    true,
                draggable:      true,
                footer_left:    cancel_btn,
                footer_right:   apply_btn_content,
                loading_event:  'vbo-overv-setnewrates-loading',
                dismiss_event:  'vbo-overv-setnewrates-dismiss',
                progress_event: 'vbo-overv-setnewrates-progress',
                loading_body:  '<?php VikBookingIcons::e('refresh', 'fa-spin fa-3x fa-fw'); ?>',
                onDismiss:      () => {
                    // reset dates selection
                    vboActionRoomRateHandleReset();

                    // reset room-ota relations
                    jQuery('.vbo-roverw-setnewrate-vcm-otas').html('');

                    // move the element back to its original position
                    jQuery('.vbo-overview-action-raterestr-wrap').appendTo('.vbo-overview-action-raterestr-helper');
                },
            });

            jQuery('.vbo-overview-action-raterestr-wrap').appendTo(modalBody);
        }, 100);
    };

    /**
     * Register function to handle the mouseover event on a room rate cell.
     */
    const vboActionRoomRateHandleHover = (cell) => {
        if (!vboActionRoomRateData.start || vboActionRoomRateData.end) {
            // abort when the selection has not been started or has been terminated
            return;
        }

        let day = cell.getAttribute('data-day'), listingId;

        if (!day) {
            return;
        }

        if (cell.matches('.vbo-roomrates-cell-day')) {
            // multi-unit room row cell
            listingId = cell.closest('tr.vbo-roomrates-row').getAttribute('data-roomid');
        } else {
            // single-unit room row cell
            listingId = cell.closest('tr').querySelector('td.roomname').getAttribute('data-roomid');
        }

        if (!listingId) {
            return;
        }

        let dayDate = new Date(day);

        // always delete the selected class from any middle-cell
        document
            .querySelectorAll('.vbo-cell-selected')
            .forEach((prevCell) => {
                // remove the selected class from any middle-cell
                prevCell.classList.remove('vbo-cell-selected');
                prevCell.classList.remove('vbo-cell-selected-last');
                prevCell.classList.remove('vbo-cell-selected-middle-row-down');
                prevCell.classList.remove('vbo-cell-selected-middle-row-up');
                if (!prevCell.classList.contains('vbo-cell-selected-initial')) {
                    prevCell.classList.remove('vbo-cell-selected-first');
                }
            });

        if (dayDate >= vboActionRoomRateData.start) {
            // date in the future hovered
            let involvedCellsMatrix = vboActionRoomRateGetCellsMatrix(vboActionRoomRateData.start, dayDate, vboActionRoomRateData.listingIdStart, listingId);
            // iterate over all the involved cells to apply the selected class
            involvedCellsMatrix.forEach((listingCells, rowIndex) => {
                listingCells.forEach((selectedCell, cellIndex) => {
                    selectedCell.classList.add('vbo-cell-selected');
                    if (!rowIndex && !cellIndex) {
                        // initial cell identified
                        selectedCell.classList.add('vbo-cell-selected-initial');
                    }
                    if (rowIndex > 0 && vboActionRoomRateData.traverseDir) {
                        // middle row-cell, check if downwards or upwards
                        selectedCell.classList.add('vbo-cell-selected-middle-row-' + vboActionRoomRateData.traverseDir);
                    }
                    if (cellIndex == 0) {
                        // first row-cell
                        selectedCell.classList.add('vbo-cell-selected-first');
                    } else if (++cellIndex == listingCells.length) {
                        // last row-cell
                        selectedCell.classList.add('vbo-cell-selected-last');
                    }
                });
            });
        } else {
            // date in the past hovered, remove the rest of the classes
            document
                .querySelectorAll('.vbo-cell-selected-initial')
                .forEach((prevCell) => {
                    // remove the selected class from the initial cell
                    prevCell.classList.remove('vbo-cell-selected-initial');
                    prevCell.classList.remove('vbo-cell-selected-first');
                });
        }
    };

    /**
     * Register function to handle the reset of the room rate cell selections.
     */
    const vboActionRoomRateHandleReset = () => {
        // reset values
        vboActionRoomRateData.start = null;
        vboActionRoomRateData.end = null;
        vboActionRoomRateData.listingIdStart = null;
        vboActionRoomRateData.listingIdEnd = null;
        vboActionRoomRateData.traverseDir = null;
        vboActionRoomRateData.listingIds = [];

        // remove room-rate selected class from cells
        document
            .querySelectorAll('.vbo-cell-selected')
            .forEach((cell) => {
                cell.classList.remove('vbo-cell-selected');
                cell.classList.remove('vbo-cell-selected-first');
                cell.classList.remove('vbo-cell-selected-last');
                cell.classList.remove('vbo-cell-selected-initial');
                cell.classList.remove('vbo-cell-selected-middle-row-down');
                cell.classList.remove('vbo-cell-selected-middle-row-up');
            });
    };

    /**
     * Register function to add event listeners for the room rate cells.
     */
    const vboActionRegisterRoomRateEvents = () => {
        // scan all rate cells
        document
            .querySelectorAll('.vbo-grid-avcell-rates')
            .forEach((rateCell) => {
                // click listener
                rateCell.addEventListener('click', (e) => {
                    if (!e || !e.target) {
                        return;
                    }

                    let element = e.target;

                    if (!element.matches('.vbo-grid-avcell-rates')) {
                        element = element.closest('.vbo-grid-avcell-rates');
                    }

                    vboActionRoomRateHandleClick(element, e);
                });

                // mouseover listener
                rateCell.addEventListener('mouseover', (e) => {
                    if (!e || !e.target) {
                        return;
                    }

                    let element = e.target;

                    if (!element.matches('.vbo-grid-avcell-rates')) {
                        element = element.closest('.vbo-grid-avcell-rates');
                    }

                    vboActionRoomRateHandleHover(element);
                });
            });
    };

    /**
     * Register function to render the CM operation results into HTML.
     * 
     * @param   object  vcm_response    The raw "setnewrates" endpoint response key "vcm".
     * @param   string  listingName     The name of the listing involved.
     * 
     * @return  string
     */
    const vboActionRenderCMResult = (vcm_response, listingName) => {
        if (!Array.isArray(vcm_response)) {
            // make sure the result is a list of result objects
            vcm_response = [vcm_response];
        }

        let htmlres = '';

        vcm_response.forEach((obj) => {
            htmlres += '<div class="vbo-vcm-rates-res-rplan-wrap">';

            if (obj.hasOwnProperty('rplan_name')) {
                htmlres += '<div class="vbo-vcm-rates-res-rplan-data">';
                htmlres += '<strong>' + listingName + ' - ' + obj['rplan_name'] + '</strong>';
                if (obj.hasOwnProperty('is_derived') && obj['is_derived']) {
                    htmlres += ' <span class="label label-info">' + <?php echo json_encode(JText::translate('VBO_IS_DERIVED_RATE')); ?> + '</span>';
                }
                htmlres += '</div>';
            }
            
            if (obj.hasOwnProperty('channels_success')) {
                htmlres += '<div class="vbo-vcm-rates-res-success">';
                for (let ch_id in obj['channels_success']) {
                    htmlres += '<div class="vbo-vcm-rates-res-channel">';
                    htmlres += '    <div class="vbo-vcm-rates-res-channel-esit">';
                    htmlres += '        <i class="<?php echo VikBookingIcons::i('check'); ?>"></i>';
                    htmlres += '    </div>';
                    htmlres += '    <div class="vbo-vcm-rates-res-channel-logo">';
                    if (obj['channels_updated'].hasOwnProperty(ch_id) && obj['channels_updated'][ch_id]['logo'].length) {
                        htmlres += '<img src="'+obj['channels_updated'][ch_id]['logo']+'" />';
                    } else {
                        htmlres += '<span>'+obj['channels_success'][ch_id]+'</span>';
                    }
                    htmlres += '    </div>';
                    htmlres += '</div>';
                }

                if (obj.hasOwnProperty('channels_bkdown')) {
                    htmlres += '<div class="vbo-vcm-rates-res-bkdown">';
                    htmlres += '    <div><pre>'+obj['channels_bkdown']+'</pre></div>';
                    htmlres += '</div>';
                }
                htmlres += '</div>';
            }

            if (obj.hasOwnProperty('channels_warnings')) {
                htmlres += '<div class="vbo-vcm-rates-res-warning">';
                for (let ch_id in obj['channels_warnings']) {
                    htmlres += '<div class="vbo-vcm-rates-res-channel">';
                    htmlres += '    <div class="vbo-vcm-rates-res-channel-esit">';
                    htmlres += '        <i class="<?php echo VikBookingIcons::i('exclamation-triangle'); ?>"></i>';
                    htmlres += '    </div>';
                    htmlres += '    <div class="vbo-vcm-rates-res-channel-logo">';
                    if (obj['channels_updated'].hasOwnProperty(ch_id) && obj['channels_updated'][ch_id]['logo'].length) {
                        htmlres += '<img src="'+obj['channels_updated'][ch_id]['logo']+'" />';
                    } else if (obj['channels_updated'].hasOwnProperty(ch_id)) {
                        htmlres += '<span>'+obj['channels_updated'][ch_id]['name']+'</span>';
                    }
                    htmlres += '    </div>';
                    htmlres += '    <div class="vbo-vcm-rates-res-channel-det">';
                    htmlres += '        <pre>'+obj['channels_warnings'][ch_id]+'</pre>';
                    htmlres += '    </div>';
                    htmlres += '</div>';
                }
                htmlres += '</div>';
            }

            if (obj.hasOwnProperty('channels_errors')) {
                htmlres += '<div class="vbo-vcm-rates-res-error">';
                for (let ch_id in obj['channels_errors']) {
                    htmlres += '<div class="vbo-vcm-rates-res-channel">';
                    htmlres += '    <div class="vbo-vcm-rates-res-channel-esit">';
                    htmlres += '        <i class="<?php echo VikBookingIcons::i('times'); ?>"></i>';
                    htmlres += '    </div>';
                    htmlres += '    <div class="vbo-vcm-rates-res-channel-logo">';
                    if (obj['channels_updated'].hasOwnProperty(ch_id) && obj['channels_updated'][ch_id]['logo'].length) {
                        htmlres += '    <img src="'+obj['channels_updated'][ch_id]['logo']+'" />';
                    } else if (obj['channels_updated'].hasOwnProperty(ch_id)) {
                        htmlres += '    <span>'+obj['channels_updated'][ch_id]['name']+'</span>';
                    }
                    htmlres += '    </div>';
                    htmlres += '    <div class="vbo-vcm-rates-res-channel-det">';
                    htmlres += '        <pre>'+obj['channels_errors'][ch_id]+'</pre>';
                    htmlres += '    </div>';
                    htmlres += '</div>';
                }
                htmlres += '</div>';
            }

            htmlres += '</div>';
        });

        return htmlres;
    };

    /**
     * Register function to apply new rates after a matrix selection.
     */
    const vboActionApplyNewRates = () => {
        let involvedCellsMatrix = vboActionRoomRateGetCellsMatrix(vboActionRoomRateData.start, vboActionRoomRateData.end, vboActionRoomRateData.listingIdStart, vboActionRoomRateData.listingIdEnd);
        let listingsInvolved = [];
        involvedCellsMatrix.forEach((listingCells, rowIndex) => {
            // push listing ID involved
            listingsInvolved.push(listingCells[0].closest('tr').getAttribute('data-roomid'));
        });

        let setrate = parseFloat(document.querySelector('#roverw-newrate').value);
        let setminlos = document.querySelector('#roverw-newrestr').value;
        let invoke_vcm = document.querySelector('input[name="roverw-newrate-vcm"]:checked') ? 1 : 0;

        if (!listingsInvolved.length) {
            alert('No listings to update.');
            return;
        }

        if (isNaN(setrate) || setrate <= 0) {
            alert('Invalid rate amount.');
            return;
        }

        // check the OTA pricing alteration rules, if any
        let ota_pricing = {};
        if (invoke_vcm) {
            // scan all OTA alteration rules, if any
            document.querySelectorAll('.vbo-roverw-setnewrate-ota-pricing-currentvalue[data-alteration]').forEach((elem) => {
                // channel alteration string
                let alter_string = elem.getAttribute('data-alteration');
                if (!alter_string) {
                    alter_string = '';
                }

                // access the parent node to get the OTA channel identifier
                let ota_id = elem
                    .closest('.vbo-roverw-setnewrate-vcm-ota-relation-channel[data-otaid]')
                    .getAttribute('data-otaid');

                if (!ota_id || !alter_string || alter_string == '+0%' || alter_string == '+0*') {
                    // avoid pushing an empty alteration command
                    return;
                }

                // push OTA pricing alteration command
                ota_pricing[ota_id] = alter_string;
            });
        }

        if (!Object.keys(ota_pricing).length) {
            // unset the object for the request
            ota_pricing = null;
        }

        // build the list of update requests (one per listing)
        let requestList = [];
        listingsInvolved.forEach((listingId) => {
            requestList.push({
                id_room: listingId,
                id_price: vboActionRoomRplansMap[listingId],
                rate: setrate,
                vcm: invoke_vcm,
                minlos: setminlos,
                fromdate: VBOCore.formatDate(vboActionRoomRateData.start, 'Y-m-d'),
                todate: VBOCore.formatDate(vboActionRoomRateData.end, 'Y-m-d'),
                rateclosed: 0,
                ota_pricing: ota_pricing,
                skip_derived: 0,
            });
        });

        // count the number of requests
        let requestCount = requestList.length;

        // start the response container for every request
        let responseContainer = [];

        // start loading
        VBOCore.emitEvent('vbo-overv-setnewrates-loading');

        // set progress
        VBOCore.emitEvent('vbo-overv-setnewrates-progress', {
            progress_content: '1 / ' + requestCount,
        });

        // dispatch the rates update requests
        vboActionDispatchRatesUpdateRequests(
            requestList,
            (obj_res, listingName) => {
                // one request was completed, push the result
                responseContainer.push(Object.assign(obj_res, {listingName: listingName}));

                // update progress
                VBOCore.emitEvent('vbo-overv-setnewrates-progress', {
                    progress_content: (requestList.length ? (requestCount - requestList.length + 1) : requestCount) + ' / ' + requestCount,
                });
            },
            () => {
                // process completed
                let showOTAResponse = false;
                let htmlOTAResponse = [];

                // iterate the operation results
                responseContainer.forEach((response) => {
                    if (typeof response !== 'object' || !response.hasOwnProperty('vcm')) {
                        // nothing to say about this response
                        return;
                    }

                    // turn flag on
                    showOTAResponse = true;

                    // build HTML response string and add it to the list
                    htmlOTAResponse.push(vboActionRenderCMResult(response.vcm, response.listingName));
                });

                if (showOTAResponse && htmlOTAResponse.length) {
                    // display the modal with the update operation results
                    VBOCore.displayModal({
                        suffix:      'vbo-vcm-rates-res',
                        extra_class: 'vbo-modal-rounded vbo-modal-tall vbo-modal-nofooter',
                        title:       <?php echo json_encode(JText::translate('VBOVCMRATESRES')); ?>,
                        body:        '<div class="vbo-vcm-rates-res-container' + (htmlOTAResponse.length > 1 ? ' vbo-vcm-ota-multicalendar-response' : '') + '">' + htmlOTAResponse.join("\n") + '</div>',
                        draggable:   true,
                    });
                }

                // update new rates on the involved cells
                involvedCellsMatrix.forEach((listingCells, rowIndex) => {
                    listingCells.forEach((selectedCell, cellIndex) => {
                        let rateAmountEl = selectedCell.querySelector('.vbo-roomrates-cell-rate-amount');
                        if (!rateAmountEl) {
                            // cell is occupied by a reservation, nothing to update
                            return;
                        }
                        rateAmountEl.innerHTML = VBOCore.getCurrency().format(setrate);
                        if (setminlos) {
                            let minlosEl = selectedCell.querySelector('.vbo-roomrates-cell-minlos');
                            minlosEl.innerHTML = '<?php VikBookingIcons::e('moon'); ?> ' + setminlos;
                        }
                        selectedCell.classList.add('vbo-cell-new-update');
                    });
                });

                // dismiss the modal upon completion, after having updated the new rates on the involved cells
                VBOCore.emitEvent('vbo-overv-setnewrates-dismiss');
            },
            (err_mess, unrecoverable) => {
                // stop loading in case of error
                VBOCore.emitEvent('vbo-overv-setnewrates-loading');
            }
        );
    };

    /**
     * Register function to queue a rates update request.
     */
    const vboActionQueueRatesUpdate = () => {
        let involvedCellsMatrix = vboActionRoomRateGetCellsMatrix(vboActionRoomRateData.start, vboActionRoomRateData.end, vboActionRoomRateData.listingIdStart, vboActionRoomRateData.listingIdEnd);
        let listingsInvolved = [];
        involvedCellsMatrix.forEach((listingCells, rowIndex) => {
            // push listing ID involved
            listingsInvolved.push(listingCells[0].closest('tr').getAttribute('data-roomid'));
        });

        let setrate = parseFloat(document.querySelector('#roverw-newrate').value);
        let setminlos = document.querySelector('#roverw-newrestr').value;
        let invoke_vcm = document.querySelector('input[name="roverw-newrate-vcm"]:checked') ? 1 : 0;

        if (!listingsInvolved.length) {
            alert('No listings to update.');
            return;
        }

        if (isNaN(setrate) || setrate <= 0) {
            alert('Invalid rate amount.');
            return;
        }

        // check the OTA pricing alteration rules, if any
        let ota_pricing = {};
        if (invoke_vcm) {
            // scan all OTA alteration rules, if any
            document.querySelectorAll('.vbo-roverw-setnewrate-ota-pricing-currentvalue[data-alteration]').forEach((elem) => {
                // channel alteration string
                let alter_string = elem.getAttribute('data-alteration');
                if (!alter_string) {
                    alter_string = '';
                }

                // access the parent node to get the OTA channel identifier
                let ota_id = elem
                    .closest('.vbo-roverw-setnewrate-vcm-ota-relation-channel[data-otaid]')
                    .getAttribute('data-otaid');

                if (!ota_id || !alter_string || alter_string == '+0%' || alter_string == '+0*') {
                    // avoid pushing an empty alteration command
                    return;
                }

                // push OTA pricing alteration command
                ota_pricing[ota_id] = alter_string;
            });
        }

        if (!Object.keys(ota_pricing).length) {
            // unset the object for the request
            ota_pricing = null;
        }

        // build the list of update requests (one per listing)
        let requestList = [];
        listingsInvolved.forEach((listingId) => {
            requestList.push({
                id_room: listingId,
                id_price: vboActionRoomRplansMap[listingId],
                _roomName: vboActionRoomNames[listingId],
                rate: setrate,
                vcm: invoke_vcm,
                minlos: setminlos,
                fromdate: VBOCore.formatDate(vboActionRoomRateData.start, 'Y-m-d'),
                todate: VBOCore.formatDate(vboActionRoomRateData.end, 'Y-m-d'),
                rateclosed: 0,
                ota_pricing: ota_pricing,
                skip_derived: 0,
            });
        });

        // queue the current requests to the admin-dock as temporary data
        VBOCore.getAdminDock().addTemporaryData(
            {
                id: '_tmp',
                persist_id: 'setnewrates',
                name: <?php echo json_encode(JText::translate('VBO_PENDING_QUEUE')); ?>,
                icon: '<?php VikBookingIcons::e('stopwatch'); ?>',
                style: 'orange',
            },
            requestList,
            (queueData) => {
                // temporary data restored from dock
                vboActionDisplayRatesQueue(queueData);
            },
            (queueData) => {
                // temporary data removed from dock
                document.querySelectorAll('.vbo-cell-pending-update').forEach((pending_cell) => {
                    pending_cell.classList.remove('vbo-cell-pending-update');
                });
            }
        );

        // update new rates on the involved cells
        involvedCellsMatrix.forEach((listingCells, rowIndex) => {
            listingCells.forEach((selectedCell, cellIndex) => {
                let rateAmountEl = selectedCell.querySelector('.vbo-roomrates-cell-rate-amount');
                if (!rateAmountEl) {
                    // cell is occupied by a reservation, nothing to update
                    return;
                }
                rateAmountEl.innerHTML = VBOCore.getCurrency().format(setrate);
                if (setminlos) {
                    let minlosEl = selectedCell.querySelector('.vbo-roomrates-cell-minlos');
                    minlosEl.innerHTML = '<?php VikBookingIcons::e('moon'); ?> ' + setminlos;
                }
                selectedCell.classList.add('vbo-cell-pending-update');
            });
        });

        // dismiss the modal upon completion, after having updated the new rates on the involved cells
        VBOCore.emitEvent('vbo-overv-setnewrates-dismiss');
    };

    /**
     * Displays the information about the current rates queue data.
     * 
     * @param   Array   ratesData   The current queue data.
     */
    const vboActionDisplayRatesQueue = (ratesData) => {
        // define the modal cancel button
        let cancel_btn = jQuery('<button></button>')
            .attr('type', 'button')
            .addClass('btn')
            .text(<?php echo json_encode(JText::translate('VBANNULLA')); ?>)
            .on('click', () => {
                // minimize again the temporary data to the dock by dismissing the modal
                VBOCore.emitEvent('vbo-overv-setnewrates-queue-dismiss');
            });

        // define the modal apply button
        let apply_btn = jQuery('<button></button>')
            .attr('type', 'button')
            .addClass('btn btn-success')
            .html('<?php VikBookingIcons::e('rocket'); ?> ' + <?php echo json_encode(JText::translate('VBAPPLY')); ?>)
            .on('click', function() {
                // count the number of requests
                let requestCount = ratesData.length;

                if (!requestCount) {
                    throw new Error('Rates queue is empty');
                }

                // start the response container for every request
                let responseContainer = [];

                // start loading
                VBOCore.emitEvent('vbo-overv-setnewrates-queue-loading');

                // set progress
                VBOCore.emitEvent('vbo-overv-setnewrates-queue-progress', {
                    progress_content: '1 / ' + requestCount,
                });

                // dispatch the rates update requests
                vboActionDispatchRatesUpdateRequests(
                    ratesData,
                    (obj_res, listingName) => {
                        // one request was completed, push the result
                        responseContainer.push(Object.assign(obj_res, {listingName: listingName}));

                        // update progress
                        VBOCore.emitEvent('vbo-overv-setnewrates-queue-progress', {
                            progress_content: (ratesData.length ? (requestCount - ratesData.length + 1) : requestCount) + ' / ' + requestCount,
                        });
                    },
                    () => {
                        // process completed
                        let showOTAResponse = false;
                        let htmlOTAResponse = [];

                        // iterate the operation results
                        responseContainer.forEach((response) => {
                            if (typeof response !== 'object' || !response.hasOwnProperty('vcm')) {
                                // nothing to say about this response
                                return;
                            }

                            // turn flag on
                            showOTAResponse = true;

                            // build HTML response string and add it to the list
                            htmlOTAResponse.push(vboActionRenderCMResult(response.vcm, response.listingName));
                        });

                        if (showOTAResponse && htmlOTAResponse.length) {
                            // display the modal with the update operation results
                            VBOCore.displayModal({
                                suffix:      'vbo-vcm-rates-res',
                                extra_class: 'vbo-modal-rounded vbo-modal-tall vbo-modal-nofooter',
                                title:       <?php echo json_encode(JText::translate('VBOVCMRATESRES')); ?>,
                                body:        '<div class="vbo-vcm-rates-res-container' + (htmlOTAResponse.length > 1 ? ' vbo-vcm-ota-multicalendar-response' : '') + '">' + htmlOTAResponse.join("\n") + '</div>',
                                draggable:   true,
                            });
                        }

                        // update the involved cells
                        document.querySelectorAll('.vbo-grid-avcell-rates.vbo-cell-pending-update').forEach((pendingCell) => {
                            pendingCell.classList.remove('vbo-cell-pending-update');
                            pendingCell.classList.add('vbo-cell-new-update');
                        });

                        // dismiss the modal upon completion, after having updated the involved cells
                        VBOCore.emitEvent('vbo-overv-setnewrates-queue-dismiss');

                        // at last, dismiss the admin-dock entry upon completion
                        VBOCore.getAdminDock().removeDockElementById('_tmp');
                    },
                    (err_mess, unrecoverable) => {
                        // do nothing in case of error when inside a queue of requests
                    }
                );
            });

        // build modal body
        let modalBodyEl = document.createElement('div');
        modalBodyEl.classList.add('vbo-rates-queue-wrapper');
        ratesData.forEach((updateData) => {
            // create queue update container
            let updateEl = document.createElement('div');
            updateEl.classList.add('vbo-rates-queue-data');

            // contain the listing
            let updateRoomEl = document.createElement('span');
            updateRoomEl.classList.add('vbo-rates-queue-data-listing');
            updateRoomEl.innerText = vboActionRoomNames[updateData.id_room] || updateData._roomName || updateData.id_room;
            updateEl.append(updateRoomEl);

            // contain the dates
            let updateDatesEl = document.createElement('span');
            updateDatesEl.classList.add('vbo-rates-queue-data-dates');
            updateDatesEl.innerText = updateData.fromdate + ' - ' + updateData.todate;
            updateEl.append(updateDatesEl);

            // contain the rate
            let updateRatesEl = document.createElement('span');
            updateRatesEl.classList.add('vbo-rates-queue-data-rate');
            updateRatesEl.innerHTML = VBOCore.getCurrency().format(updateData.rate);
            updateEl.append(updateRatesEl);

            if (updateData.minlos) {
                // contain the minimum stay
                let updateMinlosEl = document.createElement('span');
                updateMinlosEl.classList.add('vbo-rates-queue-data-minlos');
                updateMinlosEl.innerHTML = '<?php VikBookingIcons::e('moon'); ?> ' + updateData.minlos;
                updateEl.append(updateMinlosEl);
            }

            // append queue update container
            modalBodyEl.append(updateEl);
        });

        // display modal
        VBOCore.displayModal({
            suffix:         'overv_setnewrates_queue_modal',
            title:          <?php echo json_encode(JText::translate('VBO_PENDING_QUEUE')); ?>,
            extra_class:    'vbo-modal-rounded vbo-modal-tall',
            body:           modalBodyEl,
            body_prepend:   true,
            lock_scroll:    true,
            draggable:      true,
            footer_left:    cancel_btn,
            footer_right:   apply_btn,
            loading_event:  'vbo-overv-setnewrates-queue-loading',
            dismiss_event:  'vbo-overv-setnewrates-queue-dismiss',
            progress_event: 'vbo-overv-setnewrates-queue-progress',
            loading_body:  '<?php VikBookingIcons::e('refresh', 'fa-spin fa-3x fa-fw'); ?>',
            onDismiss:      () => {
                if (!ratesData.length) {
                    // temporary data queue is empty
                    return;
                }

                // minimize again the temporary data to the dock
                VBOCore.getAdminDock().addTemporaryData(
                    {
                        id: '_tmp',
                        persist_id: 'setnewrates',
                        name: <?php echo json_encode(JText::translate('VBO_PENDING_QUEUE')); ?>,
                        icon: '<?php VikBookingIcons::e('stopwatch'); ?>',
                        style: 'orange',
                    },
                    ratesData,
                    (queueData) => {
                        // temporary data restored from dock
                        vboActionDisplayRatesQueue(queueData);
                    },
                    (queueData) => {
                        // temporary data removed from dock
                        document.querySelectorAll('.vbo-cell-pending-update').forEach((pending_cell) => {
                            pending_cell.classList.remove('vbo-cell-pending-update');
                        });
                    }
                );
            },
        });
    };

    /**
     * Register function to process a list of rates update requests one after the other.
     * 
     * @param   array       requests    List of rates update request objects.
     * @param   function    onProgress  Optional callback when a request is completed.
     * @param   function    onComplete  Optional callback when all requests have been completed.
     * @param   function    onError     Optional callback in case of request error.
     * 
     * @return  void
     */
    const vboActionDispatchRatesUpdateRequests = (requests, onProgress, onComplete, onError) => {
        if (!Array.isArray(requests) || !requests.length) {
            if (typeof onComplete === 'function') {
                onComplete();
            }

            // abort
            return;
        }

        // obtain the request to process
        const request = requests.shift();

        // perform the request
        VBOCore.doAjax(
            "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=pricing.setnewrates'); ?>",
            request,
            (res) => {
                if (typeof res === 'string' && res.indexOf('e4j.error') === 0) {
                    // display error
                    let err_mess = res.replace('e4j.error.', '');
                    alert(err_mess);

                    if (typeof onError === 'function') {
                        // unrecoverable error
                        onError(err_mess, true);
                    }

                    // abort
                    return;
                }

                try {
                    // check the response
                    let obj_res = typeof res === 'string' ? JSON.parse(res) : res;

                    if (typeof onProgress === 'function') {
                        // call the given function by passing the operation result object and the room name
                        onProgress(obj_res, (vboActionRoomNames[request.id_room] || request.id_room));
                    }

                    // recursively call the same function to process the next request
                    vboActionDispatchRatesUpdateRequests(requests, onProgress, onComplete, onError);
                } catch(err) {
                    // display error
                    alert(err);

                    if (typeof onError === 'function') {
                        // unrecoverable error
                        onError(err, true);
                    }
                }
            },
            (err) => {
                // display error
                let err_mess = err.responseText || 'Request failed due to connection error';
                alert(err_mess);

                if (typeof onError === 'function') {
                    // connection error
                    onError(err_mess, false);
                }
            }
        );
    };

    /**
     * Register function to populate the room ota relations upon selecting a matrix of listings + dates.
     */
    const vboActionOvervSetAllRoomRelations = (listingsInvolved) => {
        // build the current day key in Y-m-d format
        let start_day_key = VBOCore.formatDate(vboActionRoomRateData.start, 'Y-m-d');
        let end_day_key = VBOCore.formatDate(vboActionRoomRateData.end, 'Y-m-d');

        // TODO format dates according to settings

        let listingsText = listingsInvolved.length > 1 ? listingsInvolved.length + ' ' + <?php echo json_encode(JText::translate('VBO_LISTINGS')); ?> : (vboActionRoomNames[listingsInvolved[0]] || listingsInvolved[0]);

        // set operation details first
        jQuery('.vbo-overview-action-raterestr-listings').text(listingsText);
        jQuery('.vbo-overview-action-raterestr-dates').text(start_day_key + ' - ' + end_day_key);

        // the room-ota relations wrapper
        let wrapper = jQuery('.vbo-roverw-setnewrate-vcm-otas');

        // always empty the wrapper
        wrapper.html('');

        // store a list of parsed channel IDs
        let parsedChannelIds = [];

        listingsInvolved.forEach((room_id) => {
            let rplan_id = vboActionRoomRplansMap[room_id];
            if (!room_id || !rplan_id || !vboActionRoomOtaRels.hasOwnProperty(room_id)) {
                // nothing to render for the current listing
                return;
            }

            // start counter
            let ota_ch_counter = 0;

            // build and append room-OTA relations
            for (const ota_name in vboActionRoomOtaRels[room_id]['channels']) {
                // get the current channel ID
                let channel_id = vboActionRoomOtaRels[room_id]['accounts'][ota_ch_counter]['idchannel'];

                if (parsedChannelIds.includes(channel_id)) {
                    // do not display the same channel multiple times
                    return;
                }

                // push channel ID parsed
                parsedChannelIds.push(channel_id);

                // build ota readable name
                let ota_read_name = ota_name;
                ota_read_name = ota_read_name.replace(/api$/, '');
                ota_read_name = ota_read_name.replace(/^(google)(hotel|vr)$/i, '$1 $2');

                // build room-ota relation block and elements
                let ota_block = jQuery('<div></div>');
                ota_block.addClass('vbo-roverw-setnewrate-vcm-ota-relation');

                let ota_block_inner = jQuery('<div></div>');
                ota_block_inner
                    .addClass('vbo-roverw-setnewrate-vcm-ota-relation-pricing')
                    .attr('data-ota', (ota_name + '').toLowerCase());

                let ota_block_channel = jQuery('<div></div>');
                ota_block_channel
                    .addClass('vbo-roverw-setnewrate-vcm-ota-relation-channel')
                    .attr('data-otaid', vboActionRoomOtaRels[room_id]['accounts'][ota_ch_counter]['idchannel'])
                    .append('<img src="' + vboActionRoomOtaRels[room_id]['channels'][ota_name] + '" />')
                    .append('<span>' + ota_read_name + '</span>');

                let ota_pricing_value = jQuery('<span></span>');
                ota_pricing_value
                    .addClass('vbo-roverw-setnewrate-vcm-ota-pricing-startvalue')
                    .html('<?php VikBookingIcons::e('circle-notch', 'fa-spin fa-fw'); ?>')
                    .on('click', function() {
                        jQuery(this)
                            .closest('.vbo-roverw-setnewrate-vcm-ota-relation-pricing')
                            .find('.vbo-roverw-setnewrate-vcm-ota-channel-pricing')
                            .toggle();
                    });

                let ota_block_pricing = jQuery('<div></div>');
                ota_block_pricing
                    .addClass('vbo-roverw-setnewrate-vcm-ota-channel-pricing')
                    .css('display', 'none')
                    .append(jQuery('.vbo-roverw-setnewrate-vcm-ota-pricing-alteration').first().clone());

                // register "input" event for select/input elements to control the channel alteration rule overrides
                ota_block_pricing.find('select, input').on('input', function() {
                    let input_elem = jQuery(this);

                    // get the current channel alteration command
                    let ota_alteration_command = input_elem
                        .closest('.vbo-roverw-setnewrate-vcm-ota-relation-pricing')
                        .find('.vbo-roverw-setnewrate-ota-pricing-currentvalue[data-alteration]')
                        .attr('data-alteration');

                    // access alteration rule and input value
                    let rmod_type  = input_elem.attr('data-alter-rule');
                    let rmod_value = input_elem.val();

                    if (!ota_alteration_command || !rmod_type || !(rmod_value + '').length) {
                        return;
                    }

                    // check what pricing factor was changed
                    if (rmod_type == 'rmodsop') {
                        // increase or decrease rate
                        let command_old_val = ota_alteration_command.substr(0, 1);
                        let command_new_val = parseInt(rmod_value) == 1 ? '+' : '-';
                        ota_alteration_command = ota_alteration_command.replace(command_old_val, command_new_val);
                    } else if (rmod_type == 'rmodsamount') {
                        // amount
                        let command_op  = ota_alteration_command.substr(0, 1);
                        let command_val = ota_alteration_command.substr(-1, 1);
                        let command_old_val = ota_alteration_command.replace(command_op, '').replace(command_val, '');
                        let command_new_val = parseFloat(rmod_value);
                        ota_alteration_command = ota_alteration_command.replace(command_old_val, command_new_val);
                    } else if (rmod_type == 'rmodsval') {
                        // percent or absolute
                        let command_old_val = ota_alteration_command.substr(-1, 1);
                        let command_new_val = parseInt(rmod_value) == 1 ? '%' : '*';
                        ota_alteration_command = ota_alteration_command.replace(command_old_val, command_new_val);
                    }

                    // check if the channel requires a specific currency
                    let ota_currency_data = input_elem
                        .closest('.vbo-roverw-setnewrate-vcm-ota-relation-pricing')
                        .find('.vbo-roverw-setnewrate-ota-pricing-willvalue')
                        .attr('data-currency');
                    if (ota_currency_data) {
                        // decode currency data instructions
                        try {
                            ota_currency_data = JSON.parse(ota_currency_data);
                        } catch (e) {
                            ota_currency_data = {};
                        }
                    }

                    // define the current channel alteration string (readable)
                    let ota_alteration_string = ota_alteration_command;

                    // finalize the current channel alteration string (readable)
                    let ota_alteration_val = ota_alteration_string.substr(-1, 1);
                    if (ota_alteration_val != '%') {
                        ota_alteration_string = ota_alteration_string.replace(ota_alteration_val, '') + ((ota_currency_data && ota_currency_data?.symbol ? ota_currency_data.symbol : '') || <?php echo json_encode($currencysymb) ?: '"$"'; ?>);
                    }

                    // update the alteration rule command attribute
                    input_elem
                        .closest('.vbo-roverw-setnewrate-vcm-ota-relation-pricing')
                        .find('.vbo-roverw-setnewrate-ota-pricing-currentvalue[data-alteration]')
                        .attr('data-alteration', ota_alteration_command);

                    // update the alteration rule string tag text
                    input_elem
                        .closest('.vbo-roverw-setnewrate-vcm-ota-relation-pricing')
                        .find('.vbo-roverw-setnewrate-ota-pricing-currentvalue[data-alteration]')
                        .html(ota_alteration_string);

                    // get the current rate to set
                    let current_room_rate = jQuery('#roverw-newrate').val();
                    if (current_room_rate) {
                        // dispatch the event to trigger the re-calculation of the OTA rates
                        VBOCore.emitEvent('vbo-roverv-setnewrate-calc-ota-pricing', {
                            rate: current_room_rate,
                        });
                    }
                });

                // append elements to wrapper
                ota_block_channel.append(ota_pricing_value);
                ota_block_inner.append(ota_block_channel);
                ota_block_inner.append(ota_block_pricing);
                ota_block.append(ota_block_inner);
                wrapper.append(ota_block);

                // increase OTA channel counter
                ota_ch_counter++;
            }
        });

        // obtain the details for the first involved listing
        let firstListingId = listingsInvolved[0];
        let firstRplanId = vboActionRoomRplansMap[listingsInvolved[0]];

        // trigger an AJAX request to load the current alteration rules, if any, for the first listing involved
        VBOCore.doAjax(
            "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=pricing.loadOtaAlterationRules'); ?>",
            {
                room_id: firstListingId,
                rate_id: firstRplanId,
            },
            (res) => {
                var obj_res = typeof res === 'string' ? JSON.parse(res) : res;
                let alter_room_rates = obj_res['rmod'] == '1' || obj_res['rmod'] == 1;

                // scan all room OTAs
                jQuery('.vbo-roverw-setnewrate-vcm-otas').find('.vbo-roverw-setnewrate-vcm-ota-relation').each(function(key, elem) {
                    // get the current OTA identifier and whether pricing is altered
                    let ota_wrap = jQuery(elem);
                    let ota_id = ota_wrap.find('.vbo-roverw-setnewrate-vcm-ota-relation-channel').attr('data-otaid');
                    let alter_ota_rates = alter_room_rates && obj_res['channels'] && (obj_res['channels'].includes(ota_id) || obj_res['channels'].includes(parseInt(ota_id)));
                    if (!alter_ota_rates && alter_room_rates && obj_res.hasOwnProperty('rmod_channels') && obj_res['rmod_channels'].hasOwnProperty(ota_id)) {
                        alter_ota_rates = true;
                    }

                    // check if the current channel is using a different currency
                    let ota_currency_data = {};
                    if (obj_res.hasOwnProperty('cur_rplans') && obj_res['cur_rplans'].hasOwnProperty(ota_id)) {
                        let ota_check_currency = obj_res['cur_rplans'][ota_id];
                        if (obj_res.hasOwnProperty('currency_data_options') && obj_res['currency_data_options'].hasOwnProperty(ota_check_currency)) {
                            // set custom currency data returned
                            ota_currency_data = obj_res['currency_data_options'][ota_check_currency];
                        }
                    }

                    // build pricing alteration strings
                    let alteration_command = '';
                    let alteration_string  = '';

                    // default alteration factors (no pricing alteration rules)
                    let alter_op = '+';
                    let alter_amount = '0';
                    let alter_val = '%';

                    if (alter_ota_rates) {
                        // check how rates are altered for this channel
                        if (obj_res.hasOwnProperty('rmod_channels') && obj_res['rmod_channels'].hasOwnProperty(ota_id)) {
                            // ota-level pricing alteration rule
                            if (parseInt(obj_res['rmod_channels'][ota_id]['rmod']) == 1) {
                                alter_op = parseInt(obj_res['rmod_channels'][ota_id]['rmodop']) == 1 ? '+' : '-';
                                alter_amount = obj_res['rmod_channels'][ota_id]['rmodamount'];
                                alter_val = parseInt(obj_res['rmod_channels'][ota_id]['rmodval']) == 1 ? '%' : '*';
                            }
                        } else {
                            // room-level pricing alteration rule
                            alter_op = parseInt(obj_res['rmodop']) == 1 ? '+' : '-';
                            alter_amount = obj_res['rmodamount'] || '0';
                            alter_val = parseInt(obj_res['rmodval']) == 1 ? '%' : '*';
                        }
                    }

                    // construct alteration strings
                    alteration_command = alter_op + (alter_amount + '') + (alter_val + '');
                    alteration_string  = alter_op + (alter_amount + '') + (alter_val == '%' ? '%' : (ota_currency_data?.symbol || <?php echo json_encode($currencysymb) ?: '"$"'; ?>));

                    // stop room-ota loading and set alteration string
                    let alteration_elem = jQuery('<span></span>');
                    alteration_elem
                        .addClass('vbo-roverw-setnewrate-ota-pricing-currentvalue')
                        .attr('data-alteration', alteration_command)
                        .html(alteration_string);

                    let will_alter_elem = jQuery('<span></span>').addClass('vbo-roverw-setnewrate-ota-pricing-willvalue');

                    if (ota_currency_data.symbol) {
                        // set currency data object
                        will_alter_elem.attr('data-currency', JSON.stringify(ota_currency_data));
                    }

                    // set elements
                    ota_wrap
                        .find('.vbo-roverw-setnewrate-vcm-ota-pricing-startvalue')
                        .html('')
                        .append(will_alter_elem)
                        .append(alteration_elem)
                        .append('<?php VikBookingIcons::e('edit', 'edit-ota-pricing'); ?>');

                    // populate default values for input element overrides
                    ota_wrap.find('select[data-alter-rule="rmodsop"]').val(alter_op == '+' ? 1 : 0);
                    ota_wrap.find('input[data-alter-rule="rmodsamount"]').val(parseInt(alter_amount) > 0 ? alter_amount : '');
                    ota_wrap.find('select[data-alter-rule="rmodsval"]').val(alter_val == '%' ? 1 : 0);
                });

                // check the current rate value
                let current_room_rate = jQuery('#roverw-newrate').val();
                if (current_room_rate) {
                    // dispatch the event to allow the actual calculation of the OTA rate
                    VBOCore.emitEvent('vbo-roverv-setnewrate-calc-ota-pricing', {
                        rate: current_room_rate,
                        room_id: firstListingId,
                        rate_id: firstRplanId,
                    });
                }
            },
            (err) => {
                alert(err.responseText || 'Request Failed');
            }
        );
    };

    /**
     * Restrictions can be updated only if VCM is available and toggled ON, because
     * the creation and transmission is made through the Connector Class of VCM.
     * On top of that, the OTA pricing alteration rule overrides will toggle a status class.
     */
    const vboActionVcmRestrictionsSupported = () => {
        if (!jQuery('input[name="roverw-newrate-vcm"]').prop('disabled') && jQuery('input[name="roverw-newrate-vcm"]').prop('checked')) {
            jQuery('#roverw-newrestr').val('');
            jQuery('.vbo-roverw-newrestr-wrap').show();
            jQuery('.vbo-roverw-setnewrate-vcm-ota-relation').removeClass('vbo-roverw-setnewrate-vcm-ota-relation-disabled');
        } else {
            jQuery('.vbo-roverw-newrestr-wrap').hide();
            jQuery('.vbo-roverw-setnewrate-vcm-ota-relation').addClass('vbo-roverw-setnewrate-vcm-ota-relation-disabled');
        }
    };

    /**
     * Register function for loading the tasks of a given area/project id.
     * 
     * @param   number      areaId      The area/project ID to render.
     * @param   string      from_date   The Y-m-d starting date.
     * @param   string      to_date     The Y-m-d ending date.
     * @param   Array       room_ids    List of listing IDs to include.
     * @param   function    onComplete  Optional callback to invoke upon completion.
     */
    const vboActionLoadAreaTasks = (areaId, from_date, to_date, room_ids, onComplete) => {
        let ctx_elem = document
            .querySelector('.vbo-context-menu-overview-actions');

        let lbl_elem = ctx_elem
            .querySelector('.vbo-context-menu-lbl');

        let orig_lbl = lbl_elem.innerText;

        // start loading animation
        ctx_elem.loading = 1;
        lbl_elem.innerHTML = '<?php VikBookingIcons::e('circle-notch', 'fa-spin fa-fw'); ?>';

        // make the request
        VBOCore.doAjax(
            "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=taskmanager.loadAreaListingTasks'); ?>",
            {
                area_id: areaId,
                room_ids: room_ids,
                from_date: from_date,
                to_date: to_date,
            },
            (resp) => {
                try {
                    // decode the response (if needed), and append the content to the modal body
                    resp = typeof resp === 'string' ? JSON.parse(resp) : resp;

                    // hide any previously loaded area task, if any
                    vboActionHideAreaTasks(areaId);

                    // populate tasks for all listings by scanning all month tables
                    document
                        .querySelectorAll('table.vboverviewtable[data-month-from]')
                        // iterate months
                        .forEach((month) => {
                            // detect if we are using the "old" table layout
                            let isTableLayout = month.classList.contains('vbo-overv-sticky-table-head-on');

                            month
                                .querySelectorAll('.roomname[data-roomid]')
                                // iterate month listings
                                .forEach((roomRowCell) => {
                                    if (roomRowCell.matches('.subroomname')) {
                                        // skip sub-unit rows
                                        return;
                                    }

                                    // get current listing ID
                                    let listingId = roomRowCell.getAttribute('data-roomid');

                                    if (!resp.listings.hasOwnProperty(listingId) && !resp.listingIds.includes(listingId) && !resp.listingIds.includes(parseInt(listingId))) {
                                        if (resp.listingIds.length) {
                                            // no tasks returned for this uneligible room id
                                            return;
                                        }
                                    }

                                    // access the "parent row"
                                    let parentRow = roomRowCell.closest('tr');

                                    // build area-tasks row for the current listing
                                    let areaTasksRow = document.createElement('tr');
                                    areaTasksRow.classList.add('vbo-tm-row');
                                    areaTasksRow.setAttribute('data-roomid', listingId);
                                    areaTasksRow.setAttribute('data-area-id', areaId);

                                    // build first row-cell and append it to the row
                                    let areaTasksCell = document.createElement('td');
                                    areaTasksCell.classList.add('vbo-tm-row-cell-first');
                                    areaTasksCell.innerHTML = resp.area?.icon_class ? '<i class="' + resp.area.icon_class + '"></i> ' : '';
                                    let areaNameEl = document.createElement('span');
                                    areaNameEl.classList.add('vbo-tm-row-area-name');
                                    areaNameEl.innerText = resp.area.name;
                                    areaTasksCell.append(areaNameEl);
                                    areaTasksRow.append(areaTasksCell);

                                    // iterate over the "parent row" day cells of the current listing and month
                                    parentRow
                                        .querySelectorAll('td.vbo-grid-avcell[data-day]')
                                        // iterate month listing days
                                        .forEach((roomRowDay) => {
                                            // get the day Y-m-d value
                                            let ymd = roomRowDay.getAttribute('data-day');

                                            // build month-day cell with pricing details
                                            let tasksDayCell = document.createElement('td');
                                            tasksDayCell.classList.add('vbo-tm-row-cell-day');
                                            tasksDayCell.setAttribute('data-day', ymd);

                                            // access the tasks information for this day
                                            if (resp.listings.hasOwnProperty(listingId) && resp.listings[listingId].hasOwnProperty(ymd)) {
                                                // iterate all tasks for the current listing and day
                                                let totTasks = resp.listings[listingId][ymd].length;
                                                resp.listings[listingId][ymd].forEach((task, index) => {
                                                    if (index > 2) {
                                                        // no more tasks to display for the current listing and day
                                                        return;
                                                    }

                                                    // build task element
                                                    let taskEl = document.createElement('div');
                                                    taskEl.classList.add('vbo-tm-row-cell-task');
                                                    taskEl.setAttribute('data-task-id', task.id);
                                                    taskEl.setAttribute('data-area-id', task.area_id);
                                                    taskEl.setAttribute('data-task-bid', (task.bid + ''));
                                                    if (task.color) {
                                                        taskEl.classList.add('vbo-tm-color');
                                                        taskEl.classList.add(task.color);
                                                    }
                                                    if (isTableLayout) {
                                                        taskEl.classList.add('vbo-tm-row-cell-task-notitle');
                                                        taskEl.innerHTML = '&nbsp;';
                                                    } else {
                                                        taskEl.innerText = task.title;
                                                    }

                                                    // append task element to current cell
                                                    tasksDayCell.append(taskEl);

                                                    if ((index + 1) > 2 && totTasks > (2 + 1)) {
                                                        // limit the number of tasks per listing per day by displaying a link to the TM view for this day
                                                        let exceeding = totTasks - (index + 1);
                                                        let moreTasksTxt = '+' + exceeding + ' ' + (exceeding > 1 ? <?php echo json_encode(JText::translate('VBO_TASKS')); ?> : <?php echo json_encode(JText::translate('VBO_TASK')); ?>).toLowerCase();

                                                        // build element
                                                        let moreTasksEl = document.createElement('div');
                                                        moreTasksEl.classList.add('vbo-tm-calendar-month-day-more')
                                                        moreTasksEl.setAttribute('data-day', ymd);
                                                        moreTasksEl.innerText = moreTasksTxt;
                                                        moreTasksEl.addEventListener('click', () => {
                                                            window.location.href = '<?php echo VBOFactory::getPlatform()->getUri()->admin('index.php?option=com_vikbooking&view=taskmanager&mode=calendar&filters[calendar_type]=day&filters[calendar_day]=%s', false); ?>'.replace('%s', ymd);
                                                        });

                                                        // append element to current cell
                                                        tasksDayCell.append(moreTasksEl);

                                                        return;
                                                    }
                                                });
                                            }

                                            // append tasks cell to main area-tasks row
                                            areaTasksRow.append(tasksDayCell);
                                        });

                                    // append the main area-tasks row to the DOM
                                    parentRow.insertAdjacentElement('afterend', areaTasksRow);
                                });
                        });

                    // iterating completed, stop loading
                    lbl_elem.innerHTML = '';
                    lbl_elem.innerText = orig_lbl;
                    ctx_elem.loading = 0;

                    // register events for the area tasks
                    vboActionRegisterAreaTaskEvents(areaId);

                    if (typeof onComplete === 'function') {
                        // invoke the callback upon completion
                        onComplete(areaId);
                    }
                } catch (err) {
                    console.error('Error decoding the response', err, resp);
                }
            },
            (error) => {
                // display error message
                alert(error.responseText);

                // stop loading
                lbl_elem.innerHTML = '';
                lbl_elem.innerText = orig_lbl;
                ctx_elem.loading = 0;
            }
        );
    };

    /**
     * Register function to process a list of area/project IDs for which the tasks should be loaded.
     * 
     * @param   Array       areaIds     The list of area/project IDs to process.
     * @param   string      from_date   The Y-m-d starting date.
     * @param   string      to_date     The Y-m-d ending date.
     * @param   Array       room_ids    List of listing IDs to include.
     * @param   function    onProgress  Function to invoke upon progressing to a next request.
     */
    const vboActionDispatchAreasLoading = (areaIds, from_date, to_date, room_ids, onProgress) => {
        if (!Array.isArray(areaIds) || !areaIds.length) {
            // abort when the queue is exhausted
            return;
        }

        // obtain the area ID to process
        const areaId = areaIds.shift();

        // load the tasks for the current area/project ID
        vboActionLoadAreaTasks(areaId, from_date, to_date, room_ids, (areaProcessed) => {
            if (typeof onProgress === 'function') {
                // request completed, invoke the callback upon progressing to the next request to process
                onProgress(areaProcessed);
            }

            // perform a recursive call for the next area/project ID to process, if any
            vboActionDispatchAreasLoading(areaIds, from_date, to_date, room_ids, onProgress);
        });
    };

    /**
     * Register function to hide the tasks of a given area/project id.
     */
    const vboActionHideAreaTasks = (areaId) => {
        document
            .querySelectorAll('.vbo-tm-row[data-area-id="' + areaId + '"]')
            .forEach((row) => {
                row.remove();
            });
    };

    /**
     * Register function to add event listeners for the tasks of a given area (edit task, new task, hover task).
     */
    const vboActionRegisterAreaTaskEvents = (areaId) => {
        // edit and hover existing tasks
        document
            .querySelectorAll('.vbo-tm-row-cell-task')
            .forEach((taskElement) => {
                if (taskElement.clickListener) {
                    // listener added already
                    return;
                }

                // get the clicked task, area and booking IDs
                const taskId = taskElement.getAttribute('data-task-id');
                const areaId = taskElement.getAttribute('data-area-id');
                const taskBid = taskElement.getAttribute('data-task-bid');

                // define the click event for editing the task
                taskElement.addEventListener('click', () => {
                    // define the modal delete button
                    let delete_btn = jQuery('<button></button>')
                        .attr('type', 'button')
                        .addClass('btn btn-danger')
                        .text(<?php echo json_encode(JText::translate('VBELIMINA')); ?>)
                        .on('click', function() {
                            if (!confirm(<?php echo json_encode(JText::translate('VBDELCONFIRM')); ?>)) {
                                return false;
                            }

                            // disable button to prevent double submissions
                            let submit_btn = jQuery(this);
                            submit_btn.prop('disabled', true);

                            // start loading animation
                            VBOCore.emitEvent('vbo-tm-edittask-loading');

                            // make the request
                            VBOCore.doAjax(
                                "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=taskmanager.deleteTask'); ?>",
                                {
                                    data: {
                                        id: taskId,
                                    },
                                },
                                (resp) => {
                                    // parse all context menu buttons to identify the current area/project id
                                    jQuery('.vbo-context-menu-overview-actions').vboContextMenu('buttons').forEach((btn) => {
                                        if (btn.areaId == areaId) {
                                            btn.activeState = false;
                                            // trigger action to reload the area/project tasks
                                            btn.action();
                                        }
                                    });

                                    // dismiss the modal
                                    VBOCore.emitEvent('vbo-tm-edittask-dismiss');
                                },
                                (error) => {
                                    // display error message
                                    alert(error.responseText);

                                    // re-enable submit button
                                    submit_btn.prop('disabled', false);

                                    // stop loading
                                    VBOCore.emitEvent('vbo-tm-edittask-loading');
                                }
                            );
                        });

                    // define the modal save button
                    let save_btn = jQuery('<button></button>')
                        .attr('type', 'button')
                        .addClass('btn btn-success')
                        .text(<?php echo json_encode(JText::translate('VBSAVE')); ?>)
                        .on('click', function() {
                            // disable button to prevent double submissions
                            let submit_btn = jQuery(this);
                            submit_btn.prop('disabled', true);

                            // start loading animation
                            VBOCore.emitEvent('vbo-tm-edittask-loading');

                            // get form data
                            const taskForm = new FormData(document.querySelector('#vbo-tm-task-manage-form'));

                            // build query parameters for the request
                            let qpRequest = new URLSearchParams(taskForm);

                            // make sure the request always includes the assignees query parameter, even if the list is empty
                            if (!qpRequest.has('data[assignees][]')) {
                                qpRequest.append('data[assignees][]', []);
                            }

                            // make sure the request always includes the tags query parameter, even if the list is empty
                            if (!qpRequest.has('data[tags][]')) {
                                qpRequest.append('data[tags][]', []);
                            }

                            // make the request
                            VBOCore.doAjax(
                                "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=taskmanager.updateTask'); ?>",
                                qpRequest.toString(),
                                (resp) => {
                                    // parse all context menu buttons to identify the current area/project id
                                    jQuery('.vbo-context-menu-overview-actions').vboContextMenu('buttons').forEach((btn) => {
                                        if (btn.areaId == areaId) {
                                            btn.activeState = false;
                                            // trigger action to reload the area/project tasks
                                            btn.action();
                                        }
                                    });

                                    // dismiss the modal
                                    VBOCore.emitEvent('vbo-tm-edittask-dismiss');
                                },
                                (error) => {
                                    // display error message
                                    alert(error.responseText);

                                    // re-enable submit button
                                    submit_btn.prop('disabled', false);

                                    // stop loading
                                    VBOCore.emitEvent('vbo-tm-edittask-loading');
                                }
                            );
                        });

                    // display modal
                    let modalBody = VBOCore.displayModal({
                        suffix:         'tm_edittask_modal',
                        title:          <?php echo json_encode(JText::translate('VBO_TASK')); ?> + ' #' + taskId,
                        extra_class:    'vbo-modal-rounded vbo-modal-taller vbo-modal-large',
                        body_prepend:   true,
                        lock_scroll:    true,
                        escape_dismiss: false,
                        footer_left:    delete_btn,
                        footer_right:   save_btn,
                        loading_event:  'vbo-tm-edittask-loading',
                        dismiss_event:  'vbo-tm-edittask-dismiss',
                    });

                    // start loading animation
                    VBOCore.emitEvent('vbo-tm-edittask-loading');

                    // make the request
                    VBOCore.doAjax(
                        "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=taskmanager.renderLayout'); ?>",
                        {
                            type: 'tasks.managetask',
                            data: {
                                task_id: taskId,
                                area_id: areaId,
                                form_id: 'vbo-tm-task-manage-form',
                            },
                        },
                        (resp) => {
                            // stop loading
                            VBOCore.emitEvent('vbo-tm-edittask-loading');

                            try {
                                // decode the response (if needed), and append the content to the modal body
                                let obj_res = typeof resp === 'string' ? JSON.parse(resp) : resp;
                                modalBody.append(obj_res['html']);
                            } catch (err) {
                                console.error('Error decoding the response', err, resp);
                            }
                        },
                        (error) => {
                            // display error message
                            alert(error.responseText);

                            // stop loading
                            VBOCore.emitEvent('vbo-tm-edittask-loading');
                        }
                    );
                });

                // define the mouseover/mouseout events for the current task
                if (taskBid) {
                    // highlight the booking(s) assigned to the current task when hovered
                    taskElement.addEventListener('mouseover', () => {
                        // highlight all cells for this booking
                        let bidSnakeElements = document.querySelectorAll('td.vbo-grid-avcell[data-bids*="-' + taskBid + '-"] > .vbo-tableaux-booking');
                        bidSnakeElements.forEach((snake, index) => {
                            if (snake.matches('.vbo-tableaux-booking-checkout')) {
                                // this must be a check-out snake on the check-in date of the desired booking snake
                                return;
                            }
                            // add the highlight class
                            snake.classList.add('vbo-tableaux-booking-task-highlight');
                            if (++index == bidSnakeElements.length && !snake.matches('.vbo-tableaux-booking-checkout')) {
                                // look for the check-out snake for the same booking
                                let parentCell = snake.closest('td').nextElementSibling;
                                if (parentCell && parentCell.querySelector('.vbo-tableaux-booking-checkout')) {
                                    // add the highlight class also to the check-out snake
                                    parentCell
                                        .querySelector('.vbo-tableaux-booking-checkout')
                                        .classList
                                        .add('vbo-tableaux-booking-task-highlight');
                                }
                            }
                        });
                    });

                    // un-highlight the booking(s) assigned to the current task when mouse goes out
                    taskElement.addEventListener('mouseout', () => {
                        document
                            .querySelectorAll('.vbo-tableaux-booking-task-highlight')
                            .forEach((el) => {
                                el.classList.remove('vbo-tableaux-booking-task-highlight');
                            });
                    });
                }

                // turn flag on for listener set
                taskElement.clickListener = true;
            });

        // create new tasks
        document
            .querySelectorAll('.vbo-tm-row[data-area-id="' + areaId + '"] .vbo-tm-row-cell-day')
            .forEach((tmCell) => {
                const listingId = tmCell.closest('tr').getAttribute('data-roomid');
                const day = tmCell.getAttribute('data-day');

                tmCell.addEventListener('click', (e) => {
                    if (e.target && (e.target.matches('.vbo-tm-row-cell-task') || e.target.parentNode.matches('.vbo-tm-row-cell-task') || e.target.matches('.vbo-tm-calendar-month-day-more'))) {
                        // an existing task for this cell was clicked (or the "see more" element), so we abort the process
                        return;
                    }

                    // define the modal cancel button
                    let cancel_btn = jQuery('<button></button>')
                        .attr('type', 'button')
                        .addClass('btn')
                        .text(<?php echo json_encode(JText::translate('VBANNULLA')); ?>)
                        .on('click', () => {
                            VBOCore.emitEvent('vbo-tm-newtask-dismiss');
                        });

                    // define the modal save button
                    let save_btn = jQuery('<button></button>')
                        .attr('type', 'button')
                        .addClass('btn btn-success')
                        .text(<?php echo json_encode(JText::translate('VBSAVE')); ?>)
                        .on('click', function() {
                            // disable button to prevent double submissions
                            let submit_btn = jQuery(this);
                            submit_btn.prop('disabled', true);

                            // start loading animation
                            VBOCore.emitEvent('vbo-tm-newtask-loading');

                            // get form data
                            const taskForm = new FormData(document.querySelector('#vbo-tm-task-manage-form'));

                            // build query parameters for the request
                            let qpRequest = new URLSearchParams(taskForm).toString();

                            // make the request
                            VBOCore.doAjax(
                                "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=taskmanager.createTask'); ?>",
                                qpRequest,
                                (resp) => {
                                    // parse all context menu buttons to identify the current area/project id
                                    jQuery('.vbo-context-menu-overview-actions').vboContextMenu('buttons').forEach((btn) => {
                                        if (btn.areaId == areaId) {
                                            btn.activeState = false;
                                            // trigger action to reload the area/project tasks
                                            btn.action();
                                        }
                                    });

                                    // dismiss the modal
                                    VBOCore.emitEvent('vbo-tm-newtask-dismiss');
                                },
                                (error) => {
                                    // display error message
                                    alert(error.responseText);

                                    // re-enable submit button
                                    submit_btn.prop('disabled', false);

                                    // stop loading
                                    VBOCore.emitEvent('vbo-tm-newtask-loading');
                                }
                            );
                        });

                    // display modal
                    let modalBody = VBOCore.displayModal({
                        suffix:         'tm_newtask_modal',
                        title:          <?php echo json_encode(JText::translate('VBO_NEW_TASK')); ?>,
                        extra_class:    'vbo-modal-rounded vbo-modal-taller vbo-modal-large',
                        body_prepend:   true,
                        lock_scroll:    true,
                        escape_dismiss: false,
                        footer_left:    cancel_btn,
                        footer_right:   save_btn,
                        loading_event:  'vbo-tm-newtask-loading',
                        dismiss_event:  'vbo-tm-newtask-dismiss',
                    });

                    // start loading animation
                    VBOCore.emitEvent('vbo-tm-newtask-loading');

                    // make the request
                    VBOCore.doAjax(
                        "<?php echo VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=taskmanager.renderLayout'); ?>",
                        {
                            type: 'tasks.managetask',
                            data: {
                                area_id: areaId,
                                form_id: 'vbo-tm-task-manage-form',
                                filters: {
                                    calendar_day: day,
                                    id_room: listingId,
                                },
                            },
                        },
                        (resp) => {
                            // stop loading
                            VBOCore.emitEvent('vbo-tm-newtask-loading');

                            try {
                                // decode the response (if needed), and append the content to the modal body
                                let obj_res = typeof resp === 'string' ? JSON.parse(resp) : resp;
                                modalBody.append(obj_res['html']);
                            } catch (err) {
                                console.error('Error decoding the response', err, resp);
                            }
                        },
                        (error) => {
                            // display error message
                            alert(error.responseText);

                            // stop loading
                            VBOCore.emitEvent('vbo-tm-newtask-loading');
                        }
                    );
                });
            });
    };

    jQuery(function() {

        // all task areas
        const taskAreas = <?php echo json_encode($taskAreas); ?>;

        // configure the currency object
        VBOCore.getCurrency({
            symbol:     <?php echo json_encode($currencysymb) ?: '"$"'; ?>,
            digits:     <?php echo intval($currency_digits); ?>,
            decimals:   <?php echo json_encode($currency_decimals) ?: '"."'; ?>,
            thousands:  <?php echo json_encode($currency_thousands) ?: '","'; ?>,
            noDecimals: 1,
        });

        // build buttons
        let btns = [
            {
                class: 'btngroup',
                text: <?php echo json_encode(JText::translate('VBMENUFARES')); ?>,
                disabled: true,
            },
            {
                activeState: false,
                separator: (taskAreas.length > 0),
                class: 'vbo-context-menu-entry-secondary',
                text: <?php echo json_encode(JText::translate('VBO_RATES_AND_RESTR')); ?>,
                icon: () => {
                    return this.activeState ? '<?php echo VikBookingIcons::i('toggle-on'); ?>' : '<?php echo VikBookingIcons::i('toggle-off'); ?>';
                },
                action: (root, event) => {
                    // get all month tables
                    let monthTables = document.querySelectorAll('table.vboverviewtable[data-month-from]');
                    if (!monthTables.length) {
                        return;
                    }

                    // get the date bounds
                    let from_date = monthTables[0].getAttribute('data-month-from');
                    let to_date = monthTables[(monthTables.length - 1)].getAttribute('data-month-to');

                    // get all listing IDs from the first table
                    let listingIds = [];
                    monthTables[0]
                        .querySelectorAll('.roomname[data-roomid]')
                        .forEach((roomRowCell) => {
                            if (roomRowCell.matches('.subroomname')) {
                                // skip sub-unit rows
                                return;
                            }
                            listingIds.push(roomRowCell.getAttribute('data-roomid'));
                        });

                    // toggle state
                    this.activeState = !this.activeState;

                    if (this.activeState) {
                        // load room rates
                        vboActionLoadRoomRates(from_date, to_date, listingIds);
                    } else {
                        // hide room rates
                        vboActionHideRoomRates();
                    }
                },
                disabled: () => {
                    let ctx_elem = document
                        .querySelector('.vbo-context-menu-overview-actions');

                    return <?php echo !$vbo_auth_pricing ? 'true' : 'false'; ?> || ctx_elem?.loading == 1;
                },
            },
        ];

        if (taskAreas.length) {
            // push group button
            btns.push({
                class: 'btngroup',
                text: <?php echo json_encode(JText::translate('VBO_TASK_MANAGER')); ?>,
                disabled: true,
            });

            taskAreas.forEach((area, index) => {
                // push area/project button
                btns.push({
                    activeState: false,
                    areaId: area.id,
                    class: 'vbo-context-menu-entry-secondary',
                    text: area.name,
                    separator: false,
                    icon: function() {
                        return this.activeState === true ? '<?php echo VikBookingIcons::i('check-square'); ?>' : '<?php echo VikBookingIcons::i('far fa-square'); ?>';
                    },
                    action: function(root, event) {
                        // get all month tables
                        let monthTables = document.querySelectorAll('table.vboverviewtable[data-month-from]');
                        if (!monthTables.length) {
                            return;
                        }

                        // get the date bounds
                        let from_date = monthTables[0].getAttribute('data-month-from');
                        let to_date = monthTables[(monthTables.length - 1)].getAttribute('data-month-to');

                        // get all listing IDs from the first table
                        let listingIds = [];
                        monthTables[0]
                            .querySelectorAll('.roomname[data-roomid]')
                            .forEach((roomRowCell) => {
                                if (roomRowCell.matches('.subroomname')) {
                                    // skip sub-unit rows
                                    return;
                                }
                                listingIds.push(roomRowCell.getAttribute('data-roomid'));
                            });

                        // toggle active state
                        this.activeState = !this.activeState;

                        if (this.activeState) {
                            // render tasks for the selected area
                            vboActionLoadAreaTasks(this.areaId, from_date, to_date, listingIds);
                        } else {
                            // hide tasks for the selected area
                            vboActionHideAreaTasks(this.areaId);
                        }
                    },
                    disabled: () => {
                        let ctx_elem = document
                            .querySelector('.vbo-context-menu-overview-actions');

                        return <?php echo !$vbo_auth_pms ? 'true' : 'false'; ?> || ctx_elem?.loading == 1;
                    },
                });
            });
        }

        // start context menu on the proper button element
        jQuery('.vbo-context-menu-overview-actions').vboContextMenu({
            placement: 'bottom-left',
            buttons: btns,
        });

        // register listener to reset the room-rate selection upon Esc keyup event
        window.addEventListener('keyup', (e) => {
            if (!e.key || e.key != 'Escape') {
                return;
            }

            if (vboActionRoomRateData.start) {
                // reset selection
                vboActionRoomRateHandleReset();
            }
        });

        // register listener for the "input" event on the "set new rate" input field of type number
        document.querySelector('#roverw-newrate').addEventListener('input', VBOCore.debounceEvent((e) => {
            // dispatch the event to calculate the new OTA pricing value
            VBOCore.emitEvent('vbo-roverv-setnewrate-calc-ota-pricing', {rate: e.target.value});
        }, 200));

        // register listener for when a new rate is set to update what will be the OTA pricing value
        document.addEventListener('vbo-roverv-setnewrate-calc-ota-pricing', VBOCore.debounceEvent((e) => {
            if (!e || !e.detail || !e.detail.rate) {
                // invalid event data
                return;
            }

            // get the new PMS rate
            let rate_amount = parseFloat(e.detail.rate);

            // access the currency object
            let currencyObj = VBOCore.getCurrency();

            // scan all OTA alteration rules, if any
            document.querySelectorAll('.vbo-roverw-setnewrate-ota-pricing-currentvalue[data-alteration]').forEach((elem) => {
                // channel alteration string
                let alter_string = elem.getAttribute('data-alteration');
                if (!alter_string) {
                    alter_string = '+0%';
                }

                // default alteration factors (no pricing alteration rules)
                let alter_op = alter_string.substr(0, 1);
                let alter_val = alter_string.substr(-1, 1);
                let alter_amount = parseFloat(alter_string.replace(alter_op, '').replace(alter_val, ''));

                // calculate what the rate will be on the OTA
                let ota_rate_amount = rate_amount;

                if (!isNaN(alter_amount) && Math.abs(alter_amount) > 0) {
                    if (alter_op == '+') {
                        // increase rate
                        if (alter_val == '%') {
                            // percent
                            let amount_inc = currencyObj.multiply(alter_amount, 0.01);
                            amount_inc = currencyObj.multiply(rate_amount, amount_inc);
                            ota_rate_amount = currencyObj.sum(rate_amount, amount_inc);
                        } else {
                            // absolute
                            ota_rate_amount = currencyObj.sum(rate_amount, alter_amount);
                        }
                    } else {
                        // discount rate
                        if (alter_val == '%') {
                            // percent
                            let amount_inc = currencyObj.multiply(alter_amount, 0.01);
                            amount_inc = currencyObj.multiply(rate_amount, amount_inc);
                            ota_rate_amount = currencyObj.diff(rate_amount, amount_inc);
                        } else {
                            // absolute
                            ota_rate_amount = currencyObj.diff(rate_amount, alter_amount);
                        }
                    }
                }

                // get the element containing the calculated ota pricing
                let will_alter_elem = elem
                    .closest('.vbo-roverw-setnewrate-vcm-ota-pricing-startvalue')
                    .querySelector('.vbo-roverw-setnewrate-ota-pricing-willvalue');

                // define the currency options
                let ota_currency_options = {};

                // check if the channel requires a specific currency
                let ota_currency_data = will_alter_elem.getAttribute('data-currency');
                if (ota_currency_data) {
                    // decode currency data instructions
                    try {
                        ota_currency_data = JSON.parse(ota_currency_data);
                    } catch (e) {
                        ota_currency_data = {};
                    }

                    // set custom currency options
                    if (ota_currency_data['symbol']) {
                        ota_currency_options['symbol'] = ota_currency_data['symbol'];
                    }
                    if (ota_currency_data['decimals']) {
                        ota_currency_options['digits'] = ota_currency_data['decimals'];
                    }
                    if (ota_currency_data['decimals_sep']) {
                        ota_currency_options['decimals'] = ota_currency_data['decimals_sep'];
                    }
                    if (ota_currency_data['thousands_sep']) {
                        ota_currency_options['thousands'] = ota_currency_data['thousands_sep'];
                    }
                }

                // set calculated OTA rate value
                will_alter_elem.innerHTML = currencyObj.format(ota_rate_amount, ota_currency_options);
            });
        }, 200));

    <?php
    // check if some area/project IDs should be rendered
    if ($activeAreas) {
        ?>
        // get all month tables to obtain the date bounds and the listing IDs
        let from_date, to_date;
        let listingIds = [];
        let activeAreas = <?php echo json_encode($activeAreas); ?>;
        let monthTables = document.querySelectorAll('table.vboverviewtable[data-month-from]');

        if (monthTables.length) {
            // get the date bounds
            from_date = monthTables[0].getAttribute('data-month-from');
            to_date = monthTables[(monthTables.length - 1)].getAttribute('data-month-to');
            // get all listing IDs from the first table
            monthTables[0]
                .querySelectorAll('.roomname[data-roomid]')
                .forEach((roomRowCell) => {
                    if (roomRowCell.matches('.subroomname')) {
                        // skip sub-unit rows
                        return;
                    }
                    listingIds.push(roomRowCell.getAttribute('data-roomid'));
                });
        }

        if (listingIds.length) {
            // dispatch consequent requests, one for each active area ID
            vboActionDispatchAreasLoading(activeAreas, from_date, to_date, listingIds, (areaDisplayed) => {
                // parse all context menu buttons to identify the current area/project id
                jQuery('.vbo-context-menu-overview-actions').vboContextMenu('buttons').forEach((btn) => {
                    if (btn.areaId == areaDisplayed) {
                        // enable area/project active state
                        btn.activeState = true;
                    }
                });
            });
        }
        <?php
    }
    ?>

    });
</script>