From 5013b2777704fdd90fee22acfad11cd65ae5ac1f Mon Sep 17 00:00:00 2001 From: Amit sharma Date: Fri, 9 Jan 2026 17:08:55 +0530 Subject: [PATCH 01/64] Fixed: order details action permission issue --- .../template/controllers/orders/helpers/view/view.tpl | 2 +- controllers/admin/AdminOrdersController.php | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/admin/themes/default/template/controllers/orders/helpers/view/view.tpl b/admin/themes/default/template/controllers/orders/helpers/view/view.tpl index c0c841758..62911ba03 100644 --- a/admin/themes/default/template/controllers/orders/helpers/view/view.tpl +++ b/admin/themes/default/template/controllers/orders/helpers/view/view.tpl @@ -1110,7 +1110,7 @@
 {l s='Voucher'} - {if $can_edit && $order->total_paid > 0} + {if $can_edit && $order->total_paid > 0 && $cartRuleAccess['add'] === 1} diff --git a/controllers/admin/AdminOrdersController.php b/controllers/admin/AdminOrdersController.php index 30026bcca..b142e385d 100644 --- a/controllers/admin/AdminOrdersController.php +++ b/controllers/admin/AdminOrdersController.php @@ -1028,7 +1028,11 @@ class AdminOrdersControllerCore extends AdminController $taxRulesGroups = TaxRulesGroup::getTaxRulesGroups(true); $smartyVars['taxRulesGroups'] = $taxRulesGroups; $smartyVars['invoices_collection'] = $objOrder->getInvoicesCollection(); - $smartyVars['customServiceAllowed'] = Configuration::get('PS_ALLOW_CREATE_CUSTOM_SERVICES_IN_BOOKING'); + $serviceProductAccess = Profile::getProfileAccess( + $this->context->employee->id_profile, + Tab::getIdFromClassName('AdminNormalProducts') + ); + $smartyVars['customServiceAllowed'] = $serviceProductAccess['add'] && Configuration::get('PS_ALLOW_CREATE_CUSTOM_SERVICES_IN_BOOKING'); $smartyVars['current_id_lang'] = $this->context->language->id; $this->context->smarty->assign($smartyVars); @@ -3602,6 +3606,7 @@ class AdminOrdersControllerCore extends AdminController $objProduct = new Product(); $hotelStandaloneProducts = $objProduct->getServiceProducts(null, Product::SELLING_PREFERENCE_HOTEL_STANDALONE); $standaloneProducts = $objProduct->getServiceProducts(null, Product::SELLING_PREFERENCE_STANDALONE); + $cartRuleAccess = Profile::getProfileAccess($this->context->employee->id_profile, (int)Tab::getIdFromClassName('AdminCartRules')); $this->tpl_view_vars = array( 'hotelStandaloneProducts' => $hotelStandaloneProducts, @@ -3670,6 +3675,7 @@ class AdminOrdersControllerCore extends AdminController 'iso_code_lang' => $this->context->language->iso_code, 'id_lang' => $this->context->language->id, 'can_edit' => ($this->tabAccess['edit'] === 1), + 'cartRuleAccess' => $cartRuleAccess, 'current_id_lang' => $this->context->language->id, 'invoices_collection' => $order->getInvoicesCollection(), 'not_paid_invoices_collection' => $order->getNotPaidInvoicesCollection(), From e88faa7fd42975cb5e1ba1fbe1bcca2784cb1ca2 Mon Sep 17 00:00:00 2001 From: azhar bhat Date: Wed, 21 Jan 2026 19:19:11 +0530 Subject: [PATCH 02/64] Vulnerability issue while uploading code updated --- modules/qlohotelreview/classes/QhrHotelReview.php | 4 ++++ modules/qlohotelreview/controllers/front/default.php | 2 +- modules/qlohotelreview/views/js/hook/review.js | 8 ++++---- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/modules/qlohotelreview/classes/QhrHotelReview.php b/modules/qlohotelreview/classes/QhrHotelReview.php index 8768061d2..70ef425ed 100644 --- a/modules/qlohotelreview/classes/QhrHotelReview.php +++ b/modules/qlohotelreview/classes/QhrHotelReview.php @@ -290,9 +290,13 @@ class QhrHotelReview extends ObjectModel public function saveReviewImages() { $files = QhrHotelReviewHelper::fileAttachmentMultiple('images'); + $allowedExtensions = array('jpg', 'png', 'jpeg',); if (is_array($files) && count($files)) { foreach ($files as $key => $file) { $ext = pathinfo($file['rename'], PATHINFO_EXTENSION); + if (!in_array($ext, $allowedExtensions, true)) { + continue; + } $dir = _PS_MODULE_DIR_.'qlohotelreview/views/img/review/'.Image::getImgFolderStatic($this->id); QhrHotelReviewHelper::createDirectory($dir); $useSameExt = false; diff --git a/modules/qlohotelreview/controllers/front/default.php b/modules/qlohotelreview/controllers/front/default.php index c380cd5f7..899428526 100644 --- a/modules/qlohotelreview/controllers/front/default.php +++ b/modules/qlohotelreview/controllers/front/default.php @@ -86,7 +86,7 @@ class QloHotelReviewDefaultModuleFrontController extends ModuleFrontController } if (is_array($_FILES) && array_key_exists('images', $_FILES) ) { - $allowedExtensions = array('jpg', 'png'); + $allowedExtensions = array('jpg', 'png', 'jpeg',); foreach ($_FILES['images']['name'] as $key => $imageName) { $extension = Tools::strtolower(pathinfo($imageName, PATHINFO_EXTENSION)); if (!in_array($extension, $allowedExtensions)) { diff --git a/modules/qlohotelreview/views/js/hook/review.js b/modules/qlohotelreview/views/js/hook/review.js index 2a30ce2ef..b34571456 100644 --- a/modules/qlohotelreview/views/js/hook/review.js +++ b/modules/qlohotelreview/views/js/hook/review.js @@ -147,7 +147,7 @@ var QhrReviewImages = { reader.readAsDataURL(file); }, removeFile: function(iInput, iFile) { - var input = $('.images-field input.input-images').eq(iInput); + var input = $('.images-field input.input-images').get(iInput); var files = $(input).get(0).files; var dataTransfer = new DataTransfer(); $(files).each(function(iiFile, file) { @@ -155,9 +155,9 @@ var QhrReviewImages = { dataTransfer.items.add(file); } }); - input.get(0).files = dataTransfer.files; - if (input.get(0).files.length === 0) { - input.remove(); + input.files = dataTransfer.files; + if (input.files.length === 0) { + $(input).remove(); } }, resetPreviews: function() { From 96df477887b1f9fd599a33a1a9a8218c83101bef Mon Sep 17 00:00:00 2001 From: azhar bhat Date: Fri, 23 Jan 2026 12:26:00 +0530 Subject: [PATCH 03/64] Prevent invalid formats before upload --- .../controllers/front/default.php | 2 +- modules/qlohotelreview/qlohotelreview.php | 1 + .../qlohotelreview/views/css/front/review.css | 4 +++ .../qlohotelreview/views/js/hook/review.js | 26 ++++++++++++++++++- .../views/templates/hook/add-review-popup.tpl | 1 + 5 files changed, 32 insertions(+), 2 deletions(-) diff --git a/modules/qlohotelreview/controllers/front/default.php b/modules/qlohotelreview/controllers/front/default.php index 899428526..66e1b0a4f 100644 --- a/modules/qlohotelreview/controllers/front/default.php +++ b/modules/qlohotelreview/controllers/front/default.php @@ -90,7 +90,7 @@ class QloHotelReviewDefaultModuleFrontController extends ModuleFrontController foreach ($_FILES['images']['name'] as $key => $imageName) { $extension = Tools::strtolower(pathinfo($imageName, PATHINFO_EXTENSION)); if (!in_array($extension, $allowedExtensions)) { - $errors['general'][] = $imageName . ' ' . $objModule->l('is not a supported image format.','default'); + $errors['general'][] = $objModule->l('Only images with the following extensions are allowed: jpg, jpeg, png','default'); } } } diff --git a/modules/qlohotelreview/qlohotelreview.php b/modules/qlohotelreview/qlohotelreview.php index 405f12ff4..086265f15 100644 --- a/modules/qlohotelreview/qlohotelreview.php +++ b/modules/qlohotelreview/qlohotelreview.php @@ -182,6 +182,7 @@ class QloHotelReview extends Module $this->l('You can upload a maximum of %d images.', false, true), (int) Configuration::get('QHR_MAX_IMAGES_PER_REVIEW') ), + 'allowed_extensions' => $this->l('Only images with the following extensions are allowed: jpg, jpeg, png', false, true), ), ))); diff --git a/modules/qlohotelreview/views/css/front/review.css b/modules/qlohotelreview/views/css/front/review.css index 0c7346dce..539d10199 100644 --- a/modules/qlohotelreview/views/css/front/review.css +++ b/modules/qlohotelreview/views/css/front/review.css @@ -52,6 +52,10 @@ margin-bottom: 14px; } +#add-review-popup .hint { + margin-top: 6px; +} + #add-review-popup .review-error { margin-top: 2px; color: #E85757; diff --git a/modules/qlohotelreview/views/js/hook/review.js b/modules/qlohotelreview/views/js/hook/review.js index b34571456..2db6b37b9 100644 --- a/modules/qlohotelreview/views/js/hook/review.js +++ b/modules/qlohotelreview/views/js/hook/review.js @@ -132,6 +132,29 @@ var QhrReviewImages = { init: function() { QhrReviewImages.inputHtml = ''; }, + validateFiles: function () { + $('#review-general-errors').hide(); + let isValid = true; + $('.images-field input.input-images').each(function (iInput, input) { + let files = input.files; + let dataTransfer = new DataTransfer(); + $.each(files, function (iFile, file) { + if (file.type.startsWith('image/')) { + dataTransfer.items.add(file); + } else { + isValid = false; + } + }); + input.files = dataTransfer.files; + if (!input.files.length) { + $(input).remove(); + } + }); + if (!isValid) { + QhrReviewForm.showGeneralErrors(qlo_hotel_review_js_vars.texts.allowed_extensions); + } + return isValid; + }, getFilesCount: function() { var count = 0; $('.images-field input.input-images').each(function(i, input) { @@ -244,8 +267,9 @@ $(document).on('change', 'input.input-images', function(e) { var filesCount = QhrReviewImages.getFilesCount(); if (filesCount > qlo_hotel_review_js_vars.num_images_max) { QhrReviewImages.removeLastInput(); - alert(qlo_hotel_review_js_vars.texts.num_files); + QhrReviewForm.showGeneralErrors(qlo_hotel_review_js_vars.texts.num_files); } else { + QhrReviewImages.validateFiles(); QhrReviewImages.updatePreviews(); if (filesCount == qlo_hotel_review_js_vars.num_images_max) { diff --git a/modules/qlohotelreview/views/templates/hook/add-review-popup.tpl b/modules/qlohotelreview/views/templates/hook/add-review-popup.tpl index a838127d8..1ba6f6936 100644 --- a/modules/qlohotelreview/views/templates/hook/add-review-popup.tpl +++ b/modules/qlohotelreview/views/templates/hook/add-review-popup.tpl @@ -40,6 +40,7 @@
+

{l s='Allowed formats are: .jpg, .jpeg, .png' mod='qlohotelreview'}

From 48e08bed870dc4e6ad1486fb26d0bcc06f285a29 Mon Sep 17 00:00:00 2001 From: azhar bhat Date: Tue, 17 Feb 2026 15:33:31 +0530 Subject: [PATCH 04/64] Improve room management: introduce connected rooms in Back Office --- .../orders/_current_cart_details_data.tpl | 6 +- .../controllers/orders/_product_line.tpl | 3 + .../controllers/products/configuration.tpl | 153 ++++++++++++++++++ .../controllers/products/connected-rooms.tpl | 87 ++++++++++ .../products/modal-connected-rooms.tpl | 148 +++++++++++++++++ controllers/admin/AdminOrdersController.php | 1 + .../classes/HotelCartBookingData.php | 1 + .../classes/HotelReservationSystemDb.php | 11 +- .../classes/HotelRoomConnected.php | 86 ++++++++++ .../AdminHotelRoomsBookingController.php | 1 + modules/hotelreservationsystem/define.php | 1 + .../helpers/view/_partials/booking-rooms.tpl | 6 +- 12 files changed, 501 insertions(+), 3 deletions(-) create mode 100644 admin/themes/default/template/controllers/products/connected-rooms.tpl create mode 100644 admin/themes/default/template/controllers/products/modal-connected-rooms.tpl create mode 100644 modules/hotelreservationsystem/classes/HotelRoomConnected.php diff --git a/admin/themes/default/template/controllers/orders/_current_cart_details_data.tpl b/admin/themes/default/template/controllers/orders/_current_cart_details_data.tpl index e3405fa99..161c83965 100644 --- a/admin/themes/default/template/controllers/orders/_current_cart_details_data.tpl +++ b/admin/themes/default/template/controllers/orders/_current_cart_details_data.tpl @@ -51,7 +51,11 @@ {assign var=curr_id value=$cart->id_currency|intval} {foreach from=$cart_detail_data item=data} - {$data.room_num|escape:'html':'UTF-8'} {hook h='displayRoomNumAfter' data=$data type='adminOrder'} + {$data.room_num|escape:'html':'UTF-8'} + {if isset($data['connected_rooms'][$data['id_room']]) && $data['connected_rooms'][$data['id_room']]|@count > 0} + {include file="controllers/products/connected-rooms.tpl" htl_connected_rooms= $data['connected_rooms'][$data['id_room']]} + {/if} + {hook h='displayRoomNumAfter' data=$data type='adminOrder'}

{$data.room_type|escape:'html':'UTF-8'}

diff --git a/admin/themes/default/template/controllers/orders/_product_line.tpl b/admin/themes/default/template/controllers/orders/_product_line.tpl index 1e44a648f..299c0fa22 100644 --- a/admin/themes/default/template/controllers/orders/_product_line.tpl +++ b/admin/themes/default/template/controllers/orders/_product_line.tpl @@ -29,6 +29,9 @@ {if $data.is_back_order} {l s='overbooked'} {/if} + {if isset($data['connected_rooms'][$data['id_room']]) && $data['connected_rooms'][$data['id_room']]|@count > 0} + {include file="controllers/products/connected-rooms.tpl" htl_connected_rooms=$data['connected_rooms'][$data['id_room']]} + {/if} diff --git a/admin/themes/default/template/controllers/products/configuration.tpl b/admin/themes/default/template/controllers/products/configuration.tpl index 7f0bc0f60..c960e3f53 100644 --- a/admin/themes/default/template/controllers/products/configuration.tpl +++ b/admin/themes/default/template/controllers/products/configuration.tpl @@ -67,6 +67,14 @@ + + + {hook h='displayHotelRoomListTableHeaderColumn'} {l s='--'} @@ -108,6 +116,16 @@ {* Since the data can also be used from post incase of errors, which will cause issues with the id index *} + + {if isset($room_info['id'])} + + + + {/if} + {if isset($room_info['id'])} {hook h='displayHotelRoomListTableRowColumn' index=$key id_room=$room_info['id']} {else} @@ -1885,6 +1903,141 @@ return false; } } + //connected modal + $(document).on('click', '.connectedRoomModal', function(e) { + e.preventDefault(); + + let roomId = $(this).data('id-room'); + let hotelId = $(this).data('id-hotel'); + let currentRoomType = $('[name="id_product"]').val(); + $.ajax({ + type: 'POST', + url: prod_link, + dataType: 'json', + data: { + ajax: true, + action: 'getModalConnectedRooms', + room_id: roomId, + hotel_id: hotelId, + current_roomtype: currentRoomType, + }, + success: function(response) { + $('#modalLoader').remove(); + if (response.success) { + if ($('#connectedRoomModal').length) { + $('#connectedRoomModal').modal('hide'); + $('#connectedRoomModal').data('bs.modal', null); + $('#connectedRoomModal').remove(); + } + $('.modal-backdrop').remove(); + $('body').removeClass('modal-open'); + $("#footer").next(".bootstrap").append(response.html); + $('#connectedRoomModal').modal({ + backdrop: 'static', + keyboard: true + }); + } else { + alert(response.message); + } + } + + + }); + }); + function filterRoomsByType() { + var selectedType = $('#connect_room_type').val(); + var firstVisible = null; + $('#connect_room option').each(function() { + if ($(this).data('type') == selectedType) { + $(this).show(); + if (!firstVisible) firstVisible = $(this); + } else { + $(this).hide(); + } + }); + if (firstVisible) { + firstVisible.prop('selected', true); + } + } + $(document).on('change', '#connect_room_type', function() { + filterRoomsByType(); + }); + $(document).on('shown.bs.modal', '#connectedRoomModal', function() { + filterRoomsByType(); + }); + //add connected room + $(document).on('click', '#save_connected_room', function() { + var mainRoomId = $('#connected_room_main_id').val(); + var connectedRoomId = $('#connect_room').val(); + var hotelId = $('#hotel_id').val(); + let currentRoomType = $('[name="id_product"]').val(); + if (!connectedRoomId) { + alert('Please select a room'); + return; + } + $.ajax({ + url: prod_link, + type: 'POST', + dataType: 'json', + data: { + ajax: true, + action: 'ManageConnectedRoom', + mode: 'add', + hotel_id: hotelId, + room_id: mainRoomId, + connected_room_id: connectedRoomId, + current_roomtype: currentRoomType, + }, + beforeSend: function() { + $('#save_connected_room').prop('disabled', true); + }, + success: function(response) { + if (response.success) { + showSuccessMessage(response.message); + $('#connectedRoomModal .modal-content').replaceWith($(response.html).find('.modal-content')); + filterRoomsByType(); + } else { + alert(response.message); + } + }, + complete: function() { + $('#save_connected_room').prop('disabled', false); + } + }); + }); + //remove connected room + $(document).on('click', '.delete-connected-room', function() { + var roomId = $(this).data('room-id'); + var connectedRoomId = $(this).data('connected-room-id'); + var connectedId = $(this).data('connected-id'); + var hotelId = $(this).data('hotel-id'); + let currentRoomType = $('[name="id_product"]').val(); + $.ajax({ + url: prod_link, + type: 'POST', + dataType: 'json', + data: { + ajax: true, + action: 'ManageConnectedRoom', + mode: 'delete', + room_id: roomId, + connected_room_id: connectedRoomId, + connected_id: connectedId, + hotel_id: hotelId, + current_roomtype: currentRoomType, + }, + success: function(response) { + if (response.success) { + showSuccessMessage(response.message); + $('#connectedRoomModal .modal-content').replaceWith($(response.html).find( + '.modal-content')); + filterRoomsByType(); + } else { + alert(response.message); + } + } + }); + }); }); diff --git a/admin/themes/default/template/controllers/products/connected-rooms.tpl b/admin/themes/default/template/controllers/products/connected-rooms.tpl new file mode 100644 index 000000000..2e33a4daf --- /dev/null +++ b/admin/themes/default/template/controllers/products/connected-rooms.tpl @@ -0,0 +1,87 @@ +{** +* NOTICE OF LICENSE +* +* This source file is subject to the Open Software License version 3.0 +* that is bundled with this package in the file LICENSE.md +* It is also available through the world-wide-web at this URL: +* https://opensource.org/license/osl-3-0-php +* If you did not receive a copy of the license and are unable to +* obtain it through the world-wide-web, please send an email +* to support@qloapps.com so we can send you a copy immediately. +* +* DISCLAIMER +* +* Do not edit or add to this file if you wish to upgrade this module to a newer +* versions in the future. If you wish to customize this module for your needs +* please refer to https://store.webkul.com/customisation-guidelines for more information. +* +* @author Webkul IN +* @copyright Since 2010 Webkul +* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0 +*} +{if isset($htl_connected_rooms) && $htl_connected_rooms|@count > 0} + + +
+ + +
+
+{/if} + \ No newline at end of file diff --git a/admin/themes/default/template/controllers/products/modal-connected-rooms.tpl b/admin/themes/default/template/controllers/products/modal-connected-rooms.tpl new file mode 100644 index 000000000..38b34e2d8 --- /dev/null +++ b/admin/themes/default/template/controllers/products/modal-connected-rooms.tpl @@ -0,0 +1,148 @@ +{** +* NOTICE OF LICENSE +* +* This source file is subject to the Open Software License version 3.0 +* that is bundled with this package in the file LICENSE.md +* It is also available through the world-wide-web at this URL: +* https://opensource.org/license/osl-3-0-php +* If you did not receive a copy of the license and are unable to +* obtain it through the world-wide-web, please send an email +* to support@qloapps.com so we can send you a copy immediately. +* +* DISCLAIMER +* +* Do not edit or add to this file if you wish to upgrade this module to a newer +* versions in the future. If you wish to customize this module for your needs +* please refer to https://store.webkul.com/customisation-guidelines for more information. +* +* @author Webkul IN +* @copyright Since 2010 Webkul +* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0 +*} + \ No newline at end of file diff --git a/controllers/admin/AdminOrdersController.php b/controllers/admin/AdminOrdersController.php index 30026bcca..1bc548457 100644 --- a/controllers/admin/AdminOrdersController.php +++ b/controllers/admin/AdminOrdersController.php @@ -3485,6 +3485,7 @@ class AdminOrdersControllerCore extends AdminController $order_detail_data[$key]['amt_with_qty_tax_incl'] = $value['total_price_tax_incl']; $order_detail_data[$key]['room_type_info'] = $objHotelRoomType->getRoomTypeInfoByIdProduct($value['id_product']); $order_detail_data[$key]['total_room_tax'] = $order_detail_data[$key]['total_room_price_ti'] - $order_detail_data[$key]['total_room_price_te']; + $order_detail_data[$key]['connected_rooms'] = HotelRoomConnected::getConnectedRoomsByHotel($value['id_hotel'], Context::getContext()->language->id, $value['id_room'], true); if (isset($value['refund_info']) && $value['refund_info']['refunded'] diff --git a/modules/hotelreservationsystem/classes/HotelCartBookingData.php b/modules/hotelreservationsystem/classes/HotelCartBookingData.php index be3cabc94..d57e208d0 100644 --- a/modules/hotelreservationsystem/classes/HotelCartBookingData.php +++ b/modules/hotelreservationsystem/classes/HotelCartBookingData.php @@ -1331,6 +1331,7 @@ class HotelCartBookingData extends ObjectModel ); $cart_detail_data[$key]['amt_with_qty'] = $roomTypeDateRangePrice['total_price_tax_excl']; + $cart_detail_data[$key]['connected_rooms'] = HotelRoomConnected::getConnectedRoomsByHotel($obj_htl_room_info->id_hotel, Context::getContext()->language->id, $obj_htl_room_info->id, true); } } if ($cart_detail_data) { diff --git a/modules/hotelreservationsystem/classes/HotelReservationSystemDb.php b/modules/hotelreservationsystem/classes/HotelReservationSystemDb.php index ca44a2826..1260200cd 100644 --- a/modules/hotelreservationsystem/classes/HotelReservationSystemDb.php +++ b/modules/hotelreservationsystem/classes/HotelReservationSystemDb.php @@ -552,6 +552,14 @@ class HotelReservationSystemDb SELECT 5, `id_lang`, 'Hotel Amenities Block', 'Configure Hotels Amenities settings. You can display hotel amenities images using this block. This block will be displayed on home page.' FROM `"._DB_PREFIX_."lang` ORDER BY `id_lang`;", + "CREATE TABLE IF NOT EXISTS `"._DB_PREFIX_."htl_connected_room` ( + `id_connected_room` int(11) NOT NULL AUTO_INCREMENT, + `id_room_information` int(11) NOT NULL, + `id_room` int(11) NOT NULL, + `date_add` datetime NOT NULL, + `date_upd` datetime NOT NULL, + PRIMARY KEY (`id_connected_room`) + ) ENGINE="._MYSQL_ENGINE_." DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;", ); } @@ -615,7 +623,8 @@ class HotelReservationSystemDb `'._DB_PREFIX_.'htl_room_type_bed_type`, `'._DB_PREFIX_.'htl_access`, `'._DB_PREFIX_.'htl_settings_link`, - `'._DB_PREFIX_.'htl_settings_link_lang`' + `'._DB_PREFIX_.'htl_settings_link_lang` + `'._DB_PREFIX_.'htl_connected_room`' ); } } diff --git a/modules/hotelreservationsystem/classes/HotelRoomConnected.php b/modules/hotelreservationsystem/classes/HotelRoomConnected.php new file mode 100644 index 000000000..be9ccaf6b --- /dev/null +++ b/modules/hotelreservationsystem/classes/HotelRoomConnected.php @@ -0,0 +1,86 @@ + 'htl_connected_room', + 'primary' => 'id_connected_room', + 'fields' => array( + 'id_room_information' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true), + 'id_room' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true), + 'date_add' => array('type' => self::TYPE_DATE), + 'date_upd' => array('type' => self::TYPE_DATE), + ), + ); + public static function getConnectedRoomsByHotel($idHotel, $idLang, $roomId = null, $isConnected = true) + { + if ($isConnected) { + $sql = 'SELECT + hcr.id_connected_room,hcr.id_room_information,hcr.id_room,main_room.id_hotel,main_room.room_num AS main_room_num,connected_room.room_num AS connected_room_num, + main_pl.name AS main_room_type,conn_pl.name AS connected_room_type + FROM `' . _DB_PREFIX_ . 'htl_connected_room` hcr + INNER JOIN `' . _DB_PREFIX_ . 'htl_room_information` main_room ON main_room.id = hcr.id_room_information + INNER JOIN `' . _DB_PREFIX_ . 'htl_room_information` connected_room ON connected_room.id = hcr.id_room + LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` main_pl ON (main_pl.id_product = main_room.id_product AND main_pl.id_lang = ' . $idLang . ') + LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` conn_pl ON (conn_pl.id_product = connected_room.id_product AND conn_pl.id_lang = ' . $idLang . ') + WHERE main_room.id_hotel = ' . $idHotel . ' + AND connected_room.id_hotel = ' . $idHotel; + if (!empty($roomId)) { + $sql .= ' AND main_room.id = ' . (int) $roomId; + $results = Db::getInstance()->executeS($sql); + } else { + $results = Db::getInstance()->executeS($sql); + } + $grouped = []; + foreach ($results as $row) { + $roomId = $row['id_room_information']; + $connType = $row['connected_room_type']; + if (!isset($grouped[$roomId])) { + $grouped[$roomId] = []; + } + if (!isset($grouped[$roomId][$connType])) { + $grouped[$roomId][$connType] = []; + } + $grouped[$roomId][$connType][] = $row; + } + return $grouped; + } else { + $sql = 'SELECT hri.*, pl.name AS room_type + FROM `' . _DB_PREFIX_ . 'htl_room_information` hri + LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON (pl.id_product = hri.id_product AND pl.id_lang = ' . $idLang . ') + WHERE hri.id_hotel = ' . (int) $idHotel . ' + AND hri.id != ' . (int) $roomId . ' + AND NOT EXISTS (SELECT 1 FROM `' . _DB_PREFIX_ . 'htl_connected_room` cr WHERE cr.id_room_information = ' . (int) $roomId . ' AND cr.id_room = hri.id) + ORDER BY hri.room_num ASC'; + return Db::getInstance()->executeS($sql); + } + } +} + diff --git a/modules/hotelreservationsystem/controllers/admin/AdminHotelRoomsBookingController.php b/modules/hotelreservationsystem/controllers/admin/AdminHotelRoomsBookingController.php index fac60d468..3cccd6ca7 100644 --- a/modules/hotelreservationsystem/controllers/admin/AdminHotelRoomsBookingController.php +++ b/modules/hotelreservationsystem/controllers/admin/AdminHotelRoomsBookingController.php @@ -387,6 +387,7 @@ class AdminHotelRoomsBookingController extends ModuleAdminController 'max_child_in_room' => Configuration::get('WK_GLOBAL_MAX_CHILD_IN_ROOM'), 'link' => $this->context->link, 'ALLOTMENT_MANUAL' => HotelBookingDetail::ALLOTMENT_MANUAL, + 'htl_connected_rooms' => HotelRoomConnected::getConnectedRoomsByHotel($this->id_hotel, $this->context->language->id, 0, true), )); if (Configuration::get('PS_BACKOFFICE_SEARCH_TYPE') == HotelBookingDetail::SEARCH_TYPE_OWS) { diff --git a/modules/hotelreservationsystem/define.php b/modules/hotelreservationsystem/define.php index e8a7cc909..141064a13 100644 --- a/modules/hotelreservationsystem/define.php +++ b/modules/hotelreservationsystem/define.php @@ -49,6 +49,7 @@ require_once 'classes/HotelRoomDisableDates.php'; require_once 'classes/HotelBranchRefundRules.php'; require_once 'classes/HotelBedType.php'; require_once 'classes/HotelRoomTypeBedType.php'; +require_once 'classes/HotelRoomConnected.php'; // linked products require_once 'classes/RoomTypeServiceProduct.php'; diff --git a/modules/hotelreservationsystem/views/templates/admin/hotel_rooms_booking/helpers/view/_partials/booking-rooms.tpl b/modules/hotelreservationsystem/views/templates/admin/hotel_rooms_booking/helpers/view/_partials/booking-rooms.tpl index 7c68db350..e83979106 100644 --- a/modules/hotelreservationsystem/views/templates/admin/hotel_rooms_booking/helpers/view/_partials/booking-rooms.tpl +++ b/modules/hotelreservationsystem/views/templates/admin/hotel_rooms_booking/helpers/view/_partials/booking-rooms.tpl @@ -40,7 +40,11 @@ {foreach from=$book_v['data']['available'] key=avai_k item=avai_v} - {$avai_v['room_num']|escape:'htmlall':'UTF-8'} {hook h='displayRoomNumAfter' data=$avai_v type='available'} + {$avai_v['room_num']|escape:'htmlall':'UTF-8'} + {if isset($htl_connected_rooms[$avai_v['id_room']]) && $htl_connected_rooms[$avai_v['id_room']]|@count > 0} + {include file="controllers/products/connected-rooms.tpl" htl_connected_rooms=$htl_connected_rooms[$avai_v['id_room']]} + {/if} + {hook h='displayRoomNumAfter' data=$avai_v type='available'} {assign var="is_full_date" value=($show_full_date && ($date_from|date_format:'%D' == $date_to|date_format:'%D'))} {dateFormat date=$date_from full=$is_full_date} - {dateFormat date=$date_to full=$is_full_date} {$avai_v['room_comment']|escape:'htmlall':'UTF-8'} From 0a5a5a0289e4c84bb7d99550f4b5d03736827fe4 Mon Sep 17 00:00:00 2001 From: azhar bhat Date: Tue, 17 Feb 2026 17:52:55 +0530 Subject: [PATCH 05/64] fixed ajax error --- controllers/admin/AdminProductsController.php | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/controllers/admin/AdminProductsController.php b/controllers/admin/AdminProductsController.php index 0a7acb76f..113547c93 100644 --- a/controllers/admin/AdminProductsController.php +++ b/controllers/admin/AdminProductsController.php @@ -5646,4 +5646,114 @@ class AdminProductsControllerCore extends AdminController return $tpl->fetch(); } + + public function ajaxProcessGetModalConnectedRooms() + { + $roomId = (int) Tools::getValue('room_id'); + $hotelId = (int) Tools::getValue('hotel_id'); + if (!$roomId || !$hotelId) { + die(json_encode([ + 'success' => false, + 'message' => $this->l('Invalid data.') + ])); + } + $objRoomType = new HotelRoomType(); + $connectedRooms = HotelRoomConnected::getConnectedRoomsByHotel($hotelId, $this->context->language->id, $roomId, true); + $notConnectedRooms = HotelRoomConnected::getConnectedRoomsByHotel($hotelId, $this->context->language->id, $roomId, false); + $this->context->smarty->assign(array( + 'htl_connected_rooms' => $connectedRooms, + 'htl_not_connected_rooms' => $notConnectedRooms, + 'main_room_id' => $roomId, + 'hotel_id' => $hotelId, + 'current_roomtype' => Tools::getValue('current_roomtype') + )); + $modalContent = $this->context->smarty->fetch( + 'controllers/products/modal-connected-rooms.tpl' + ); + die(json_encode([ + 'success' => true, + 'html' => $modalContent + ])); + } + + public function ajaxProcessManageConnectedRoom() + { + $mode = Tools::getValue('mode'); + $roomId = (int) Tools::getValue('room_id'); + $connectedRoomId = (int) Tools::getValue('connected_room_id'); + $connectedId = (int) Tools::getValue('connected_id'); + $hotelId = Tools::getValue('hotel_id'); + if (!$roomId || !$connectedRoomId) { + die(json_encode([ + 'success' => false, + 'message' => $this->l('Invalid room data.') + ])); + } + if ($roomId == $connectedRoomId) { + die(json_encode([ + 'success' => false, + 'message' => $this->l('You cannot connect a room to itself.') + ])); + } + if ($mode === 'add') { + $connection = new HotelRoomConnected(); + $connection->id_room_information = $roomId; + $connection->id_room = $connectedRoomId; + if ($connection->add()) { + $connectedRooms = HotelRoomConnected::getConnectedRoomsByHotel($hotelId, $this->context->language->id, $roomId, true); + $notConnectedRooms = HotelRoomConnected::getConnectedRoomsByHotel($hotelId, $this->context->language->id, $roomId, false); + $this->context->smarty->assign(array( + 'htl_connected_rooms' => $connectedRooms, + 'htl_not_connected_rooms' => $notConnectedRooms, + 'main_room_id' => $roomId, + 'hotel_id' => $hotelId, + 'current_roomtype' => Tools::getValue('current_roomtype') + )); + $modalContent = $this->context->smarty->fetch( + 'controllers/products/modal-connected-rooms.tpl' + ); + die(json_encode([ + 'html' => $modalContent, + 'success' => true, + 'message' => $this->l('Room connected successfully.') + ])); + } + + die(json_encode([ + 'success' => false, + 'message' => $this->l('Failed to connect room.') + ])); + } + //remove room + if ($mode === 'delete') { + $objHotelRoomConnected = new HotelRoomConnected($connectedId); + if ($objHotelRoomConnected->delete()) { + $connectedRooms = HotelRoomConnected::getConnectedRoomsByHotel($hotelId, $this->context->language->id, $roomId, true); + $notConnectedRooms = HotelRoomConnected::getConnectedRoomsByHotel($hotelId, $this->context->language->id, $roomId, false); + $this->context->smarty->assign(array( + 'htl_connected_rooms' => $connectedRooms, + 'htl_not_connected_rooms' => $notConnectedRooms, + 'main_room_id' => $roomId, + 'hotel_id' => $hotelId, + 'current_roomtype' => Tools::getValue('current_roomtype') + )); + $modalContent = $this->context->smarty->fetch( + 'controllers/products/modal-connected-rooms.tpl' + ); + die(json_encode([ + 'success' => true, + 'html' => $modalContent, + 'message' => $this->l('Connected room removed successfully.') + ])); + } + die(json_encode([ + 'success' => false, + 'message' => $this->l('Failed to remove connected room.') + ])); + } + die(json_encode([ + 'success' => false, + 'message' => $this->l('Invalid action.') + ])); + } } From 7756fb70713579f270679d8b42b31f81ac579f92 Mon Sep 17 00:00:00 2001 From: azhar bhat Date: Sat, 4 Apr 2026 17:10:32 +0530 Subject: [PATCH 06/64] BO: harden connected rooms (permissions, escaping, safe DB handling) --- .../controllers/products/configuration.tpl | 9 +- .../products/modal-connected-rooms.tpl | 26 +-- controllers/admin/AdminOrdersController.php | 10 +- controllers/admin/AdminProductsController.php | 149 +++++++++++++----- .../classes/HotelReservationSystemDb.php | 7 +- .../classes/HotelRoomConnected.php | 93 ++++++++--- 6 files changed, 218 insertions(+), 76 deletions(-) diff --git a/admin/themes/default/template/controllers/products/configuration.tpl b/admin/themes/default/template/controllers/products/configuration.tpl index c960e3f53..77f0e2b37 100644 --- a/admin/themes/default/template/controllers/products/configuration.tpl +++ b/admin/themes/default/template/controllers/products/configuration.tpl @@ -119,9 +119,9 @@ {if isset($room_info['id'])} + data-target="#connectedRoomModal" data-id-room="{$room_info['id']|intval}" + data-id-hotel="{$room_info['id_hotel']|intval}" data-room-num="{$room_info['room_num']|escape:'html':'UTF-8'}" + data-room-type="{$room_info['id_product']|intval}"> {/if} @@ -603,6 +603,7 @@ var rm_status = {$rm_status|@json_encode}; var confirmText = "{l s='Are you sure?' js=1}"; var removeDisableDateText = "{l s='Are you sure you want to remove the selected date range?' js=1}"; + var selectRoomText = "{l s='Please select a room' js=1}"; var currentRoomRow = 0; $(document).ready(function() { var tooltipCounter = 0; @@ -1972,7 +1973,7 @@ var hotelId = $('#hotel_id').val(); let currentRoomType = $('[name="id_product"]').val(); if (!connectedRoomId) { - alert('Please select a room'); + alert(selectRoomText); return; } $.ajax({ diff --git a/admin/themes/default/template/controllers/products/modal-connected-rooms.tpl b/admin/themes/default/template/controllers/products/modal-connected-rooms.tpl index 38b34e2d8..fe023de40 100644 --- a/admin/themes/default/template/controllers/products/modal-connected-rooms.tpl +++ b/admin/themes/default/template/controllers/products/modal-connected-rooms.tpl @@ -33,7 +33,7 @@
\ No newline at end of file + + diff --git a/controllers/admin/AdminOrdersController.php b/controllers/admin/AdminOrdersController.php index d7e024438..eb77bb77e 100644 --- a/controllers/admin/AdminOrdersController.php +++ b/controllers/admin/AdminOrdersController.php @@ -3484,7 +3484,15 @@ class AdminOrdersControllerCore extends AdminController $order_detail_data[$key]['amt_with_qty_tax_incl'] = $value['total_price_tax_incl']; $order_detail_data[$key]['room_type_info'] = $objHotelRoomType->getRoomTypeInfoByIdProduct($value['id_product']); $order_detail_data[$key]['total_room_tax'] = $order_detail_data[$key]['total_room_price_ti'] - $order_detail_data[$key]['total_room_price_te']; - $order_detail_data[$key]['connected_rooms'] = HotelRoomConnected::getConnectedRoomsByHotel($value['id_hotel'], Context::getContext()->language->id, $value['id_room'], true); + $order_detail_data[$key]['connected_rooms'] = array(); + if (class_exists('HotelRoomConnected')) { + $order_detail_data[$key]['connected_rooms'] = HotelRoomConnected::getConnectedRoomsByHotel( + (int) $value['id_hotel'], + (int) Context::getContext()->language->id, + (int) $value['id_room'], + true + ); + } if (isset($value['refund_info']) && $value['refund_info']['refunded'] diff --git a/controllers/admin/AdminProductsController.php b/controllers/admin/AdminProductsController.php index 113547c93..ddd932a4d 100644 --- a/controllers/admin/AdminProductsController.php +++ b/controllers/admin/AdminProductsController.php @@ -5647,8 +5647,39 @@ class AdminProductsControllerCore extends AdminController return $tpl->fetch(); } + /** + * Render connected rooms modal HTML for a given room/hotel. + * + * @param int $hotelId + * @param int $roomId + * @param int $currentRoomType + * + * @return string + */ + protected function getConnectedRoomsModalHtml($hotelId, $roomId, $currentRoomType) + { + $connectedRooms = HotelRoomConnected::getConnectedRoomsByHotel($hotelId, $this->context->language->id, $roomId, true); + $notConnectedRooms = HotelRoomConnected::getConnectedRoomsByHotel($hotelId, $this->context->language->id, $roomId, false); + $this->context->smarty->assign(array( + 'htl_connected_rooms' => $connectedRooms, + 'htl_not_connected_rooms' => $notConnectedRooms, + 'main_room_id' => (int) $roomId, + 'hotel_id' => (int) $hotelId, + 'current_roomtype' => (int) $currentRoomType, + )); + + return $this->context->smarty->fetch('controllers/products/modal-connected-rooms.tpl'); + } + public function ajaxProcessGetModalConnectedRooms() { + if ($this->tabAccess['edit'] !== 1) { + die(json_encode([ + 'success' => false, + 'message' => $this->l('Permission denied.') + ])); + } + $roomId = (int) Tools::getValue('room_id'); $hotelId = (int) Tools::getValue('hotel_id'); if (!$roomId || !$hotelId) { @@ -5657,19 +5688,25 @@ class AdminProductsControllerCore extends AdminController 'message' => $this->l('Invalid data.') ])); } - $objRoomType = new HotelRoomType(); - $connectedRooms = HotelRoomConnected::getConnectedRoomsByHotel($hotelId, $this->context->language->id, $roomId, true); - $notConnectedRooms = HotelRoomConnected::getConnectedRoomsByHotel($hotelId, $this->context->language->id, $roomId, false); - $this->context->smarty->assign(array( - 'htl_connected_rooms' => $connectedRooms, - 'htl_not_connected_rooms' => $notConnectedRooms, - 'main_room_id' => $roomId, - 'hotel_id' => $hotelId, - 'current_roomtype' => Tools::getValue('current_roomtype') - )); - $modalContent = $this->context->smarty->fetch( - 'controllers/products/modal-connected-rooms.tpl' + + if (!class_exists('HotelRoomConnected') || !HotelRoomConnected::isConnectedRoomTableAvailable()) { + die(json_encode([ + 'success' => false, + 'message' => $this->l('Connected rooms feature is not available. Please upgrade the Hotel Reservation System module.') + ])); + } + + $mainRoomHotelId = (int) Db::getInstance()->getValue( + 'SELECT `id_hotel` FROM `' . _DB_PREFIX_ . 'htl_room_information` WHERE `id` = ' . (int) $roomId ); + if ($mainRoomHotelId !== (int) $hotelId) { + die(json_encode([ + 'success' => false, + 'message' => $this->l('Invalid room data.') + ])); + } + + $modalContent = $this->getConnectedRoomsModalHtml($hotelId, $roomId, (int) Tools::getValue('current_roomtype')); die(json_encode([ 'success' => true, 'html' => $modalContent @@ -5678,40 +5715,72 @@ class AdminProductsControllerCore extends AdminController public function ajaxProcessManageConnectedRoom() { + if ($this->tabAccess['edit'] !== 1) { + die(json_encode([ + 'success' => false, + 'message' => $this->l('Permission denied.') + ])); + } + $mode = Tools::getValue('mode'); $roomId = (int) Tools::getValue('room_id'); $connectedRoomId = (int) Tools::getValue('connected_room_id'); $connectedId = (int) Tools::getValue('connected_id'); - $hotelId = Tools::getValue('hotel_id'); + $hotelId = (int) Tools::getValue('hotel_id'); if (!$roomId || !$connectedRoomId) { die(json_encode([ 'success' => false, 'message' => $this->l('Invalid room data.') ])); } + + if (!class_exists('HotelRoomConnected') || !HotelRoomConnected::isConnectedRoomTableAvailable()) { + die(json_encode([ + 'success' => false, + 'message' => $this->l('Connected rooms feature is not available. Please upgrade the Hotel Reservation System module.') + ])); + } + if ($roomId == $connectedRoomId) { die(json_encode([ 'success' => false, 'message' => $this->l('You cannot connect a room to itself.') ])); } + + $mainRoomHotelId = (int) Db::getInstance()->getValue( + 'SELECT `id_hotel` FROM `' . _DB_PREFIX_ . 'htl_room_information` WHERE `id` = ' . (int) $roomId + ); + $connectedRoomHotelId = (int) Db::getInstance()->getValue( + 'SELECT `id_hotel` FROM `' . _DB_PREFIX_ . 'htl_room_information` WHERE `id` = ' . (int) $connectedRoomId + ); + if (!$hotelId || $mainRoomHotelId !== $hotelId || $connectedRoomHotelId !== $hotelId) { + die(json_encode([ + 'success' => false, + 'message' => $this->l('Invalid room data.') + ])); + } + + $currentRoomType = (int) Tools::getValue('current_roomtype'); + if ($mode === 'add') { + $existingConnectedId = (int) Db::getInstance()->getValue( + 'SELECT `id_connected_room` FROM `' . _DB_PREFIX_ . 'htl_connected_room` WHERE `id_room_information` = ' . (int) $roomId . ' AND `id_room` = ' . (int) $connectedRoomId + ); + if ($existingConnectedId) { + $modalContent = $this->getConnectedRoomsModalHtml($hotelId, $roomId, $currentRoomType); + die(json_encode([ + 'html' => $modalContent, + 'success' => true, + 'message' => $this->l('Room is already connected.') + ])); + } + $connection = new HotelRoomConnected(); $connection->id_room_information = $roomId; $connection->id_room = $connectedRoomId; if ($connection->add()) { - $connectedRooms = HotelRoomConnected::getConnectedRoomsByHotel($hotelId, $this->context->language->id, $roomId, true); - $notConnectedRooms = HotelRoomConnected::getConnectedRoomsByHotel($hotelId, $this->context->language->id, $roomId, false); - $this->context->smarty->assign(array( - 'htl_connected_rooms' => $connectedRooms, - 'htl_not_connected_rooms' => $notConnectedRooms, - 'main_room_id' => $roomId, - 'hotel_id' => $hotelId, - 'current_roomtype' => Tools::getValue('current_roomtype') - )); - $modalContent = $this->context->smarty->fetch( - 'controllers/products/modal-connected-rooms.tpl' - ); + $modalContent = $this->getConnectedRoomsModalHtml($hotelId, $roomId, $currentRoomType); die(json_encode([ 'html' => $modalContent, 'success' => true, @@ -5726,20 +5795,26 @@ class AdminProductsControllerCore extends AdminController } //remove room if ($mode === 'delete') { + if (!$connectedId) { + die(json_encode([ + 'success' => false, + 'message' => $this->l('Invalid data.') + ])); + } + + $matchesConnection = (int) Db::getInstance()->getValue( + 'SELECT `id_connected_room` FROM `' . _DB_PREFIX_ . 'htl_connected_room` WHERE `id_connected_room` = ' . (int) $connectedId . ' AND `id_room_information` = ' . (int) $roomId . ' AND `id_room` = ' . (int) $connectedRoomId + ); + if (!$matchesConnection) { + die(json_encode([ + 'success' => false, + 'message' => $this->l('Invalid data.') + ])); + } + $objHotelRoomConnected = new HotelRoomConnected($connectedId); if ($objHotelRoomConnected->delete()) { - $connectedRooms = HotelRoomConnected::getConnectedRoomsByHotel($hotelId, $this->context->language->id, $roomId, true); - $notConnectedRooms = HotelRoomConnected::getConnectedRoomsByHotel($hotelId, $this->context->language->id, $roomId, false); - $this->context->smarty->assign(array( - 'htl_connected_rooms' => $connectedRooms, - 'htl_not_connected_rooms' => $notConnectedRooms, - 'main_room_id' => $roomId, - 'hotel_id' => $hotelId, - 'current_roomtype' => Tools::getValue('current_roomtype') - )); - $modalContent = $this->context->smarty->fetch( - 'controllers/products/modal-connected-rooms.tpl' - ); + $modalContent = $this->getConnectedRoomsModalHtml($hotelId, $roomId, $currentRoomType); die(json_encode([ 'success' => true, 'html' => $modalContent, diff --git a/modules/hotelreservationsystem/classes/HotelReservationSystemDb.php b/modules/hotelreservationsystem/classes/HotelReservationSystemDb.php index 1260200cd..f907c111a 100644 --- a/modules/hotelreservationsystem/classes/HotelReservationSystemDb.php +++ b/modules/hotelreservationsystem/classes/HotelReservationSystemDb.php @@ -558,7 +558,10 @@ class HotelReservationSystemDb `id_room` int(11) NOT NULL, `date_add` datetime NOT NULL, `date_upd` datetime NOT NULL, - PRIMARY KEY (`id_connected_room`) + PRIMARY KEY (`id_connected_room`), + UNIQUE KEY `uniq_room_connection` (`id_room_information`, `id_room`), + KEY `idx_id_room_information` (`id_room_information`), + KEY `idx_id_room` (`id_room`) ) ENGINE="._MYSQL_ENGINE_." DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;", ); } @@ -623,7 +626,7 @@ class HotelReservationSystemDb `'._DB_PREFIX_.'htl_room_type_bed_type`, `'._DB_PREFIX_.'htl_access`, `'._DB_PREFIX_.'htl_settings_link`, - `'._DB_PREFIX_.'htl_settings_link_lang` + `'._DB_PREFIX_.'htl_settings_link_lang`, `'._DB_PREFIX_.'htl_connected_room`' ); } diff --git a/modules/hotelreservationsystem/classes/HotelRoomConnected.php b/modules/hotelreservationsystem/classes/HotelRoomConnected.php index be9ccaf6b..0bc358b55 100644 --- a/modules/hotelreservationsystem/classes/HotelRoomConnected.php +++ b/modules/hotelreservationsystem/classes/HotelRoomConnected.php @@ -23,6 +23,9 @@ class HotelRoomConnected extends ObjectModel { + /** @var bool|null */ + protected static $connectedRoomTableAvailable = null; + public $id_connected_room; public $id_room_information; public $id_room; @@ -39,48 +42,100 @@ class HotelRoomConnected extends ObjectModel 'date_upd' => array('type' => self::TYPE_DATE), ), ); + + /** + * Check whether the connected room table exists and is queryable. + * + * @return bool + */ + public static function isConnectedRoomTableAvailable() + { + if (self::$connectedRoomTableAvailable !== null) { + return (bool) self::$connectedRoomTableAvailable; + } + + $result = Db::getInstance()->executeS('SELECT 1 FROM `' . _DB_PREFIX_ . 'htl_connected_room` LIMIT 1'); + self::$connectedRoomTableAvailable = ($result !== false); + + return (bool) self::$connectedRoomTableAvailable; + } + + /** + * Fetch connected/not-connected rooms for a hotel. + * + * @param int $idHotel + * @param int $idLang + * @param int|null $roomId + * @param bool $isConnected + * + * @return array + */ public static function getConnectedRoomsByHotel($idHotel, $idLang, $roomId = null, $isConnected = true) { + $idHotel = (int) $idHotel; + $idLang = (int) $idLang; + $roomId = (int) $roomId; + if ($isConnected) { + if (!self::isConnectedRoomTableAvailable()) { + return array(); + } + $sql = 'SELECT hcr.id_connected_room,hcr.id_room_information,hcr.id_room,main_room.id_hotel,main_room.room_num AS main_room_num,connected_room.room_num AS connected_room_num, main_pl.name AS main_room_type,conn_pl.name AS connected_room_type FROM `' . _DB_PREFIX_ . 'htl_connected_room` hcr INNER JOIN `' . _DB_PREFIX_ . 'htl_room_information` main_room ON main_room.id = hcr.id_room_information INNER JOIN `' . _DB_PREFIX_ . 'htl_room_information` connected_room ON connected_room.id = hcr.id_room - LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` main_pl ON (main_pl.id_product = main_room.id_product AND main_pl.id_lang = ' . $idLang . ') - LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` conn_pl ON (conn_pl.id_product = connected_room.id_product AND conn_pl.id_lang = ' . $idLang . ') - WHERE main_room.id_hotel = ' . $idHotel . ' - AND connected_room.id_hotel = ' . $idHotel; - if (!empty($roomId)) { + LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` main_pl ON (main_pl.id_product = main_room.id_product AND main_pl.id_lang = ' . (int) $idLang . ') + LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` conn_pl ON (conn_pl.id_product = connected_room.id_product AND conn_pl.id_lang = ' . (int) $idLang . ') + WHERE main_room.id_hotel = ' . (int) $idHotel . ' + AND connected_room.id_hotel = ' . (int) $idHotel; + if ($roomId > 0) { $sql .= ' AND main_room.id = ' . (int) $roomId; - $results = Db::getInstance()->executeS($sql); - } else { - $results = Db::getInstance()->executeS($sql); } - $grouped = []; + + $results = Db::getInstance()->executeS($sql); + if ($results === false) { + return array(); + } + + $grouped = array(); foreach ($results as $row) { - $roomId = $row['id_room_information']; - $connType = $row['connected_room_type']; - if (!isset($grouped[$roomId])) { - $grouped[$roomId] = []; + $mainRoomId = (int) $row['id_room_information']; + $connType = (string) $row['connected_room_type']; + if (!isset($grouped[$mainRoomId])) { + $grouped[$mainRoomId] = array(); } - if (!isset($grouped[$roomId][$connType])) { - $grouped[$roomId][$connType] = []; + if (!isset($grouped[$mainRoomId][$connType])) { + $grouped[$mainRoomId][$connType] = array(); } - $grouped[$roomId][$connType][] = $row; + $grouped[$mainRoomId][$connType][] = $row; } return $grouped; } else { + // If the connected room table is not yet available (e.g., older installs), + // fall back to returning all rooms except the main room. + if (!self::isConnectedRoomTableAvailable()) { + $sql = 'SELECT hri.*, pl.name AS room_type + FROM `' . _DB_PREFIX_ . 'htl_room_information` hri + LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON (pl.id_product = hri.id_product AND pl.id_lang = ' . (int) $idLang . ') + WHERE hri.id_hotel = ' . (int) $idHotel . ' + AND hri.id != ' . (int) $roomId . ' + ORDER BY hri.room_num ASC'; + $results = Db::getInstance()->executeS($sql); + return ($results === false) ? array() : $results; + } + $sql = 'SELECT hri.*, pl.name AS room_type FROM `' . _DB_PREFIX_ . 'htl_room_information` hri - LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON (pl.id_product = hri.id_product AND pl.id_lang = ' . $idLang . ') + LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON (pl.id_product = hri.id_product AND pl.id_lang = ' . (int) $idLang . ') WHERE hri.id_hotel = ' . (int) $idHotel . ' AND hri.id != ' . (int) $roomId . ' AND NOT EXISTS (SELECT 1 FROM `' . _DB_PREFIX_ . 'htl_connected_room` cr WHERE cr.id_room_information = ' . (int) $roomId . ' AND cr.id_room = hri.id) ORDER BY hri.room_num ASC'; - return Db::getInstance()->executeS($sql); + $results = Db::getInstance()->executeS($sql); + return ($results === false) ? array() : $results; } } } - From 8d2c1294750ee0c558197cf19130500f3291c2ff Mon Sep 17 00:00:00 2001 From: azhar bhat Date: Wed, 8 Apr 2026 19:51:23 +0530 Subject: [PATCH 07/64] ui updated --- admin/themes/default/sass/admin-theme.sass | 1 + .../default/sass/partials/_qlotooltip.sass | 100 +++++++++ .../controllers/products/configuration.tpl | 125 ++++++++++-- .../controllers/products/connected-rooms.tpl | 103 +++++----- .../products/modal-connected-rooms.tpl | 193 ++++++++++-------- controllers/admin/AdminProductsController.php | 30 +-- .../classes/HotelRoomConnected.php | 47 +++++ 7 files changed, 418 insertions(+), 181 deletions(-) create mode 100644 admin/themes/default/sass/partials/_qlotooltip.sass diff --git a/admin/themes/default/sass/admin-theme.sass b/admin/themes/default/sass/admin-theme.sass index 33b4084b0..92f9e4282 100644 --- a/admin/themes/default/sass/admin-theme.sass +++ b/admin/themes/default/sass/admin-theme.sass @@ -85,6 +85,7 @@ @import "partials/multistore" @import "partials/product" @import "partials/skeleton-loading" + @import "partials/qlotooltip" //Controllers @import "controllers/carrier-wizard" diff --git a/admin/themes/default/sass/partials/_qlotooltip.sass b/admin/themes/default/sass/partials/_qlotooltip.sass new file mode 100644 index 000000000..369a3c790 --- /dev/null +++ b/admin/themes/default/sass/partials/_qlotooltip.sass @@ -0,0 +1,100 @@ +.qlo-tooltip + display: none + position: absolute + top: 130% + left: 50% + transform: translateX(-50%) + z-index: 99999 + font-size: 11px !important + line-height: 1.35 !important + font-family: "Open Sans", Arial, sans-serif !important + text-align: left + color: #666 !important + background: #ffffff !important + border: 1px solid #e5e5e5 !important + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.12) !important + padding: 10px 12px !important + border-radius: 4px !important + width: fit-content + min-width: 0 + max-width: none + +.qlo-tooltip-wrapper + position: relative + display: inline-block + cursor: pointer + color: #333 + +.qlo-tooltip-wrapper:hover .qlo-tooltip + display: block + +.qlo-tooltip.align-right + left: auto + right: 0 + transform: none + +.qlo-tooltip.align-left + left: 0 + transform: none + +.qlo-tooltip .qlo_tooltip_content, +.qlo-tooltip .qlo_tooltip_cont + width: 100% + +.qlo-tooltip .qlo_header + margin-bottom: 6px + +.qlo-tooltip .qlo_date + font-weight: 700 + font-size: 12px + color: #333 + white-space: nowrap + +.qlo-tooltip .qlo_body.grid + display: grid + grid-template-columns: repeat(2, minmax(0, max-content)) + gap: 10px 18px + justify-content: start + justify-items: start + width: max-content + +.qlo-tooltip .qlo_body.grid.single-col + grid-template-columns: 1fr + +.qlo-tooltip .qlo_body.grid.single-col .qlo_element + display: block + width: auto + +.qlo-tooltip.single-col + min-width: 0 + max-width: 240px + width: fit-content + +.qlo-tooltip .qlo_element_value + color: #666 + font-weight: 600 + +.qlo_tooltip_list + list-style: none !important + padding: 0 !important + margin: 0 !important + +.qlo_tooltip_list li + margin: 0 !important + padding: 0 !important + font-weight: 600 !important + color: #666 !important + +@media (max-width: 768px) + .qlo-tooltip + left: 0 + right: auto + transform: none + min-width: 0 + max-width: calc(100vw - 24px) + +.qlo-tooltip .qlo_element_heading + font-weight: 700 + color: #333 + margin-bottom: 2px + font-size: 11px diff --git a/admin/themes/default/template/controllers/products/configuration.tpl b/admin/themes/default/template/controllers/products/configuration.tpl index 77f0e2b37..884fa5a80 100644 --- a/admin/themes/default/template/controllers/products/configuration.tpl +++ b/admin/themes/default/template/controllers/products/configuration.tpl @@ -122,7 +122,12 @@ data-target="#connectedRoomModal" data-id-room="{$room_info['id']|intval}" data-id-hotel="{$room_info['id_hotel']|intval}" data-room-num="{$room_info['room_num']|escape:'html':'UTF-8'}" data-room-type="{$room_info['id_product']|intval}"> - + + + + {if isset($room_info['connected_rooms_count'])}{$room_info['connected_rooms_count']|intval}{else}0{/if} + + {/if} @@ -596,6 +601,26 @@ border: 1px solid #f2f2f2; margin-top: 10px; } + .connected-room-icon-wrapper { + display: inline-flex; + align-items: center; + gap: 4px; + justify-content: center; + } + .connected-room-count { + min-width: 14px; + height: 14px; + padding: 0 3px; + border-radius: 9px; + background: #f5f5f5; + color: #666; + border: 1px solid #d5d5d5; + font-size: 9px; + line-height: 12px; + font-weight: 600; + text-align: center; + box-shadow: none; + } diff --git a/admin/themes/default/template/controllers/products/modal-connected-rooms.tpl b/admin/themes/default/template/controllers/products/modal-connected-rooms.tpl index fe023de40..66548ac80 100644 --- a/admin/themes/default/template/controllers/products/modal-connected-rooms.tpl +++ b/admin/themes/default/template/controllers/products/modal-connected-rooms.tpl @@ -20,95 +20,75 @@ * @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0 *}