From f81882a31e069b94c61d114eaa83419a3a0aa2bd Mon Sep 17 00:00:00 2001 From: Amit sharma Date: Fri, 22 May 2026 14:34:46 +0530 Subject: [PATCH 01/78] Fixed: booking flow must work independently from blockcart module --- classes/Cart.php | 35 ++++++++- controllers/front/CartController.php | 20 ++++- controllers/front/ProductController.php | 9 ++- modules/blockcart/ajax-cart.js | 46 ------------ .../_partials/booking-form.tpl | 14 ++-- .../_partials/room_type_list.tpl | 2 +- themes/hotel-reservation-theme/global.tpl | 10 +++ .../js/cart-summary.js | 2 +- .../js/modules/blockcart/ajax-cart.js | 47 ------------ .../hotel-reservation-theme/js/occupancy.js | 73 ++++++++++++++++++- themes/hotel-reservation-theme/js/product.js | 64 +++++++++++++++- 11 files changed, 210 insertions(+), 112 deletions(-) diff --git a/classes/Cart.php b/classes/Cart.php index ad5ee4302..4deaee6d1 100644 --- a/classes/Cart.php +++ b/classes/Cart.php @@ -3930,6 +3930,38 @@ class CartCore extends ObjectModel } } + $totalRoomsCount = 0; + if ($htlCartData = HotelCartBookingData::getHotelCartBookingData(0)) { + foreach ($htlCartData as $roomTypeCart) { + $totalRoomsCount += $roomTypeCart['total_num_rooms']; + } + } + $nbTotalProducts = 0; + $objServiceProductCartDetail = new ServiceProductCartDetail(); + foreach ($products as &$product) { + if (!$product['booking_product']) { + if (Product::SELLING_PREFERENCE_STANDALONE == $product['selling_preference_type']) { + $nbTotalProducts += (int) $product['cart_quantity']; + } elseif (Product::SELLING_PREFERENCE_HOTEL_STANDALONE == $product['selling_preference_type'] + || Product::SELLING_PREFERENCE_HOTEL_STANDALONE_AND_WITH_ROOM_TYPE == $product['selling_preference_type'] + ) { + if ($serviceProducts = $objServiceProductCartDetail->getServiceProductsInCart( + $this->id, + [Product::SELLING_PREFERENCE_HOTEL_STANDALONE, Product::SELLING_PREFERENCE_HOTEL_STANDALONE_AND_WITH_ROOM_TYPE], + null, + 0, + null, + $product['id_product'] + )) { + foreach ($serviceProducts as $serviceProduct) { + if ($serviceProduct['id_hotel'] && $serviceProduct['id_product'] == $product['id_product']) { + $nbTotalProducts += (int) $serviceProduct['quantity']; + } + } + } + } + } + } $objHotelAdvancedPayment = new HotelAdvancedPayment(); $total_rooms_with_services_without_discount_te = $total_rooms + $total_demands + $total_additional_services + $total_additional_services_auto_add + $total_standalone_service_products; $total_rooms_with_services_without_discount_ti = $total_rooms_wt + $total_demands_wt + $total_additional_services_wt + $total_additional_services_auto_add_wt + $total_standalone_service_products_wt; @@ -3986,7 +4018,8 @@ class CartCore extends ObjectModel 'total_rooms_with_services_without_discount_ti' => $total_rooms_with_services_without_discount_ti, 'cart_total_without_discount_te' => $cart_total_without_discount_te, 'cart_total_without_discount_ti' => $cart_total_without_discount_ti, - 'total_tax_without_discount' => $total_tax_without_discount + 'total_tax_without_discount' => $total_tax_without_discount, + 'total_products_in_cart' => $totalRoomsCount + $nbTotalProducts, ); $hook = Hook::exec('actionCartSummary', $summary, null, true); if (is_array($hook)) { diff --git a/controllers/front/CartController.php b/controllers/front/CartController.php index 76f2e5982..080a605b5 100644 --- a/controllers/front/CartController.php +++ b/controllers/front/CartController.php @@ -315,7 +315,7 @@ class CartControllerCore extends FrontController } if ($occupancyRequiredForBooking && $operator == 'up') { - if ($occupancy = json_decode(Tools::getValue('occupancy'), true)) { + if ($occupancy = is_array(Tools::getValue('occupancy')) ? Tools::getValue('occupancy') : json_decode(Tools::getValue('occupancy'), true)) { $this->qty = count($occupancy); } else { $this->errors[] = Tools::displayError('Invalid occupnacy.'); @@ -341,7 +341,23 @@ class CartControllerCore extends FrontController $date_to = Tools::getValue('dateTo'); $date_from = date("Y-m-d H:i:s", strtotime($date_from)); $date_to = date("Y-m-d H:i:s", strtotime($date_to)); - $serviceProducts = json_decode(Tools::getValue('serviceProducts'),true); + + $serviceProducts = Tools::getValue('service_product'); + if (is_array($serviceProducts) && !empty($serviceProducts)) { + $formattedServiceProducts = []; + + foreach ($serviceProducts as $id_product => $data) { + $formattedServiceProducts[] = [ + 'id_product' => (int) $id_product, + 'quantity' => isset($data['quantity']) ? (int) $data['quantity'] : 1, + ]; + } + + $serviceProducts = $formattedServiceProducts; + } else { + $serviceProducts = json_decode(Tools::getValue('serviceProducts'), true); + } + $serviceProducts = is_array($serviceProducts) ? $serviceProducts : []; // valdiate occupancy if providede if ($operator == 'up' && $occupancyRequiredForBooking) { diff --git a/controllers/front/ProductController.php b/controllers/front/ProductController.php index ef4f509f7..a40ccab6f 100644 --- a/controllers/front/ProductController.php +++ b/controllers/front/ProductController.php @@ -1449,8 +1449,8 @@ class ProductControllerCore extends FrontController $response = array('status' => false); $idProduct = (int) Tools::getValue('id_product'); if ($this->product->booking_product) { - $dateFrom = Tools::getValue('room_check_in'); - $dateTo = Tools::getValue('room_check_out'); + $dateFrom = Tools::getValue('dateFrom'); + $dateTo = Tools::getValue('dateTo'); $occupancy = Tools::getValue('occupancy'); if (Configuration::get('PS_FRONT_ROOM_UNIT_SELECTION_TYPE') == HotelBookingDetail::PS_ROOM_UNIT_SELECTION_TYPE_OCCUPANCY) { if (!Validate::isOccupancy($occupancy)) { @@ -1474,9 +1474,9 @@ class ProductControllerCore extends FrontController )) { } } else { - $idHotel = Tools::getValue('service_id_hotel'); + $idHotel = Tools::getValue('id_hotel'); $id_product_option = Tools::getValue('id_product_option'); - $quantity = Tools::getValue('service_product_qty'); + $quantity = Tools::getValue('qty'); $this->assignServiceProductVars( $id_product_option, $quantity, @@ -1484,6 +1484,7 @@ class ProductControllerCore extends FrontController ); } + $this->context->smarty->assign('static_token', Tools::getToken(false)); $html = $this->context->smarty->fetch('_partials/booking-form.tpl'); $response['status'] = true; $response['html_booking_form'] = $html; diff --git a/modules/blockcart/ajax-cart.js b/modules/blockcart/ajax-cart.js index ca2ad2695..360d1b577 100644 --- a/modules/blockcart/ajax-cart.js +++ b/modules/blockcart/ajax-cart.js @@ -1450,49 +1450,3 @@ function getRoomsServiceProducts() return serviceProducts; } -function getBookingOccupancyDetails(bookingform, booking_product) -{ - let occupancy; - if (booking_product) { - if (occupancy_required_for_booking) { - $('.booking_guest_occupancy_conatiner .dropdown').removeClass('open'); - let selected_occupancy = $(bookingform).find(".occupancy_info_block.selected") - if (selected_occupancy.length) { - occupancy = []; - $(selected_occupancy).each(function(ind, element) { - if (parseInt($(element).find('.num_adults').val())) { - let child_ages = []; - $(element).find('.guest_child_age').each(function(index) { - if ($(this).val() > -1) { - child_ages.push($(this).val()); - } - }); - if ($(element).find('.num_children').val()) { - if (child_ages.length != $(element).find('.num_children').val()) { - $(bookingform).find('.booking_occupancy_wrapper').parent().addClass('open') - occupancy = false; - } - } - occupancy.push({ - 'adults': $(element).find('.num_adults').val(), - 'children': $(element).find('.num_children').val(), - 'child_ages': child_ages - }); - } else { - $(bookingform).find('.booking_occupancy_wrapper').parent().addClass('open') - occupancy = false; - } - }); - } else { - $(bookingform).find('.booking_occupancy_wrapper').parent().addClass('open') - occupancy = false; - } - } else { - occupancy = parseInt($(bookingform).find(".quantity_wanted").val()); - } - } else { - occupancy = parseInt($('#service_product_qty').val()); - } - - return occupancy; -} diff --git a/themes/hotel-reservation-theme/_partials/booking-form.tpl b/themes/hotel-reservation-theme/_partials/booking-form.tpl index 439363d23..e18c6cc6c 100644 --- a/themes/hotel-reservation-theme/_partials/booking-form.tpl +++ b/themes/hotel-reservation-theme/_partials/booking-form.tpl @@ -23,7 +23,7 @@
{block name='booking_form_content'} -
+ {block name='product_hidden_fields'}
{l s='Check-in'}    {l s='Check-out'}
- - + +
{/block}
@@ -192,7 +192,7 @@ @@ -231,7 +231,7 @@ {if isset($associated_hotels) && $associated_hotels}
- {foreach $associated_hotels as $hotel} {/foreach} @@ -276,7 +276,7 @@
- +
{if isset($quantity)}{$quantity|intval}{else}{if $product->minimal_quantity > 1}{$product->minimal_quantity}{else}1{/if}{/if}
@@ -292,7 +292,7 @@
{/if} {else} - + {/if} {/block} {block name='booking_form_actions'} diff --git a/themes/hotel-reservation-theme/_partials/room_type_list.tpl b/themes/hotel-reservation-theme/_partials/room_type_list.tpl index aaa0f3ec0..a85302439 100644 --- a/themes/hotel-reservation-theme/_partials/room_type_list.tpl +++ b/themes/hotel-reservation-theme/_partials/room_type_list.tpl @@ -118,7 +118,7 @@ {/if} {block name='room_type_list_room_book_now_button'}
- {l s='Book Now'} + {l s='Book Now'}
{/block}
diff --git a/themes/hotel-reservation-theme/global.tpl b/themes/hotel-reservation-theme/global.tpl index eaa7cd71a..7d8bfda70 100644 --- a/themes/hotel-reservation-theme/global.tpl +++ b/themes/hotel-reservation-theme/global.tpl @@ -37,6 +37,9 @@ {addJsDef currencySign=$currency->sign|html_entity_decode:2:"UTF-8"} {addJsDef currencyFormat=$currency->format|intval} {addJsDef currencyBlank=$currency->blank|intval} +{addJsDef currency_sign=$currency->sign} +{addJsDef currency_format=$currency->format} +{addJsDef currency_blank=$currency->blank} {addJsDef isLogged=$is_logged|intval} {addJsDef isGuest=$is_guest|intval} {addJsDef page_name=$page_name|escape:'html':'UTF-8'} @@ -62,6 +65,7 @@ {addJsDef is_occupancy_wise_search=$is_occupancy_wise_search|boolval} {addJsDef occupancy_required_for_booking=$occupancy_required_for_booking|boolval} {addJsDef max_child_age=$max_child_age} +{addJsDef hasDeliveryAddress=(isset($cart->id_address_delivery) && $cart->id_address_delivery)} {addJsDefL name='max_occupancy_reached_txt'}{l s='Maximum room occupancy reached' js=1}{/addJsDefL} {addJsDefL name='max_adults_txt'}{l s='Maximum adult occupancy reached' js=1}{/addJsDefL} @@ -81,4 +85,10 @@ {addJsDefL name='years_txt'}{l s='years' js=1}{/addJsDefL} {addJsDefL name='all_children_txt'}{l s='All Children' js=1}{/addJsDefL} {addJsDefL name='invalid_occupancy_txt'}{l s='Invalid occupancy(adults/children) found.' js=1}{/addJsDefL} +{addJsDefL name=customizationIdMessage}{l s='Customization #' mod='blockcart' js=1}{/addJsDefL} +{addJsDefL name=removingLinkText}{l s='remove this product from my cart' mod='blockcart' js=1}{/addJsDefL} +{addJsDefL name=freeShippingTranslation}{l s='Free shipping!' mod='blockcart' js=1}{/addJsDefL} +{addJsDefL name=freeProductTranslation}{l s='Free!' mod='blockcart' js=1}{/addJsDefL} +{addJsDefL name=delete_txt}{l s='Delete' mod='blockcart' js=1}{/addJsDefL} +{addJsDefL name=toBeDetermined}{l s='To be determined' mod='blockcart' js=1}{/addJsDefL} {/strip} diff --git a/themes/hotel-reservation-theme/js/cart-summary.js b/themes/hotel-reservation-theme/js/cart-summary.js index d715077f5..7e3c538ca 100644 --- a/themes/hotel-reservation-theme/js/cart-summary.js +++ b/themes/hotel-reservation-theme/js/cart-summary.js @@ -942,8 +942,8 @@ function updateCartSummary(json) $('input[name=quantity_' + key_for_blockcart_nocustom + '_hidden]').val(product_list[i].id_customization? product_list[i].quantity_without_customization : product_list[i].cart_quantity); if (typeof(product_list[i].customizationQuantityTotal) !== 'undefined' && product_list[i].customizationQuantityTotal > 0) $('#cart_quantity_custom_' + key_for_blockcart).html(product_list[i].customizationQuantityTotal); - nbrProducts += parseInt(product_list[i].quantity); } + nbrProducts += parseInt(json.total_products_in_cart); // Update discounts var discount_count = 0; diff --git a/themes/hotel-reservation-theme/js/modules/blockcart/ajax-cart.js b/themes/hotel-reservation-theme/js/modules/blockcart/ajax-cart.js index ca2ad2695..c200f5a15 100644 --- a/themes/hotel-reservation-theme/js/modules/blockcart/ajax-cart.js +++ b/themes/hotel-reservation-theme/js/modules/blockcart/ajax-cart.js @@ -1449,50 +1449,3 @@ function getRoomsServiceProducts() return serviceProducts; } - -function getBookingOccupancyDetails(bookingform, booking_product) -{ - let occupancy; - if (booking_product) { - if (occupancy_required_for_booking) { - $('.booking_guest_occupancy_conatiner .dropdown').removeClass('open'); - let selected_occupancy = $(bookingform).find(".occupancy_info_block.selected") - if (selected_occupancy.length) { - occupancy = []; - $(selected_occupancy).each(function(ind, element) { - if (parseInt($(element).find('.num_adults').val())) { - let child_ages = []; - $(element).find('.guest_child_age').each(function(index) { - if ($(this).val() > -1) { - child_ages.push($(this).val()); - } - }); - if ($(element).find('.num_children').val()) { - if (child_ages.length != $(element).find('.num_children').val()) { - $(bookingform).find('.booking_occupancy_wrapper').parent().addClass('open') - occupancy = false; - } - } - occupancy.push({ - 'adults': $(element).find('.num_adults').val(), - 'children': $(element).find('.num_children').val(), - 'child_ages': child_ages - }); - } else { - $(bookingform).find('.booking_occupancy_wrapper').parent().addClass('open') - occupancy = false; - } - }); - } else { - $(bookingform).find('.booking_occupancy_wrapper').parent().addClass('open') - occupancy = false; - } - } else { - occupancy = parseInt($(bookingform).find(".quantity_wanted").val()); - } - } else { - occupancy = parseInt($('#service_product_qty').val()); - } - - return occupancy; -} diff --git a/themes/hotel-reservation-theme/js/occupancy.js b/themes/hotel-reservation-theme/js/occupancy.js index 56b59edea..1f463b85c 100644 --- a/themes/hotel-reservation-theme/js/occupancy.js +++ b/themes/hotel-reservation-theme/js/occupancy.js @@ -19,9 +19,30 @@ * @copyright Since 2010 Webkul * @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0 */ - - $(document).ready(function(){ + $(document).off('click', '.ajax_add_to_cart_button').on('click', '.ajax_add_to_cart_button', function (e) { + e.preventDefault(); + var baseUrl = $(this).attr('href'); + var occupancy = getBookingOccupancyDetails($(this).closest('.booking_room_fields'), true); + if ($(this).prop('disabled') != 'disabled' && occupancy) { + var occupancyParams = ''; + if (Array.isArray(occupancy)) { + $.each(occupancy, function(i, room) { + occupancyParams += '&occupancy[' + i + '][adults]=' + encodeURIComponent(room.adults); + occupancyParams += '&occupancy[' + i + '][children]=' + encodeURIComponent(room.children); + if (room.child_ages && room.child_ages.length) { + $.each(room.child_ages, function(_, age) { + occupancyParams += '&occupancy[' + i + '][child_ages][]=' + encodeURIComponent(age); + }); + } + }); + } else { + occupancyParams += '&qty=' + encodeURIComponent(occupancy); + } + window.location.href = baseUrl + occupancyParams; + } + }); + $(document).on('click', '.booking_occupancy_wrapper .remove-room-link', function(e) { e.preventDefault(); @@ -412,3 +433,51 @@ function resetOccupancyField(booking_occupancy_wrapper) } }); } + + +function getBookingOccupancyDetails(bookingform, booking_product) +{ + let occupancy; + if (booking_product) { + if (occupancy_required_for_booking) { + $('.booking_guest_occupancy_conatiner .dropdown').removeClass('open'); + let selected_occupancy = $(bookingform).find(".occupancy_info_block.selected") + if (selected_occupancy.length) { + occupancy = []; + $(selected_occupancy).each(function(ind, element) { + if (parseInt($(element).find('.num_adults').val())) { + let child_ages = []; + $(element).find('.guest_child_age').each(function(index) { + if ($(this).val() > -1) { + child_ages.push($(this).val()); + } + }); + if ($(element).find('.num_children').val()) { + if (child_ages.length != $(element).find('.num_children').val()) { + $(bookingform).find('.booking_occupancy_wrapper').parent().addClass('open') + occupancy = false; + } + } + occupancy.push({ + 'adults': $(element).find('.num_adults').val(), + 'children': $(element).find('.num_children').val(), + 'child_ages': child_ages + }); + } else { + $(bookingform).find('.booking_occupancy_wrapper').parent().addClass('open') + occupancy = false; + } + }); + } else { + $(bookingform).find('.booking_occupancy_wrapper').parent().addClass('open') + occupancy = false; + } + } else { + occupancy = parseInt($(bookingform).find(".quantity_wanted").val()); + } + } else { + occupancy = parseInt($('#service_product_qty').val()); + } + + return occupancy; +} \ No newline at end of file diff --git a/themes/hotel-reservation-theme/js/product.js b/themes/hotel-reservation-theme/js/product.js index b0771177c..3da768218 100644 --- a/themes/hotel-reservation-theme/js/product.js +++ b/themes/hotel-reservation-theme/js/product.js @@ -1269,7 +1269,7 @@ function addProductToRoomType(that) { } else { $('').attr({ id: 'service_product_'+ id_product, - name: 'service_product['+ id_product +'][]', + name: 'service_product['+ id_product +'][quantity]', class: 'service_product', 'data-id_product': id_product, value: qty @@ -1590,4 +1590,66 @@ function getBookingOccupancy() } return occupancy; +} + +function resetRoomtypeServices(refresh = true) { + $('.room_demands_container').find('input.id_room_type_demand:checked').prop('checked', false).uniform(); + $('#additional_products').empty(); + $('#additional_products div') + $('.remove_roomtype_product').text(select_txt).removeClass('btn-danger remove_roomtype_product').addClass('btn-success add_roomtype_product'); + if (refresh) { + BookingForm.refresh(); + } +} + +function disableRoomTypeDemands(show) { + if (show) { + $('.room_demands_container_overlay').show(); + $('.room_demands_container').find('input:checkbox.id_room_type_demand').prop('checked', false); + $('.room_demand_block').find('.id_room_type_demand').prop('checked', false).parent().removeClass('checked'); + $('.room_demands_container').find('input:checkbox.id_room_type_demand').attr('disabled', 'disabled'); + } else { + $('.room_demands_container_overlay').hide(); + $('.room_demands_container').find('input:checkbox.id_room_type_demand').removeAttr('disabled'); + $('.room_demands_container').find('.checker').removeClass('disabled'); + } +} + +function disableRoomTypeServices(disable) { + if (disable) { + resetRoomtypeServices(false); + $('#service_products_cont').find('button.add_roomtype_product').attr('disabled', 'disabled'); + $('#service_products_cont').find('.qty_container .qty_direction a').attr('disabled', 'disabled'); + } else { + $('#service_products_cont').find('button.add_roomtype_product').removeAttr('disabled'); + $('#service_products_cont').find('.qty_container .qty_direction a').removeAttr('disabled'); + } +} + +function getRoomsExtraDemands() +{ + var roomDemands = []; + + $('input:checkbox.id_room_type_demand:checked').each(function () { + roomDemands.push({ + 'id_global_demand':$(this).val(), + 'id_option': $(this).closest('.room_demand_block').find('.id_option').val() + }); + }); + + return roomDemands; +} + +function getRoomsServiceProducts() +{ + var serviceProducts = []; + + $('#additional_products input.service_product').each(function () { + serviceProducts.push({ + 'id_product': $(this).data('id_product'), + 'quantity':$(this).val(), + }); + }); + + return serviceProducts; } \ No newline at end of file From debabcc33b5983e4f6c33d126603ed5c79328e1a Mon Sep 17 00:00:00 2001 From: Amit sharma Date: Fri, 22 May 2026 16:36:37 +0530 Subject: [PATCH 02/78] =?UTF-8?q?Fixed:=20Fix=20CartController=20error=20h?= =?UTF-8?q?andling=20using=C2=A0=20errors=20with=20blockcart=20AJAX=20chec?= =?UTF-8?q?ks=20and=20checkout=20redirects?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Name --- controllers/front/CartController.php | 21 ++++++++++++++++++--- controllers/front/OrderOpcController.php | 10 ++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/controllers/front/CartController.php b/controllers/front/CartController.php index 080a605b5..cbfe4071f 100644 --- a/controllers/front/CartController.php +++ b/controllers/front/CartController.php @@ -105,6 +105,9 @@ class CartControllerCore extends FrontController } } + Tools::redirect('index.php?controller=order&'.(isset($this->id_product) ? 'ipa='.$this->id_product : '')); + } elseif ($this->errors && !$this->ajax) { + $this->context->cookie->__set('cart_errors', json_encode($this->errors)); Tools::redirect('index.php?controller=order&'.(isset($this->id_product) ? 'ipa='.$this->id_product : '')); } } elseif (!$this->isTokenValid()) { @@ -435,7 +438,11 @@ class CartControllerCore extends FrontController $total_available_rooms = $hotelRoomData['stats']['num_avail']; if (Tools::getValue('op', 'up') == 'up') { if ($total_available_rooms < $req_rm) { - die(json_encode(array('status' => 'unavailable_quantity', 'avail_rooms' => $total_available_rooms))); + if (Module::isInstalled('blockcart') && Module::isEnabled('blockcart') && Configuration::get('PS_BLOCK_CART_AJAX')) { + die(json_encode(array('status' => 'unavailable_quantity', 'avail_rooms' => $total_available_rooms))); + } else { + $this->errors[] = Tools::displayError('All rooms are sold out for the selected dates. Please try with different dates.'); + } } else { // validate service products if available if ($serviceProducts) { @@ -466,10 +473,18 @@ class CartControllerCore extends FrontController } } } else { - die(json_encode(array('status' => 'failed3'))); + if (Module::isInstalled('blockcart') && Module::isEnabled('blockcart') && Configuration::get('PS_BLOCK_CART_AJAX')) { + die(json_encode(array('status' => 'failed3'))); + } else { + $this->errors[] = Tools::displayError('An error occurred while processing your request.'); + } } } else { - die(json_encode(array('status' => 'failed4'))); + if (Module::isInstalled('blockcart') && Module::isEnabled('blockcart') && Configuration::get('PS_BLOCK_CART_AJAX')) { + die(json_encode(array('status' => 'failed4'))); + } else { + $this->errors[] = Tools::displayError('An error occurred while processing your request.'); + } } } } else { diff --git a/controllers/front/OrderOpcController.php b/controllers/front/OrderOpcController.php index d1814c445..cd3ed1624 100644 --- a/controllers/front/OrderOpcController.php +++ b/controllers/front/OrderOpcController.php @@ -44,6 +44,16 @@ class OrderOpcControllerCore extends ParentOrderController { parent::init(); + if ($this->context->cookie->__isset('cart_errors')) { + $cartErrors = json_decode($this->context->cookie->__get('cart_errors'), true); + if (is_array($cartErrors)) { + foreach ($cartErrors as $cartError) { + $this->errors[] = $cartError; + } + } + $this->context->cookie->__unset('cart_errors'); + } + if ($this->nbProducts) { $this->context->smarty->assign('virtual_cart', $this->context->cart->isVirtualCart()); } From 3d6b493b5a7878174c24bd3b0d95a2fdc3106bd8 Mon Sep 17 00:00:00 2001 From: Amit sharma Date: Wed, 27 May 2026 17:34:56 +0530 Subject: [PATCH 03/78] remove unused code and use optimized way to fix this issue --- controllers/front/CartController.php | 45 ++++------ controllers/front/OrderOpcController.php | 10 --- controllers/front/ProductController.php | 4 +- modules/blockcart/ajax-cart.js | 89 ++++--------------- .../_partials/booking-form.tpl | 4 +- .../_partials/room_type_list.tpl | 2 +- themes/hotel-reservation-theme/global.tpl | 9 -- .../js/cart-summary.js | 4 +- themes/hotel-reservation-theme/js/category.js | 87 +++++++++++++++++- .../js/modules/blockcart/ajax-cart.js | 86 ++++-------------- .../hotel-reservation-theme/js/occupancy.js | 23 ----- themes/hotel-reservation-theme/order-opc.tpl | 2 + 12 files changed, 148 insertions(+), 217 deletions(-) diff --git a/controllers/front/CartController.php b/controllers/front/CartController.php index cbfe4071f..f646f1be3 100644 --- a/controllers/front/CartController.php +++ b/controllers/front/CartController.php @@ -105,9 +105,6 @@ class CartControllerCore extends FrontController } } - Tools::redirect('index.php?controller=order&'.(isset($this->id_product) ? 'ipa='.$this->id_product : '')); - } elseif ($this->errors && !$this->ajax) { - $this->context->cookie->__set('cart_errors', json_encode($this->errors)); Tools::redirect('index.php?controller=order&'.(isset($this->id_product) ? 'ipa='.$this->id_product : '')); } } elseif (!$this->isTokenValid()) { @@ -158,8 +155,8 @@ class CartControllerCore extends FrontController $result = $this->context->cart->deleteProduct($this->id_product, $this->id_product_attribute, $this->customization_id, $this->id_address_delivery); // now get updated available rooms - $date_from = Tools::getValue('dateFrom'); - $date_to = Tools::getValue('dateTo'); + $date_from = Tools::getValue('date_from'); + $date_to = Tools::getValue('date_to'); $date_from = date("Y-m-d H:i:s", strtotime($date_from)); $date_to = date("Y-m-d H:i:s", strtotime($date_to)); $objRoomType = new HotelRoomType(); @@ -318,7 +315,7 @@ class CartControllerCore extends FrontController } if ($occupancyRequiredForBooking && $operator == 'up') { - if ($occupancy = is_array(Tools::getValue('occupancy')) ? Tools::getValue('occupancy') : json_decode(Tools::getValue('occupancy'), true)) { + if ($occupancy = Tools::getValue('occupancy')) { $this->qty = count($occupancy); } else { $this->errors[] = Tools::displayError('Invalid occupnacy.'); @@ -340,27 +337,21 @@ class CartControllerCore extends FrontController if (!$this->errors) { $objRoomType = new HotelRoomType(); if ($roomTypeInfo = $objRoomType->getRoomTypeInfoByIdProduct($this->id_product)) { - $date_from = Tools::getValue('dateFrom'); - $date_to = Tools::getValue('dateTo'); + $date_from = Tools::getValue('date_from'); + $date_to = Tools::getValue('date_to'); $date_from = date("Y-m-d H:i:s", strtotime($date_from)); $date_to = date("Y-m-d H:i:s", strtotime($date_to)); - $serviceProducts = Tools::getValue('service_product'); - if (is_array($serviceProducts) && !empty($serviceProducts)) { - $formattedServiceProducts = []; - - foreach ($serviceProducts as $id_product => $data) { - $formattedServiceProducts[] = [ - 'id_product' => (int) $id_product, - 'quantity' => isset($data['quantity']) ? (int) $data['quantity'] : 1, - ]; - } - - $serviceProducts = $formattedServiceProducts; - } else { - $serviceProducts = json_decode(Tools::getValue('serviceProducts'), true); + $rawServiceProducts = Tools::getValue('service_product'); + $serviceProducts = []; + if (is_array($rawServiceProducts) && !empty($rawServiceProducts)) { + foreach ($rawServiceProducts as $id_product => $data) { + $serviceProducts[] = [ + 'id_product' => (int) $id_product, + 'quantity' => isset($data['quantity']) ? (int) $data['quantity'] : 1, + ]; + } } - $serviceProducts = is_array($serviceProducts) ? $serviceProducts : []; // valdiate occupancy if providede if ($operator == 'up' && $occupancyRequiredForBooking) { @@ -438,7 +429,7 @@ class CartControllerCore extends FrontController $total_available_rooms = $hotelRoomData['stats']['num_avail']; if (Tools::getValue('op', 'up') == 'up') { if ($total_available_rooms < $req_rm) { - if (Module::isInstalled('blockcart') && Module::isEnabled('blockcart') && Configuration::get('PS_BLOCK_CART_AJAX')) { + if (Tools::getValue('ajax')) { die(json_encode(array('status' => 'unavailable_quantity', 'avail_rooms' => $total_available_rooms))); } else { $this->errors[] = Tools::displayError('All rooms are sold out for the selected dates. Please try with different dates.'); @@ -473,14 +464,14 @@ class CartControllerCore extends FrontController } } } else { - if (Module::isInstalled('blockcart') && Module::isEnabled('blockcart') && Configuration::get('PS_BLOCK_CART_AJAX')) { + if (Tools::getValue('ajax')) { die(json_encode(array('status' => 'failed3'))); } else { $this->errors[] = Tools::displayError('An error occurred while processing your request.'); } } } else { - if (Module::isInstalled('blockcart') && Module::isEnabled('blockcart') && Configuration::get('PS_BLOCK_CART_AJAX')) { + if (Tools::getValue('ajax')) { die(json_encode(array('status' => 'failed4'))); } else { $this->errors[] = Tools::displayError('An error occurred while processing your request.'); @@ -638,7 +629,7 @@ class CartControllerCore extends FrontController */ if ($product->booking_product) { $objHotelCartBookingData = new HotelCartBookingData(); - $roomDemand = json_decode(Tools::getValue('roomDemands'), true); + $roomDemand = json_decode(Tools::getValue('room_demands'), true); $roomDemand = json_encode($roomDemand); $availQty = $total_available_rooms; $update_quantity = $objHotelCartBookingData->updateCartBooking( diff --git a/controllers/front/OrderOpcController.php b/controllers/front/OrderOpcController.php index cd3ed1624..d1814c445 100644 --- a/controllers/front/OrderOpcController.php +++ b/controllers/front/OrderOpcController.php @@ -44,16 +44,6 @@ class OrderOpcControllerCore extends ParentOrderController { parent::init(); - if ($this->context->cookie->__isset('cart_errors')) { - $cartErrors = json_decode($this->context->cookie->__get('cart_errors'), true); - if (is_array($cartErrors)) { - foreach ($cartErrors as $cartError) { - $this->errors[] = $cartError; - } - } - $this->context->cookie->__unset('cart_errors'); - } - if ($this->nbProducts) { $this->context->smarty->assign('virtual_cart', $this->context->cart->isVirtualCart()); } diff --git a/controllers/front/ProductController.php b/controllers/front/ProductController.php index a40ccab6f..1fe70ea20 100644 --- a/controllers/front/ProductController.php +++ b/controllers/front/ProductController.php @@ -1449,8 +1449,8 @@ class ProductControllerCore extends FrontController $response = array('status' => false); $idProduct = (int) Tools::getValue('id_product'); if ($this->product->booking_product) { - $dateFrom = Tools::getValue('dateFrom'); - $dateTo = Tools::getValue('dateTo'); + $dateFrom = Tools::getValue('date_from'); + $dateTo = Tools::getValue('date_to'); $occupancy = Tools::getValue('occupancy'); if (Configuration::get('PS_FRONT_ROOM_UNIT_SELECTION_TYPE') == HotelBookingDetail::PS_ROOM_UNIT_SELECTION_TYPE_OCCUPANCY) { if (!Validate::isOccupancy($occupancy)) { diff --git a/modules/blockcart/ajax-cart.js b/modules/blockcart/ajax-cart.js index 360d1b577..eaa8a6bd9 100644 --- a/modules/blockcart/ajax-cart.js +++ b/modules/blockcart/ajax-cart.js @@ -394,19 +394,29 @@ var ajaxCart = { if (parseInt(booking_product)) { // get the selected extra demands by customer if (typeof dateFrom != 'undefined') - req.append('dateFrom', dateFrom); + req.append('date_from', dateFrom); if (typeof dateTo != 'undefined') - req.append('dateTo', dateTo); + req.append('date_to', dateTo); - if (occupancy_required_for_booking) { - req.append('occupancy', JSON.stringify(occupancy)); + if (occupancy_required_for_booking) { + occupancy.forEach(function(room, i) { + req.append('occupancy['+i+'][adults]', room.adults); + req.append('occupancy['+i+'][children]', room.children || 0); + if (room.child_ages && room.child_ages.length) { + room.child_ages.forEach(function(age) { + req.append('occupancy['+i+'][child_ages][]', age); + }); + } + }); } else { req.append('qty', ((occupancy && occupancy != null) ? occupancy : '1')); } var roomDemands = getRoomsExtraDemands(); - req.append('roomDemands', JSON.stringify(roomDemands) ); - req.append('serviceProducts', JSON.stringify(getRoomsServiceProducts()) ); + req.append('room_demands', JSON.stringify(roomDemands) ); + getRoomsServiceProducts().forEach(function(product) { + req.append('service_product['+product.id_product+'][quantity]', product.quantity); + }); } else { req.append('qty', ((occupancy && occupancy != null) ? occupancy : '1')); } @@ -555,7 +565,7 @@ var ajaxCart = { async: true, cache: false, dataType: "json", - data: 'controller=cart&add=1&op=down&dateFrom=' + dateFrom + '&dateTo=' + dateTo + '&ajax=true&qty=' + ((quantity && quantity != null) ? quantity : '1') + '&id_product=' + idProduct + '&token=' + static_token + ((parseInt(idCombination) && idCombination != null) ? '&ipa=' + parseInt(idCombination) : '' + '&id_customization=' + ((typeof customizationId !== 'undefined') ? customizationId : 0)), + data: 'controller=cart&add=1&op=down&date_from=' + dateFrom + '&date_to=' + dateTo + '&ajax=true&qty=' + ((quantity && quantity != null) ? quantity : '1') + '&id_product=' + idProduct + '&token=' + static_token + ((parseInt(idCombination) && idCombination != null) ? '&ipa=' + parseInt(idCombination) : '' + '&id_customization=' + ((typeof customizationId !== 'undefined') ? customizationId : 0)), success: function(jsonData, textStatus, jqXHR) { /*by webkul checking and setting availability of rooms*/ /*for product page add to cart quantity management*/ @@ -629,7 +639,7 @@ var ajaxCart = { async: true, cache: false, dataType: "json", - data: 'controller=cart&delete=1&dateFrom=' + dateFrom + '&dateTo=' + dateTo + '&id_product=' + idProduct + '&id_hotel=' + idHotel + '&id_product_option=' + ((idCombination != null && parseInt(idCombination)) ? idCombination : '') + ((customizationId && customizationId != null) ? '&id_customization=' + customizationId : '') + '&id_address_delivery=' + idAddressDelivery + '&token=' + static_token + '&ajax=true', + data: 'controller=cart&delete=1&date_from=' + dateFrom + '&date_to=' + dateTo + '&id_product=' + idProduct + '&id_hotel=' + idHotel + '&id_product_option=' + ((idCombination != null && parseInt(idCombination)) ? idCombination : '') + ((customizationId && customizationId != null) ? '&id_customization=' + customizationId : '') + '&id_address_delivery=' + idAddressDelivery + '&token=' + static_token + '&ajax=true', success: function(jsonData) { if (pagename == 'product') { disableRoomTypeDemands(0); @@ -1387,66 +1397,3 @@ function crossselling_serialScroll() { pager: false }); } - -function resetRoomtypeServices(refresh = true) { - $('.room_demands_container').find('input.id_room_type_demand:checked').prop('checked', false).uniform(); - $('#additional_products').empty(); - $('#additional_products div') - $('.remove_roomtype_product').text(select_txt).removeClass('btn-danger remove_roomtype_product').addClass('btn-success add_roomtype_product'); - if (refresh) { - BookingForm.refresh(); - } -} - -function disableRoomTypeDemands(show) { - if (show) { - $('.room_demands_container_overlay').show(); - $('.room_demands_container').find('input:checkbox.id_room_type_demand').prop('checked', false); - $('.room_demand_block').find('.id_room_type_demand').prop('checked', false).parent().removeClass('checked'); - $('.room_demands_container').find('input:checkbox.id_room_type_demand').attr('disabled', 'disabled'); - } else { - $('.room_demands_container_overlay').hide(); - $('.room_demands_container').find('input:checkbox.id_room_type_demand').removeAttr('disabled'); - $('.room_demands_container').find('.checker').removeClass('disabled'); - } -} - -function disableRoomTypeServices(disable) { - if (disable) { - resetRoomtypeServices(false); - $('#service_products_cont').find('button.add_roomtype_product').attr('disabled', 'disabled'); - $('#service_products_cont').find('.qty_container .qty_direction a').attr('disabled', 'disabled'); - } else { - $('#service_products_cont').find('button.add_roomtype_product').removeAttr('disabled'); - $('#service_products_cont').find('.qty_container .qty_direction a').removeAttr('disabled'); - } -} - -function getRoomsExtraDemands() -{ - var roomDemands = []; - - $('input:checkbox.id_room_type_demand:checked').each(function () { - roomDemands.push({ - 'id_global_demand':$(this).val(), - 'id_option': $(this).closest('.room_demand_block').find('.id_option').val() - }); - }); - - return roomDemands; -} - -function getRoomsServiceProducts() -{ - var serviceProducts = []; - - $('#additional_products input.service_product').each(function () { - serviceProducts.push({ - 'id_product': $(this).data('id_product'), - 'quantity':$(this).val(), - }); - }); - - return serviceProducts; -} - diff --git a/themes/hotel-reservation-theme/_partials/booking-form.tpl b/themes/hotel-reservation-theme/_partials/booking-form.tpl index e18c6cc6c..c54f6e695 100644 --- a/themes/hotel-reservation-theme/_partials/booking-form.tpl +++ b/themes/hotel-reservation-theme/_partials/booking-form.tpl @@ -51,8 +51,8 @@
{l s='Check-in'}    {l s='Check-out'}
- - + +
{/block}
diff --git a/themes/hotel-reservation-theme/_partials/room_type_list.tpl b/themes/hotel-reservation-theme/_partials/room_type_list.tpl index a85302439..1633ebb26 100644 --- a/themes/hotel-reservation-theme/_partials/room_type_list.tpl +++ b/themes/hotel-reservation-theme/_partials/room_type_list.tpl @@ -118,7 +118,7 @@ {/if} {block name='room_type_list_room_book_now_button'}
- {l s='Book Now'} + {l s='Book Now'}
{/block}
diff --git a/themes/hotel-reservation-theme/global.tpl b/themes/hotel-reservation-theme/global.tpl index 7d8bfda70..f8f62346a 100644 --- a/themes/hotel-reservation-theme/global.tpl +++ b/themes/hotel-reservation-theme/global.tpl @@ -37,9 +37,6 @@ {addJsDef currencySign=$currency->sign|html_entity_decode:2:"UTF-8"} {addJsDef currencyFormat=$currency->format|intval} {addJsDef currencyBlank=$currency->blank|intval} -{addJsDef currency_sign=$currency->sign} -{addJsDef currency_format=$currency->format} -{addJsDef currency_blank=$currency->blank} {addJsDef isLogged=$is_logged|intval} {addJsDef isGuest=$is_guest|intval} {addJsDef page_name=$page_name|escape:'html':'UTF-8'} @@ -85,10 +82,4 @@ {addJsDefL name='years_txt'}{l s='years' js=1}{/addJsDefL} {addJsDefL name='all_children_txt'}{l s='All Children' js=1}{/addJsDefL} {addJsDefL name='invalid_occupancy_txt'}{l s='Invalid occupancy(adults/children) found.' js=1}{/addJsDefL} -{addJsDefL name=customizationIdMessage}{l s='Customization #' mod='blockcart' js=1}{/addJsDefL} -{addJsDefL name=removingLinkText}{l s='remove this product from my cart' mod='blockcart' js=1}{/addJsDefL} -{addJsDefL name=freeShippingTranslation}{l s='Free shipping!' mod='blockcart' js=1}{/addJsDefL} -{addJsDefL name=freeProductTranslation}{l s='Free!' mod='blockcart' js=1}{/addJsDefL} -{addJsDefL name=delete_txt}{l s='Delete' mod='blockcart' js=1}{/addJsDefL} -{addJsDefL name=toBeDetermined}{l s='To be determined' mod='blockcart' js=1}{/addJsDefL} {/strip} diff --git a/themes/hotel-reservation-theme/js/cart-summary.js b/themes/hotel-reservation-theme/js/cart-summary.js index 7e3c538ca..cd3461070 100644 --- a/themes/hotel-reservation-theme/js/cart-summary.js +++ b/themes/hotel-reservation-theme/js/cart-summary.js @@ -601,8 +601,8 @@ function deleteRoomFromSummary(el) + '&op=down' + '&qty='+qty + '&id_product='+id_product - + '&dateFrom='+date_from - + '&dateTo='+date_to + + '&date_from='+date_from + + '&date_to='+date_to + '&token=' + static_token + '&allow_refresh=1', success: function(jsonData) diff --git a/themes/hotel-reservation-theme/js/category.js b/themes/hotel-reservation-theme/js/category.js index c872efdfb..50fcbf200 100644 --- a/themes/hotel-reservation-theme/js/category.js +++ b/themes/hotel-reservation-theme/js/category.js @@ -66,4 +66,89 @@ function resizeCatimg() $( "#check_out_time" ).datepicker( "option", "maxDate", max_order_date_format); } });*/ -/*End*/ \ No newline at end of file +/*End*/ + +$(document).off('click', '.ajax_add_to_cart_button').on('click', '.ajax_add_to_cart_button', function (e) { + e.preventDefault(); + var baseUrl = $(this).attr('href'); + var occupancy = getBookingOccupancyDetails($(this).closest('.booking_room_fields'), true); + if ($(this).prop('disabled') != 'disabled' && occupancy) { + var occupancyParams = ''; + if (Array.isArray(occupancy)) { + $.each(occupancy, function(i, room) { + occupancyParams += '&occupancy[' + i + '][adults]=' + encodeURIComponent(room.adults); + occupancyParams += '&occupancy[' + i + '][children]=' + encodeURIComponent(room.children); + if (room.child_ages && room.child_ages.length) { + $.each(room.child_ages, function(_, age) { + occupancyParams += '&occupancy[' + i + '][child_ages][]=' + encodeURIComponent(age); + }); + } + }); + } else { + occupancyParams += '&qty=' + encodeURIComponent(occupancy); + } + window.location.href = baseUrl + occupancyParams; + } +}); + +function resetRoomtypeServices(refresh = true) { + $('.room_demands_container').find('input.id_room_type_demand:checked').prop('checked', false).uniform(); + $('#additional_products').empty(); + $('#additional_products div') + $('.remove_roomtype_product').text(select_txt).removeClass('btn-danger remove_roomtype_product').addClass('btn-success add_roomtype_product'); + if (refresh) { + BookingForm.refresh(); + } +} + +function disableRoomTypeDemands(show) { + if (show) { + $('.room_demands_container_overlay').show(); + $('.room_demands_container').find('input:checkbox.id_room_type_demand').prop('checked', false); + $('.room_demand_block').find('.id_room_type_demand').prop('checked', false).parent().removeClass('checked'); + $('.room_demands_container').find('input:checkbox.id_room_type_demand').attr('disabled', 'disabled'); + } else { + $('.room_demands_container_overlay').hide(); + $('.room_demands_container').find('input:checkbox.id_room_type_demand').removeAttr('disabled'); + $('.room_demands_container').find('.checker').removeClass('disabled'); + } +} + +function disableRoomTypeServices(disable) { + if (disable) { + resetRoomtypeServices(false); + $('#service_products_cont').find('button.add_roomtype_product').attr('disabled', 'disabled'); + $('#service_products_cont').find('.qty_container .qty_direction a').attr('disabled', 'disabled'); + } else { + $('#service_products_cont').find('button.add_roomtype_product').removeAttr('disabled'); + $('#service_products_cont').find('.qty_container .qty_direction a').removeAttr('disabled'); + } +} + +function getRoomsExtraDemands() +{ + var roomDemands = []; + + $('input:checkbox.id_room_type_demand:checked').each(function () { + roomDemands.push({ + 'id_global_demand':$(this).val(), + 'id_option': $(this).closest('.room_demand_block').find('.id_option').val() + }); + }); + + return roomDemands; +} + +function getRoomsServiceProducts() +{ + var serviceProducts = []; + + $('#additional_products input.service_product').each(function () { + serviceProducts.push({ + 'id_product': $(this).data('id_product'), + 'quantity':$(this).val(), + }); + }); + + return serviceProducts; +} \ No newline at end of file diff --git a/themes/hotel-reservation-theme/js/modules/blockcart/ajax-cart.js b/themes/hotel-reservation-theme/js/modules/blockcart/ajax-cart.js index c200f5a15..49b65b0db 100644 --- a/themes/hotel-reservation-theme/js/modules/blockcart/ajax-cart.js +++ b/themes/hotel-reservation-theme/js/modules/blockcart/ajax-cart.js @@ -394,19 +394,29 @@ var ajaxCart = { if (parseInt(booking_product)) { // get the selected extra demands by customer if (typeof dateFrom != 'undefined') - req.append('dateFrom', dateFrom); + req.append('date_from', dateFrom); if (typeof dateTo != 'undefined') - req.append('dateTo', dateTo); + req.append('date_to', dateTo); if (occupancy_required_for_booking) { - req.append('occupancy', JSON.stringify(occupancy)); + occupancy.forEach(function(room, i) { + req.append('occupancy['+i+'][adults]', room.adults); + req.append('occupancy['+i+'][children]', room.children || 0); + if (room.child_ages && room.child_ages.length) { + room.child_ages.forEach(function(age) { + req.append('occupancy['+i+'][child_ages][]', age); + }); + } + }); } else { req.append('qty', ((occupancy && occupancy != null) ? occupancy : '1')); } var roomDemands = getRoomsExtraDemands(); - req.append('roomDemands', JSON.stringify(roomDemands) ); - req.append('serviceProducts', JSON.stringify(getRoomsServiceProducts()) ); + req.append('room_demands', JSON.stringify(roomDemands) ); + getRoomsServiceProducts().forEach(function(product) { + req.append('service_product['+product.id_product+'][quantity]', product.quantity); + }); } else { req.append('qty', ((occupancy && occupancy != null) ? occupancy : '1')); } @@ -555,7 +565,7 @@ var ajaxCart = { async: true, cache: false, dataType: "json", - data: 'controller=cart&add=1&op=down&dateFrom=' + dateFrom + '&dateTo=' + dateTo + '&ajax=true&qty=' + ((quantity && quantity != null) ? quantity : '1') + '&id_product=' + idProduct + '&token=' + static_token + ((parseInt(idCombination) && idCombination != null) ? '&ipa=' + parseInt(idCombination) : '' + '&id_customization=' + ((typeof customizationId !== 'undefined') ? customizationId : 0)), + data: 'controller=cart&add=1&op=down&date_from=' + dateFrom + '&date_to=' + dateTo + '&ajax=true&qty=' + ((quantity && quantity != null) ? quantity : '1') + '&id_product=' + idProduct + '&token=' + static_token + ((parseInt(idCombination) && idCombination != null) ? '&ipa=' + parseInt(idCombination) : '' + '&id_customization=' + ((typeof customizationId !== 'undefined') ? customizationId : 0)), success: function(jsonData, textStatus, jqXHR) { /*by webkul checking and setting availability of rooms*/ /*for product page add to cart quantity management*/ @@ -629,7 +639,7 @@ var ajaxCart = { async: true, cache: false, dataType: "json", - data: 'controller=cart&delete=1&dateFrom=' + dateFrom + '&dateTo=' + dateTo + '&id_product=' + idProduct + '&id_hotel=' + idHotel + '&id_product_option=' + ((idCombination != null && parseInt(idCombination)) ? idCombination : '') + ((customizationId && customizationId != null) ? '&id_customization=' + customizationId : '') + '&id_address_delivery=' + idAddressDelivery + '&token=' + static_token + '&ajax=true', + data: 'controller=cart&delete=1&date_from=' + dateFrom + '&date_to=' + dateTo + '&id_product=' + idProduct + '&id_hotel=' + idHotel + '&id_product_option=' + ((idCombination != null && parseInt(idCombination)) ? idCombination : '') + ((customizationId && customizationId != null) ? '&id_customization=' + customizationId : '') + '&id_address_delivery=' + idAddressDelivery + '&token=' + static_token + '&ajax=true', success: function(jsonData) { if (pagename == 'product') { disableRoomTypeDemands(0); @@ -1387,65 +1397,3 @@ function crossselling_serialScroll() { pager: false }); } - -function resetRoomtypeServices(refresh = true) { - $('.room_demands_container').find('input.id_room_type_demand:checked').prop('checked', false).uniform(); - $('#additional_products').empty(); - $('#additional_products div') - $('.remove_roomtype_product').text(select_txt).removeClass('btn-danger remove_roomtype_product').addClass('btn-success add_roomtype_product'); - if (refresh) { - BookingForm.refresh(); - } -} - -function disableRoomTypeDemands(show) { - if (show) { - $('.room_demands_container_overlay').show(); - $('.room_demands_container').find('input:checkbox.id_room_type_demand').prop('checked', false); - $('.room_demand_block').find('.id_room_type_demand').prop('checked', false).parent().removeClass('checked'); - $('.room_demands_container').find('input:checkbox.id_room_type_demand').attr('disabled', 'disabled'); - } else { - $('.room_demands_container_overlay').hide(); - $('.room_demands_container').find('input:checkbox.id_room_type_demand').removeAttr('disabled'); - $('.room_demands_container').find('.checker').removeClass('disabled'); - } -} - -function disableRoomTypeServices(disable) { - if (disable) { - resetRoomtypeServices(false); - $('#service_products_cont').find('button.add_roomtype_product').attr('disabled', 'disabled'); - $('#service_products_cont').find('.qty_container .qty_direction a').attr('disabled', 'disabled'); - } else { - $('#service_products_cont').find('button.add_roomtype_product').removeAttr('disabled'); - $('#service_products_cont').find('.qty_container .qty_direction a').removeAttr('disabled'); - } -} - -function getRoomsExtraDemands() -{ - var roomDemands = []; - - $('input:checkbox.id_room_type_demand:checked').each(function () { - roomDemands.push({ - 'id_global_demand':$(this).val(), - 'id_option': $(this).closest('.room_demand_block').find('.id_option').val() - }); - }); - - return roomDemands; -} - -function getRoomsServiceProducts() -{ - var serviceProducts = []; - - $('#additional_products input.service_product').each(function () { - serviceProducts.push({ - 'id_product': $(this).data('id_product'), - 'quantity':$(this).val(), - }); - }); - - return serviceProducts; -} diff --git a/themes/hotel-reservation-theme/js/occupancy.js b/themes/hotel-reservation-theme/js/occupancy.js index 1f463b85c..b435429b6 100644 --- a/themes/hotel-reservation-theme/js/occupancy.js +++ b/themes/hotel-reservation-theme/js/occupancy.js @@ -20,29 +20,6 @@ * @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0 */ $(document).ready(function(){ - $(document).off('click', '.ajax_add_to_cart_button').on('click', '.ajax_add_to_cart_button', function (e) { - e.preventDefault(); - var baseUrl = $(this).attr('href'); - var occupancy = getBookingOccupancyDetails($(this).closest('.booking_room_fields'), true); - if ($(this).prop('disabled') != 'disabled' && occupancy) { - var occupancyParams = ''; - if (Array.isArray(occupancy)) { - $.each(occupancy, function(i, room) { - occupancyParams += '&occupancy[' + i + '][adults]=' + encodeURIComponent(room.adults); - occupancyParams += '&occupancy[' + i + '][children]=' + encodeURIComponent(room.children); - if (room.child_ages && room.child_ages.length) { - $.each(room.child_ages, function(_, age) { - occupancyParams += '&occupancy[' + i + '][child_ages][]=' + encodeURIComponent(age); - }); - } - }); - } else { - occupancyParams += '&qty=' + encodeURIComponent(occupancy); - } - window.location.href = baseUrl + occupancyParams; - } - }); - $(document).on('click', '.booking_occupancy_wrapper .remove-room-link', function(e) { e.preventDefault(); diff --git a/themes/hotel-reservation-theme/order-opc.tpl b/themes/hotel-reservation-theme/order-opc.tpl index 81115a208..f9bcef63f 100644 --- a/themes/hotel-reservation-theme/order-opc.tpl +++ b/themes/hotel-reservation-theme/order-opc.tpl @@ -395,6 +395,8 @@ {addJsDefL name=txtExtraDemandSucc}{l s='Updated Successfully' js=1}{/addJsDefL} {addJsDefL name=txtMaxQuantityAdded}{l s='Maximum quantity of service added' js=1}{/addJsDefL} {addJsDefL name=txtExtraDemandErr}{l s='Some error occurred while updating demands' js=1}{/addJsDefL} + {addJsDefL name=freeShippingTranslation}{l s='Free shipping!' mod='blockcart' js=1}{/addJsDefL} + {/strip} {/block} {/if} From ee4fb7a07a61a023633cf6f88fceb66461dd92fc Mon Sep 17 00:00:00 2001 From: shazar rahman Date: Thu, 2 Jul 2026 13:05:40 +0530 Subject: [PATCH 04/78] Added: Tourism tax system with per-hotel configurable, rate tiers, child pricing, and exemption Co-authored-by: Name --- .../controllers/normal_products/prices.tpl | 17 + .../controllers/orders/_product_line.tpl | 31 + .../orders/_rooms_informaion_table.tpl | 81 +- .../controllers/orders/helpers/view/view.tpl | 12 + .../template/controllers/products/prices.tpl | 17 + classes/Cart.php | 22 +- classes/PaymentModule.php | 37 + classes/Product.php | 8 +- classes/order/OrderDetail.php | 6 +- classes/order/OrderInvoice.php | 10 + classes/order/OrderReturn.php | 10 + classes/pdf/HTMLTemplateInvoice.php | 17 + classes/tax/Tax.php | 12 +- classes/tax/TaxRulesGroup.php | 37 +- .../admin/AdminNormalProductsController.php | 5 + controllers/admin/AdminOrdersController.php | 125 ++- controllers/admin/AdminProductsController.php | 5 + .../admin/AdminTaxRulesGroupController.php | 167 +++- controllers/admin/AdminTaxesController.php | 717 +++++++++++++++++- .../front/OrderConfirmationController.php | 13 + controllers/front/OrderDetailController.php | 1 + install/data/db_structure.sql | 5 + .../classes/HotelBookingDetail.php | 35 + .../classes/HotelBookingTaxExemption.php | 68 ++ .../classes/HotelBranchInformation.php | 2 + .../classes/HotelHelper.php | 2 + .../classes/HotelOrderTourismTax.php | 565 ++++++++++++++ .../classes/HotelReservationSystemDb.php | 83 +- .../classes/HotelRoomTypeFeaturePricing.php | 47 ++ .../classes/HotelTourismTax.php | 192 +++++ .../classes/HotelTourismTaxCalculator.php | 513 +++++++++++++ .../classes/HotelTourismTaxChildRange.php | 168 ++++ .../classes/HotelTourismTaxTier.php | 145 ++++ .../admin/AdminAddHotelController.php | 3 + modules/hotelreservationsystem/define.php | 7 + .../hotelreservationsystem.php | 53 +- .../views/css/TourismTaxAdmin.css | 73 ++ .../views/js/TourismTaxForm.js | 185 +++++ .../admin/add_hotel/helpers/form/form.tpl | 27 + .../helpers/view/view.tpl | 18 +- pdf/invoice.tax-tab.tpl | 24 + pdf/invoice.total-tab.tpl | 20 + .../cart-total-block.tpl | 14 + .../order-confirmation.tpl | 20 + .../hotel-reservation-theme/order-detail.tpl | 16 + 45 files changed, 3575 insertions(+), 60 deletions(-) create mode 100644 modules/hotelreservationsystem/classes/HotelBookingTaxExemption.php create mode 100644 modules/hotelreservationsystem/classes/HotelOrderTourismTax.php create mode 100644 modules/hotelreservationsystem/classes/HotelTourismTax.php create mode 100644 modules/hotelreservationsystem/classes/HotelTourismTaxCalculator.php create mode 100644 modules/hotelreservationsystem/classes/HotelTourismTaxChildRange.php create mode 100644 modules/hotelreservationsystem/classes/HotelTourismTaxTier.php create mode 100644 modules/hotelreservationsystem/views/css/TourismTaxAdmin.css create mode 100644 modules/hotelreservationsystem/views/js/TourismTaxForm.js diff --git a/admin/themes/default/template/controllers/normal_products/prices.tpl b/admin/themes/default/template/controllers/normal_products/prices.tpl index bae5d1211..274fe5b0f 100644 --- a/admin/themes/default/template/controllers/normal_products/prices.tpl +++ b/admin/themes/default/template/controllers/normal_products/prices.tpl @@ -197,6 +197,23 @@ $(document).ready(function () { + {if isset($tourismTaxRulesGroups)} +
+
+ +
+ +
+
+ {/if} + {*
auto_add_to_cart && $product->price_addition_type == Product::PRICE_ADDITION_TYPE_WITH_ROOM}style="display:none"{/if}>
diff --git a/admin/themes/default/template/controllers/orders/_product_line.tpl b/admin/themes/default/template/controllers/orders/_product_line.tpl index 1e44a648f..013adfbd2 100644 --- a/admin/themes/default/template/controllers/orders/_product_line.tpl +++ b/admin/themes/default/template/controllers/orders/_product_line.tpl @@ -81,6 +81,20 @@ {convertPriceWithCurrency price=($data['total_room_tax']) currency=$currency->id} + {if isset($use_tourism_tax) && $use_tourism_tax + && isset($data.tourism_tax_status) + && $data.tourism_tax_status eq $tourism_tax_status_applied + && isset($data.tourism_tax_total) && $data.tourism_tax_total > 0} + +   + + + {/if} {convertPriceWithCurrency price=($data['total_room_price_ti']) currency=$currency->id} @@ -136,6 +150,23 @@ {l s='Delete'} + {if isset($use_tourism_tax) && $use_tourism_tax && isset($data.tourism_tax_status)} + {if $data.tourism_tax_status eq $tourism_tax_status_applied} +
  • + + + {l s='Exempt Tourism Tax'} + +
  • + {else} +
  • + + + {l s='Apply Tourism Tax'} + +
  • + {/if} + {/if} {/if}
    diff --git a/admin/themes/default/template/controllers/orders/_rooms_informaion_table.tpl b/admin/themes/default/template/controllers/orders/_rooms_informaion_table.tpl index 743254785..15f21bf46 100644 --- a/admin/themes/default/template/controllers/orders/_rooms_informaion_table.tpl +++ b/admin/themes/default/template/controllers/orders/_rooms_informaion_table.tpl @@ -40,7 +40,7 @@ {l s='Refund/Cancel Status'} {l s='Refunded amount'} {/if} - {if ($can_edit && !$order->hasBeenDelivered())} + {if (isset($can_edit) && $can_edit && !$order->hasBeenDelivered())} {l s='Actions'} {/if} @@ -62,4 +62,83 @@ {l s='Room information not available.'}
    +{/if} + +{if isset($use_tourism_tax) && $use_tourism_tax} + + {/if} \ No newline at end of file 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 3ed6b7448..4a4208baf 100644 --- a/admin/themes/default/template/controllers/orders/helpers/view/view.tpl +++ b/admin/themes/default/template/controllers/orders/helpers/view/view.tpl @@ -852,6 +852,18 @@ {l s='Add Rooms'} {/if} + {if isset($use_tourism_tax) && $use_tourism_tax && $order_detail_data} + {if isset($order_tourism_tax_has_applied) && $order_tourism_tax_has_applied} + + {/if} + {if isset($order_tourism_tax_has_unapplied) && $order_tourism_tax_has_unapplied} + + {/if} + {/if} {* by webkul this code is added for showing rooms information on the order detail page *} {include file='controllers/orders/_rooms_informaion_table.tpl'} diff --git a/admin/themes/default/template/controllers/products/prices.tpl b/admin/themes/default/template/controllers/products/prices.tpl index c27d0906c..e01d8cab8 100644 --- a/admin/themes/default/template/controllers/products/prices.tpl +++ b/admin/themes/default/template/controllers/products/prices.tpl @@ -193,6 +193,22 @@ $(document).ready(function () { + {if isset($tourismTaxRulesGroups)} +
    +
    + +
    + +
    +
    + {/if}
    @@ -465,6 +481,7 @@ $(document).ready(function () {
    + {if isset($specificPriceModificationForm)}

    {l s='Specific prices'}

    diff --git a/classes/Cart.php b/classes/Cart.php index d7421ec7d..0eca871d0 100644 --- a/classes/Cart.php +++ b/classes/Cart.php @@ -1638,6 +1638,7 @@ class CartCore extends ObjectModel } $products_total = array(); $ecotax_total = 0; + $tourism_tax_total = 0.0; $totalDemandsPrice = 0; $objCartBookingData = new HotelCartBookingData(); $objServiceProductCartDetail = new ServiceProductCartDetail(); @@ -1911,6 +1912,10 @@ class CartCore extends ObjectModel } else { $products_total[$id_tax_rules_group] += Tools::processPriceRounding($totalPriceByProduct); } + + if ($with_taxes && in_array($type, array(Cart::BOTH, Cart::BOTH_WITHOUT_SHIPPING))) { + $tourism_tax_total += $roomTotalPrice['tourism_tax_online']; + } } } } @@ -2052,6 +2057,10 @@ class CartCore extends ObjectModel $order_total += $shipping_fees + $wrapping_fees; } + if ($tourism_tax_total > 0) { + $order_total += $tourism_tax_total; + } + if ($order_total < 0 && $type != Cart::ONLY_DISCOUNTS) { return 0; } @@ -3942,6 +3951,15 @@ class CartCore extends ObjectModel if ($total_tax_without_discount < 0) { $total_tax_without_discount = 0; } + $tourism_tax_online = $total_tax - $total_tax_without_discount; + if ($tourism_tax_online > 0) { + if (Configuration::get('QLO_TOURISM_TAX_GROSSED_UP')) { + $total_rooms_wt += $tourism_tax_online; + $total_rooms_with_services_without_discount_ti += $tourism_tax_online; + } else { + $total_tax_without_discount += $tourism_tax_online; + } + } $summary = array( 'delivery' => $delivery, @@ -3988,7 +4006,9 @@ class CartCore extends ObjectModel 'total_rooms_with_services_without_discount_ti' => $total_rooms_with_services_without_discount_ti, 'cart_total_without_discount_te' => $cart_total_without_discount_te, 'cart_total_without_discount_ti' => $cart_total_without_discount_ti, - 'total_tax_without_discount' => $total_tax_without_discount + 'total_tax_without_discount' => $total_tax_without_discount, + 'tourism_tax_online' => max(0.0, (float) $tourism_tax_online), + 'tourism_tax_grossed_up' => (bool) Configuration::get('QLO_TOURISM_TAX_GROSSED_UP'), ); $hook = Hook::exec('actionCartSummary', $summary, null, true); if (is_array($hook)) { diff --git a/classes/PaymentModule.php b/classes/PaymentModule.php index fe1ce4392..c76c981f5 100644 --- a/classes/PaymentModule.php +++ b/classes/PaymentModule.php @@ -902,6 +902,43 @@ abstract class PaymentModuleCore extends Module } } if ($objBookingDetail->save()) { + if (Configuration::get('QLO_USE_TOURISM_TAX')) { + $idTourismTrg = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + 'SELECT `id_tourism_tax_rules_group` + FROM `' . _DB_PREFIX_ . 'product_shop` product_shop + WHERE `id_product` = ' . (int) $idProduct + . Shop::addSqlRestriction(false, 'product_shop') + ); + if ($idTourismTrg) { + $htlTaxAddressId = Cart::getIdAddressForTaxCalculation($idProduct); + $htlTaxAddress = new Address($htlTaxAddressId ?: 0); + $checkInDt = new DateTime($objBookingDetail->date_from); + $checkOutDt = new DateTime($objBookingDetail->date_to); + $ttNumNights = max(1, (int) $checkInDt->diff($checkOutDt)->days); + $ttNumAdults = (int) $objCartBookingData->adults; + $ttChildAges = !empty($objCartBookingData->child_ages) ? (array) json_decode($objCartBookingData->child_ages, true) : array(); + $ttUnitTe = (float) $total_price['total_price_tax_excl'] / $ttNumNights; + $ttCollType = Validate::isLoadedObject($objHotelBranch) ? (int) $objHotelBranch->tourism_tax_collection_type : 0; + + HotelTourismTaxCalculator::computeAndSave( + $order->id, + (int) $id_order_detail, + $objBookingDetail->id, + (int) $objCartBookingData->id_hotel, + $idTourismTrg, + $htlTaxAddress, + $ttUnitTe, + $objBookingDetail->date_from, + $ttNumNights, + $ttNumAdults, + $ttChildAges, + (int) $this->context->cart->id_currency, + $ttCollType, + (int) $idLang + ); + } + } + // save extra demands info if ($objCartBookingData->extra_demands && ($extraDemands = json_decode($objCartBookingData->extra_demands, true)) diff --git a/classes/Product.php b/classes/Product.php index fc962d3c0..4f96668ac 100644 --- a/classes/Product.php +++ b/classes/Product.php @@ -226,6 +226,9 @@ class ProductCore extends ObjectModel public $id_tax_rules_group = 1; + /** @var int Tourism tax rules group id (0 = no tourism tax) */ + public $id_tourism_tax_rules_group = 0; + /** * We keep this variable for retrocompatibility for themes * @deprecated 1.5.0 @@ -313,8 +316,9 @@ class ProductCore extends ObjectModel /* Shop fields */ 'id_category_default' => array('type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedId'), - 'id_tax_rules_group' => array('type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedId'), - 'on_sale' => array('type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'), + 'id_tax_rules_group' => array('type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedId'), + 'id_tourism_tax_rules_group' => array('type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedId'), + 'on_sale' => array('type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'), 'online_only' => array('type' => self::TYPE_BOOL, 'shop' => true, 'validate' => 'isBool'), 'ecotax' => array('type' => self::TYPE_FLOAT, 'shop' => true, 'validate' => 'isPrice'), 'minimal_quantity' => array('type' => self::TYPE_INT, 'shop' => true, 'validate' => 'isUnsignedInt'), diff --git a/classes/order/OrderDetail.php b/classes/order/OrderDetail.php index cce75207a..4ea14e253 100644 --- a/classes/order/OrderDetail.php +++ b/classes/order/OrderDetail.php @@ -131,6 +131,9 @@ class OrderDetailCore extends ObjectModel /** @var float */ public $ecotax_tax_rate; + /** @var float Tourism tax charged online for this order detail line */ + public $tourism_tax_amount = 0; + /** @var int */ public $discount_quantity_applied; @@ -218,7 +221,8 @@ class OrderDetailCore extends ObjectModel 'tax_computation_method' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId'), 'id_tax_rules_group' => array('type' => self::TYPE_INT, 'validate' => 'isInt'), 'ecotax' => array('type' => self::TYPE_FLOAT, 'validate' => 'isFloat'), - 'ecotax_tax_rate' => array('type' => self::TYPE_FLOAT, 'validate' => 'isFloat'), + 'ecotax_tax_rate' => array('type' => self::TYPE_FLOAT, 'validate' => 'isFloat'), + 'tourism_tax_amount' => array('type' => self::TYPE_FLOAT, 'validate' => 'isFloat'), 'discount_quantity_applied' => array('type' => self::TYPE_INT, 'validate' => 'isInt'), 'download_hash' => array('type' => self::TYPE_STRING, 'validate' => 'isGenericName'), 'download_nb' => array('type' => self::TYPE_INT, 'validate' => 'isInt'), diff --git a/classes/order/OrderInvoice.php b/classes/order/OrderInvoice.php index 9d097b5d4..616e01bde 100644 --- a/classes/order/OrderInvoice.php +++ b/classes/order/OrderInvoice.php @@ -866,6 +866,16 @@ class OrderInvoiceCore extends ObjectModel * @since 1.5 * @return array */ + /** + * Return tourism tax breakdown grouped by id_tax for invoice display. + * + * @return array [['tax_name' => string, 'total_amount' => float], ...] + */ + public function getTourismTaxBreakdown() + { + return HotelOrderTourismTax::getBreakdownForInvoice((int) $this->id_order); + } + public function getEcoTaxTaxesBreakdown() { $result = Db::getInstance()->executeS(' diff --git a/classes/order/OrderReturn.php b/classes/order/OrderReturn.php index 3da74d335..cbcca6cb9 100644 --- a/classes/order/OrderReturn.php +++ b/classes/order/OrderReturn.php @@ -211,6 +211,8 @@ class OrderReturnCore extends ObjectModel $calcServicePriceFirst = true; } + $onlineTourismTaxByBooking = Configuration::get('QLO_USE_TOURISM_TAX') ? HotelOrderTourismTax::getOnlineTotalsByBooking($idOrder) : array(); + foreach ($returnDetails as $key => &$bookingRow) { if ($skipReqCompletedNonRefunded) { $objReturnState = new OrderReturnState($bookingRow['id_return_state']); @@ -224,6 +226,8 @@ class OrderReturnCore extends ObjectModel $bookingRow['extra_service_total_paid_amount'] = 0; $bookingRow['extra_service_total_price_tax_incl'] = 0; $bookingRow['room_paid_amount'] = 0; + $bookingRow['tourism_tax_online_amount'] = 0; + $bookingRow['tourism_tax_paid_amount'] = 0; $roomSelectedDemands = $objBookingDemands->getRoomTypeBookingExtraDemands( $idOrder, @@ -291,6 +295,12 @@ class OrderReturnCore extends ObjectModel } } } + $tourismTaxOnline = isset($onlineTourismTaxByBooking[(int)$bookingRow['id_htl_booking']]) ? (float) $onlineTourismTaxByBooking[(int)$bookingRow['id_htl_booking']] : 0.0; + $bookingRow['tourism_tax_online_amount'] = $tourismTaxOnline; + if ($tourismTaxOnline > 0 && $objOrder->total_paid_real > 0) { + $denominator = $objOrder->total_paid_tax_incl + $objOrder->total_discounts_tax_incl; + $bookingRow['tourism_tax_paid_amount'] = $denominator > 0 ? ($objOrder->total_paid_real * $tourismTaxOnline / $denominator) : 0.0; + } if ($customerView) { $dateJoin = $bookingRow['id_product'].'_'.strtotime($bookingRow['date_from']).strtotime($bookingRow['date_to']); if (isset($returnsCustView[$dateJoin]['num_rooms'])) { diff --git a/classes/pdf/HTMLTemplateInvoice.php b/classes/pdf/HTMLTemplateInvoice.php index e5b36b518..1a721b3b4 100644 --- a/classes/pdf/HTMLTemplateInvoice.php +++ b/classes/pdf/HTMLTemplateInvoice.php @@ -692,6 +692,22 @@ class HTMLTemplateInvoiceCore extends HTMLTemplate $footer['total_tax_without_discount'] = 0; } + $ttBreakdown = Configuration::get('QLO_USE_TOURISM_TAX') ? HotelOrderTourismTax::getBreakdownForInvoice((int) $this->order->id) : array(); + $footer['tourism_tax_breakdown'] = $ttBreakdown; + $footer['tourism_tax_online'] = 0.0; + $footer['tourism_tax_at_hotel'] = 0.0; + foreach ($ttBreakdown as $ttRow) { + if ((int) $ttRow['collection_type'] === 0) { + $footer['tourism_tax_online'] += (float) $ttRow['total_amount']; + } else { + $footer['tourism_tax_at_hotel'] += (float) $ttRow['total_amount']; + } + } + + if ($footer['tourism_tax_online'] > 0) { + $footer['total_tax_without_discount'] = max(0.0, $footer['total_tax_without_discount'] - $footer['tourism_tax_online']); + } + $data = array( 'cart_htl_data' => $cart_htl_data, 'is_hotel_order' => HotelBookingDetail::getIdHotelByIdOrder($this->order->id), @@ -812,6 +828,7 @@ class HTMLTemplateInvoiceCore extends HTMLTemplate 'shipping_tax' => $this->order_invoice->getShippingTaxesBreakdown($this->order), 'ecotax_tax' => $this->order_invoice->getEcoTaxTaxesBreakdown(), 'wrapping_tax' => $this->order_invoice->getWrappingTaxesBreakdown(), + 'tourism_tax' => $this->order_invoice->getTourismTaxBreakdown(), ); foreach ($breakdowns as $type => $bd) { diff --git a/classes/tax/Tax.php b/classes/tax/Tax.php index a7f8ee1c3..13838239b 100644 --- a/classes/tax/Tax.php +++ b/classes/tax/Tax.php @@ -39,6 +39,9 @@ class TaxCore extends ObjectModel /** @var bool true if the tax has been historized */ public $deleted = 0; + /** @var int 1 = this ps_tax row is a tourism tax (subtype params in htl_tourism_tax) */ + public $is_tourism_tax = 0; + /** * @see ObjectModel::$definition */ @@ -47,11 +50,12 @@ class TaxCore extends ObjectModel 'primary' => 'id_tax', 'multilang' => true, 'fields' => array( - 'rate' => array('type' => self::TYPE_FLOAT, 'validate' => 'isFloat', 'required' => true), - 'active' => array('type' => self::TYPE_BOOL), - 'deleted' => array('type' => self::TYPE_BOOL), + 'rate' => array('type' => self::TYPE_FLOAT, 'validate' => 'isFloat', 'required' => true), + 'active' => array('type' => self::TYPE_BOOL), + 'deleted' => array('type' => self::TYPE_BOOL), + 'is_tourism_tax' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), /* Lang fields */ - 'name' => array('type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'required' => true, 'size' => 32), + 'name' => array('type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'required' => true, 'size' => 32), ), ); diff --git a/classes/tax/TaxRulesGroup.php b/classes/tax/TaxRulesGroup.php index c37d6367a..140130443 100644 --- a/classes/tax/TaxRulesGroup.php +++ b/classes/tax/TaxRulesGroup.php @@ -40,6 +40,9 @@ class TaxRulesGroupCore extends ObjectModel /** @var string Object last modification date */ public $date_upd; + /** @var int 1 = all rules in this TRG reference tourism taxes only */ + public $is_tourism_tax_rule = 0; + /** * @see ObjectModel::$definition */ @@ -47,11 +50,12 @@ class TaxRulesGroupCore extends ObjectModel 'table' => 'tax_rules_group', 'primary' => 'id_tax_rules_group', 'fields' => array( - 'name' => array('type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => true, 'size' => 64), - 'active' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), - 'deleted' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), - 'date_add' => array('type' => self::TYPE_DATE, 'validate' => 'isDate'), - 'date_upd' => array('type' => self::TYPE_DATE, 'validate' => 'isDate'), + 'name' => array('type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => true, 'size' => 64), + 'active' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), + 'deleted' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), + 'is_tourism_tax_rule' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), + 'date_add' => array('type' => self::TYPE_DATE, 'validate' => 'isDate'), + 'date_upd' => array('type' => self::TYPE_DATE, 'validate' => 'isDate'), ), ); @@ -139,23 +143,32 @@ class TaxRulesGroupCore extends ObjectModel return false; } - public static function getTaxRulesGroups($only_active = true) + /** + * @param bool $only_active + * @param bool $isTourismTax true = return only tourism TRGs; false (default) = return standard TRGs + * @return array + */ + public static function getTaxRulesGroups($only_active = true, $isTourismTax = false) { return Db::getInstance()->executeS(' SELECT DISTINCT g.id_tax_rules_group, g.name, g.active FROM `'._DB_PREFIX_.'tax_rules_group` g' - .Shop::addSqlAssociation('tax_rules_group', 'g').' WHERE deleted = 0' + .Shop::addSqlAssociation('tax_rules_group', 'g').' WHERE deleted = 0 + AND g.`is_tourism_tax_rule` = '.($isTourismTax ? 1 : 0) .($only_active ? ' AND g.`active` = 1' : '').' ORDER BY name ASC'); } /** - * @return array an array of tax rules group formatted as $id => $name - */ - public static function getTaxRulesGroupsForOptions() + * @param bool $onlyActive + * @param bool $isTourismTax true = tourism TRG list with "None" placeholder; false (default) = standard TRG list with "No tax" placeholder + * @return array + */ + public static function getTaxRulesGroupsForOptions($onlyActive = true, $isTourismTax = false) { - $tax_rules[] = array('id_tax_rules_group' => 0, 'name' => Tools::displayError('No tax')); - return array_merge($tax_rules, TaxRulesGroup::getTaxRulesGroups()); + $placeholder = $isTourismTax ? Tools::displayError('None') : Tools::displayError('No tax'); + $tax_rules = array(array('id_tax_rules_group' => 0, 'name' => $placeholder)); + return array_merge($tax_rules, TaxRulesGroup::getTaxRulesGroups($onlyActive, $isTourismTax)); } public function delete() diff --git a/controllers/admin/AdminNormalProductsController.php b/controllers/admin/AdminNormalProductsController.php index 43838ccc8..83f33dc6c 100644 --- a/controllers/admin/AdminNormalProductsController.php +++ b/controllers/admin/AdminNormalProductsController.php @@ -3373,6 +3373,11 @@ class AdminNormalProductsControllerCore extends AdminController // $data->assign('productFeaturePrices', $productFeaturePrices); // } + if (Configuration::get('QLO_USE_TOURISM_TAX')) { + $data->assign('tourismTaxRulesGroups', TaxRulesGroup::getTaxRulesGroupsForOptions(true, true)); + $data->assign('id_tourism_tax_rules_group', isset($product->id_tourism_tax_rules_group) ? (int) $product->id_tourism_tax_rules_group : 0); + } + $this->tpl_form_vars['custom_form'] = $data->fetch(); } diff --git a/controllers/admin/AdminOrdersController.php b/controllers/admin/AdminOrdersController.php index 7b7c58fc2..f76246c52 100644 --- a/controllers/admin/AdminOrdersController.php +++ b/controllers/admin/AdminOrdersController.php @@ -1349,6 +1349,7 @@ class AdminOrdersControllerCore extends AdminController parent::setMedia(); $this->addJqueryUI('ui.datepicker'); + $this->addJqueryUI('ui.tooltip', 'base', true); $this->addJS(_PS_JS_DIR_.'vendor/d3.v3.min.js'); if ($this->display == 'view') { @@ -3397,12 +3398,21 @@ class AdminOrdersControllerCore extends AdminController $totalDemandsPriceTI = 0; $totalRefundedRooms = 0; $orderConvenienceFeeServices = array(); + $bookingTourismTaxMap = array(); + if (Configuration::get('QLO_USE_TOURISM_TAX')) { + $bookingTourismTaxMap = HotelOrderTourismTax::getOnlineTotalsByBooking($order->id); + } if ($order_detail_data = $objBookingDetail->getOrderFormatedBookinInfoByIdOrder($order->id)) { $objBookingDemand = new HotelBookingDemands(); $objHotelRoomType = new HotelRoomType(); foreach ($order_detail_data as $key => $value) { $order_detail_data[$key]['total_room_price_te'] = $value['total_price_tax_excl']; $order_detail_data[$key]['total_room_price_ti'] = $value['total_price_tax_incl']; + $order_detail_data[$key]['tourism_tax_amount'] = 0.0; + if (isset($bookingTourismTaxMap[$value['id']]) && $bookingTourismTaxMap[$value['id']] > 0) { + $order_detail_data[$key]['tourism_tax_amount'] = $bookingTourismTaxMap[$value['id']]; + $order_detail_data[$key]['total_room_price_ti'] += $bookingTourismTaxMap[$value['id']]; + } $order_detail_data[$key]['extra_demands'] = $objBookingDemand->getRoomTypeBookingExtraDemands( $order->id, @@ -3572,7 +3582,7 @@ class AdminOrdersControllerCore extends AdminController /*Product price when order was created*/ $totalRoomsCostTE += $value['total_price_tax_excl']; - $total_room_tax += $value['total_price_tax_incl']-$value['total_price_tax_excl']; + $total_room_tax += ($value['total_price_tax_incl'] - $value['total_price_tax_excl']) + $order_detail_data[$key]['tourism_tax_amount']; $num_days = HotelHelper::getNumberOfDays($value['date_from'], $value['date_to']); $order_detail_data[$key]['unit_amt_tax_excl'] = $value['total_price_tax_excl']/$num_days; $order_detail_data[$key]['unit_amt_tax_incl'] = $value['total_price_tax_incl']/$num_days; @@ -3580,6 +3590,10 @@ 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']; + if (Configuration::get('QLO_USE_TOURISM_TAX')) { + $order_detail_data[$key]['tourism_tax_status'] = HotelOrderTourismTax::getStatusForBooking($value['id']); + $order_detail_data[$key]['tourism_tax_total'] = HotelOrderTourismTax::getTotalForBooking($value['id']); + } if (isset($value['refund_info']) && $value['refund_info']['refunded'] @@ -3594,6 +3608,20 @@ class AdminOrdersControllerCore extends AdminController $order_detail_data = array(); } + $orderTourismTaxHasApplied = false; + $orderTourismTaxHasUnapplied = false; + if (Configuration::get('QLO_USE_TOURISM_TAX') && $order_detail_data) { + foreach ($order_detail_data as $bookingRow) { + if (isset($bookingRow['tourism_tax_status'])) { + if ($bookingRow['tourism_tax_status'] === HotelOrderTourismTax::STATUS_APPLIED) { + $orderTourismTaxHasApplied = true; + } else { + $orderTourismTaxHasUnapplied = true; + } + } + } + } + if ($orderedRooms = $objServiceProductOrderDetail->getRoomTypeServiceProducts( $order->id, 0, @@ -3722,6 +3750,12 @@ class AdminOrdersControllerCore extends AdminController 'htl_booking_order_data' => $bookingOrderInfo, 'hotel_order_status' => $htlOrderStatus, 'order_detail_data' => $order_detail_data, + 'use_tourism_tax' => (bool) Configuration::get('QLO_USE_TOURISM_TAX'), + 'info_icon_path' => $this->context->link->getMediaLink(_MODULE_DIR_.'hotelreservationsystem/views/img/Slices/icon-info.svg'), + 'tourism_tax_status_applied' => HotelOrderTourismTax::STATUS_APPLIED, + 'tourism_tax_status_exempted' => HotelOrderTourismTax::STATUS_EXEMPTED, + 'order_tourism_tax_has_applied' => $orderTourismTaxHasApplied, + 'order_tourism_tax_has_unapplied' => $orderTourismTaxHasUnapplied, 'max_child_age' => Configuration::get('WK_GLOBAL_CHILD_MAX_AGE'), 'hotel_service_products' => $orderHotelServiceProducts, 'standalone_service_products' => $orderStandaloneServiceProducts, @@ -8947,4 +8981,93 @@ class AdminOrdersControllerCore extends AdminController $this->ajaxDie(json_encode($result)); } + + /** + * Apply tourism tax to a single booking (id_htl_booking) or all bookings of an order (id_order). + */ + public function ajaxProcessApplyTourismTax() + { + $idHtlBooking = (int) Tools::getValue('id_htl_booking', 0); + $idOrder = (int) Tools::getValue('id_order', 0); + $errors = array(); + + if ($idHtlBooking) { + $result = HotelOrderTourismTax::applyForBooking($idHtlBooking); + if ($result === HotelOrderTourismTax::APPLY_ERROR_RESTORE) { + $errors[] = $this->l('Could not restore tourism tax for this booking.'); + } elseif ($result === HotelOrderTourismTax::APPLY_ERROR_NO_RULE) { + $errors[] = $this->l('No tourism tax rule is assigned to this room type.'); + } elseif ($result === HotelOrderTourismTax::APPLY_ERROR_REFUNDED) { + $errors[] = $this->l('Tourism tax cannot be applied to a refunded booking.'); + } + } elseif ($idOrder) { + $hasNoRuleError = false; + $hasRestoreError = false; + foreach (HotelOrderTourismTax::applyForOrder($idOrder) as $failure) { + if ($failure['error'] === HotelOrderTourismTax::APPLY_ERROR_RESTORE) { + $hasRestoreError = true; + } else { + $hasNoRuleError = true; + } + } + if ($hasRestoreError) { + $errors[] = $this->l('Could not restore tourism tax for one or more bookings.'); + } + if ($hasNoRuleError) { + $errors[] = $this->l('One or more bookings do not have a tourism tax rule assigned.'); + } + } else { + $errors[] = $this->l('Invalid request parameters.'); + } + + die(Tools::jsonEncode(array( + 'hasError' => !empty($errors), + 'errors' => $errors, + ))); + } + + /** + * Exempt tourism tax for a single booking (id_htl_booking) or all bookings of an order (id_order). + */ + public function ajaxProcessExemptTourismTax() + { + $idHtlBooking = (int) Tools::getValue('id_htl_booking', 0); + $idOrder = (int) Tools::getValue('id_order', 0); + $idEmployee = (int) Context::getContext()->employee->id; + $errors = array(); + + if ($idHtlBooking) { + $result = HotelOrderTourismTax::exemptIfApplied($idHtlBooking, $idEmployee); + if ($result === HotelOrderTourismTax::EXEMPT_ERROR_NO_RULE) { + $errors[] = $this->l('No tourism tax rule is assigned to this room type.'); + } elseif ($result === HotelOrderTourismTax::EXEMPT_ERROR_SAVE) { + $errors[] = $this->l('Could not exempt tourism tax for this booking.'); + } elseif ($result === HotelOrderTourismTax::EXEMPT_ERROR_REFUNDED) { + $errors[] = $this->l('Tourism tax cannot be exempted for a refunded booking.'); + } + } elseif ($idOrder) { + $hasNoRuleError = false; + $hasSaveError = false; + foreach (HotelOrderTourismTax::exemptForOrder($idOrder, $idEmployee) as $failure) { + if ($failure['error'] === HotelOrderTourismTax::EXEMPT_ERROR_NO_RULE) { + $hasNoRuleError = true; + } else { + $hasSaveError = true; + } + } + if ($hasNoRuleError) { + $errors[] = $this->l('One or more bookings do not have a tourism tax rule assigned.'); + } + if ($hasSaveError) { + $errors[] = $this->l('Could not exempt tourism tax for one or more bookings.'); + } + } else { + $errors[] = $this->l('Invalid request parameters.'); + } + + die(Tools::jsonEncode(array( + 'hasError' => !empty($errors), + 'errors' => $errors, + ))); + } } diff --git a/controllers/admin/AdminProductsController.php b/controllers/admin/AdminProductsController.php index 9b8e1ab48..25fcc213e 100644 --- a/controllers/admin/AdminProductsController.php +++ b/controllers/admin/AdminProductsController.php @@ -4157,6 +4157,11 @@ class AdminProductsControllerCore extends AdminController $data->assign('productFeaturePrices', $productFeaturePrices); } + if (Configuration::get('QLO_USE_TOURISM_TAX')) { + $data->assign('tourismTaxRulesGroups', TaxRulesGroup::getTaxRulesGroupsForOptions(true, true)); + $data->assign('id_tourism_tax_rules_group', isset($product->id_tourism_tax_rules_group) ? (int) $product->id_tourism_tax_rules_group : 0); + } + $this->tpl_form_vars['custom_form'] = $data->fetch(); } diff --git a/controllers/admin/AdminTaxRulesGroupController.php b/controllers/admin/AdminTaxRulesGroupController.php index 894e7f257..3a64f4d3f 100644 --- a/controllers/admin/AdminTaxRulesGroupController.php +++ b/controllers/admin/AdminTaxRulesGroupController.php @@ -96,6 +96,59 @@ class AdminTaxRulesGroupControllerCore extends AdminController return parent::renderList(); } + /** + * Return taxes for the rule-form dropdown, filtered by whether this TRG is a tourism group. + * + * @return array + */ + protected function getTaxOptionsForGroup() + { + $idTrg = (int) Tools::getValue('id_tax_rules_group'); + $isTourism = false; + if ($idTrg) { + $trg = new TaxRulesGroup($idTrg); + if (Validate::isLoadedObject($trg)) { + $isTourism = (bool) $trg->is_tourism_tax_rule; + } + } + + $idLang = (int) $this->context->language->id; + $sql = new DbQuery(); + if ($isTourism) { + $sql->select( + 't.`id_tax`, tl.`name`,' + . ' IFNULL(ht.`tax_type`, 0) AS ht_tax_type,' + . ' IFNULL(ht.`tax_value`, 0) AS ht_tax_value' + ); + } else { + $sql->select('t.`id_tax`, tl.`name`, t.`rate`'); + } + $sql->from('tax', 't'); + $sql->leftJoin('tax_lang', 'tl', 't.`id_tax` = tl.`id_tax` AND tl.`id_lang` = ' . $idLang); + if ($isTourism) { + $sql->leftJoin('htl_tourism_tax', 'ht', 'ht.`id_tax` = t.`id_tax`'); + } + $sql->where('t.`deleted` != 1 AND t.`active` = 1'); + $sql->where('t.`is_tourism_tax` = ' . ($isTourism ? 1 : 0)); + $sql->orderBy('tl.`name` ASC'); + + $rows = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql); + + if ($isTourism && is_array($rows)) { + $currency = $this->context->currency; + $currencySign = Tools::safeOutput(trim($currency->prefix . $currency->suffix)); + foreach ($rows as &$row) { + $typeLabel = ((int) $row['ht_tax_type'] === 1) + ? (float) $row['ht_tax_value'] . '%' + : $currencySign . ' ' . (float) $row['ht_tax_value']; + $row['name'] = $row['name'] . ' (' . $typeLabel . ')'; + } + unset($row); + } + + return $rows; + } + public function initRulesList($id_group) { $this->table = 'tax_rule'; @@ -127,7 +180,10 @@ class AdminTaxRulesGroupControllerCore extends AdminController ), 'rate' => array( 'title' => $this->l('Tax'), - 'class' => 'fixed-width-sm' + 'class' => 'fixed-width-sm', + 'callback' => 'displayTaxRuleRate', + 'orderby' => false, + 'search' => false, ), 'description' => array( 'title' => $this->l('Description') @@ -141,7 +197,10 @@ class AdminTaxRulesGroupControllerCore extends AdminController c.`name` AS country_name, s.`name` AS state_name, CONCAT_WS(" - ", a.`zipcode_from`, a.`zipcode_to`) AS zipcode, - t.rate'; + t.`rate`, + t.`is_tourism_tax`, + IFNULL(ht.`tax_type`, 0) AS ht_tax_type, + IFNULL(ht.`tax_value`, 0) AS ht_tax_value'; $this->_join = ' LEFT JOIN `'._DB_PREFIX_.'country_lang` c @@ -149,7 +208,9 @@ class AdminTaxRulesGroupControllerCore extends AdminController LEFT JOIN `'._DB_PREFIX_.'state` s ON (a.`id_state` = s.`id_state`) LEFT JOIN `'._DB_PREFIX_.'tax` t - ON (a.`id_tax` = t.`id_tax`)'; + ON (a.`id_tax` = t.`id_tax`) + LEFT JOIN `'._DB_PREFIX_.'htl_tourism_tax` ht + ON (ht.`id_tax` = t.`id_tax`)'; $this->_where = 'AND `id_tax_rules_group` = '.(int)$id_group; $this->_use_found_rows = false; @@ -194,7 +255,19 @@ class AdminTaxRulesGroupControllerCore extends AdminController 'label' => $this->l('Disabled') ) ) - ) + ), + array( + 'type' => 'switch', + 'label' => $this->l('Tourism tax rule group'), + 'name' => 'is_tourism_tax_rule', + 'required' => false, + 'is_bool' => true, + 'hint' => $this->l('Mark this group as tourism-only. All tax rules added here must reference tourism taxes.'), + 'values' => array( + array('id' => 'is_tourism_tax_rule_on', 'value' => 1, 'label' => $this->l('Yes')), + array('id' => 'is_tourism_tax_rule_off', 'value' => 0, 'label' => $this->l('No')), + ) + ), ), 'submit' => array( 'title' => $this->l('Save and stay'), @@ -309,7 +382,7 @@ class AdminTaxRulesGroupControllerCore extends AdminController $this->l('- This tax only: Will apply only this tax').'
    ', $this->l('- Combine: Combine taxes (e.g.: 10% + 5% = 15%)').'
    ', $this->l('- One after another: Apply taxes one after another (e.g.: 0 + 10% = 0 + 5% = 5.5)') - ) + ), ), array( 'type' => 'select', @@ -317,7 +390,7 @@ class AdminTaxRulesGroupControllerCore extends AdminController 'name' => 'id_tax', 'required' => false, 'options' => array( - 'query' => Tax::getTaxes((int)$this->context->language->id), + 'query' => $this->getTaxOptionsForGroup(), 'id' => 'id_tax', 'name' => 'name', 'default' => array( @@ -325,7 +398,7 @@ class AdminTaxRulesGroupControllerCore extends AdminController 'label' => $this->l('No Tax') ) ), - 'hint' => sprintf($this->l('(Total tax: %s)'), '9%') + 'hint' => $this->l('Select the tax to apply for this rule.') ), array( 'type' => 'text', @@ -374,6 +447,86 @@ class AdminTaxRulesGroupControllerCore extends AdminController } + /** + * List column callback: render the Tax column in the tax rules list. + * Tourism-fixed taxes show the currency sign; tourism-percent and regular VAT taxes show %. + * + * @param mixed $value t.rate (always 0 for tourism taxes) + * @param array $row Full list row — includes is_tourism_tax, ht_tax_type, ht_tax_value + * @return string + */ + public function displayTaxRuleRate($value, $row) + { + if (!(int) $row['is_tourism_tax']) { + return Tools::safeOutput($value) . '%'; + } + $displayValue = Tools::safeOutput((float) $row['ht_tax_value']); + if ((int) $row['ht_tax_type'] === 1) { + return $displayValue . '%'; + } + $currency = $this->context->currency; + $currencySign = Tools::safeOutput(trim($currency->prefix . $currency->suffix)); + return $currencySign . ' ' . $displayValue; + } + + public function postProcess() + { + if ($this->action == 'save') { + $idGroup = (int) Tools::getValue('id_tax_rules_group'); + $newIsTourism = (int) Tools::getValue('is_tourism_tax_rule'); + + if ($idGroup) { + $trg = new TaxRulesGroup($idGroup); + if (Validate::isLoadedObject($trg)) { + $oldIsTourism = (int) $trg->is_tourism_tax_rule; + + // Tourism → VAT: block while tourism tax rules still exist in this group. + if ($oldIsTourism === 1 && $newIsTourism === 0) { + $hasConflict = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + 'SELECT COUNT(*) FROM `' . _DB_PREFIX_ . 'tax_rule` tr + INNER JOIN `' . _DB_PREFIX_ . 'tax` t ON t.`id_tax` = tr.`id_tax` + WHERE tr.`id_tax_rules_group` = ' . $idGroup . ' + AND t.`is_tourism_tax` = 1' + ); + if ($hasConflict) { + $this->errors[] = $this->l( + 'Cannot convert this to a VAT group: it still contains tourism taxes. Remove the tourism tax rules first.' + ); + // Reset POST to DB state: getFieldValue() prefers $_POST over the DB + // object, so without this reset the toggle, behavior dropdown, and tax + // dropdown would render inconsistently (toggle=NO, sub-forms=tourism state). + $_POST['is_tourism_tax_rule'] = $oldIsTourism; + $this->display = 'edit'; + return; + } + } + + // VAT → Tourism: block while regular VAT tax rules still exist in this group. + // "No Tax" rules (id_tax = 0) are type-neutral and are excluded from the check. + if ($oldIsTourism === 0 && $newIsTourism === 1) { + $hasConflict = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + 'SELECT COUNT(*) FROM `' . _DB_PREFIX_ . 'tax_rule` tr + INNER JOIN `' . _DB_PREFIX_ . 'tax` t ON t.`id_tax` = tr.`id_tax` + WHERE tr.`id_tax_rules_group` = ' . $idGroup . ' + AND tr.`id_tax` != 0 + AND t.`is_tourism_tax` = 0' + ); + if ($hasConflict) { + $this->errors[] = $this->l( + 'Cannot convert this to a tourism group: it still contains VAT taxes. Remove the VAT tax rules first.' + ); + // Same reason: reset POST to DB state so the whole form is consistent. + $_POST['is_tourism_tax_rule'] = $oldIsTourism; + $this->display = 'edit'; + return; + } + } + } + } + } + parent::postProcess(); + } + public function initProcess() { if (Tools::isSubmit('deletetax_rule')) { diff --git a/controllers/admin/AdminTaxesController.php b/controllers/admin/AdminTaxesController.php index ca2787e05..0997006c2 100644 --- a/controllers/admin/AdminTaxesController.php +++ b/controllers/admin/AdminTaxesController.php @@ -49,7 +49,21 @@ class AdminTaxesControllerCore extends AdminController $this->fields_list = array( 'id_tax' => array('title' => $this->l('ID'), 'align' => 'center', 'class' => 'fixed-width-xs'), 'name' => array('title' => $this->l('Name'), 'width' => 'auto'), - 'rate' => array('title' => $this->l('Rate'), 'align' => 'center', 'suffix' => '%' , 'class' => 'fixed-width-md'), + 'is_tourism_tax' => array( + 'title' => $this->l('Tourism Tax'), + 'align' => 'center', + 'class' => 'fixed-width-sm', + 'callback' => 'displayTourismTaxBadge', + 'orderby' => false, + 'search' => false, + ), + 'rate' => array( + 'title' => $this->l('Rate'), + 'align' => 'center', + 'class' => 'fixed-width-md', + 'callback' => 'displayRate', + 'orderby' => false, + ), 'active' => array('title' => $this->l('Enabled'), 'width' => 25, 'align' => 'center', 'active' => 'status', 'type' => 'bool', 'orderby' => false, 'class' => 'fixed-width-sm', 'remove_onclick' => true) ); @@ -110,9 +124,30 @@ class AdminTaxesControllerCore extends AdminController ); } + $this->fields_options['general']['fields']['QLO_USE_TOURISM_TAX'] = array( + 'title' => $this->l('Use tourism tax'), + 'hint' => $this->l('Enable tourism / city tax calculation for hotel room bookings.'), + 'validation' => 'isBool', + 'cast' => 'intval', + 'type' => 'bool', + ); + + if (Configuration::get('QLO_USE_TOURISM_TAX') || Tools::getValue('QLO_USE_TOURISM_TAX')) { + $this->fields_options['general']['fields']['QLO_TOURISM_TAX_GROSSED_UP'] = array( + 'title' => $this->l('Include tourism tax in displayed room price'), + 'hint' => $this->l('Yes: rolled into the headline room price. No: shown as a separate line item.'), + 'validation' => 'isBool', + 'cast' => 'intval', + 'type' => 'bool', + ); + } + parent::__construct(); $this->_where .= ' AND a.deleted = 0'; + $this->_join .= ' LEFT JOIN `'._DB_PREFIX_.'htl_tourism_tax` ht ON ht.`id_tax` = a.`id_tax`'; + $extraSelect = 'IFNULL(ht.`tax_type`, 0) AS `ht_tax_type`, IFNULL(ht.`tax_value`, 0) AS `ht_tax_value`'; + $this->_select = ($this->_select ? $this->_select . ', ' : '') . $extraSelect; } public function initPageHeaderToolbar() @@ -189,27 +224,247 @@ class AdminTaxesControllerCore extends AdminController public function renderForm() { + $idTax = (int) Tools::getValue('id_' . $this->table); + $subtype = null; + $tiers = array(); + $childRanges = array(); + $contextCurrency = $this->context->currency; + $currencySign = Tools::safeOutput(trim($contextCurrency->prefix . $contextCurrency->suffix)); + + $isAnySubmit = Tools::isSubmit('submitAdd' . $this->table) || Tools::isSubmit('submitAdd' . $this->table . 'AndStay'); + + if ($idTax && !$isAnySubmit) { + $subtype = HotelTourismTax::getByTaxId($idTax); + if ($subtype) { + $_POST['htl_tax_type'] = $subtype->tax_type; + $_POST['htl_is_per_night'] = $subtype->is_per_night; + $_POST['htl_is_per_person'] = $subtype->is_per_person; + $_POST['htl_tax_value'] = $subtype->tax_value; + $_POST['htl_is_tiered'] = $subtype->is_tiered; + $_POST['htl_has_child_rate'] = $subtype->has_child_rate; + $_POST['htl_valid_from'] = $subtype->valid_from ?: ''; + $_POST['htl_valid_to'] = $subtype->valid_to ?: ''; + + $tiers = HotelTourismTaxTier::getByTaxId($idTax); + $childRanges = HotelTourismTaxChildRange::getByTaxId($idTax); + } + } elseif ($isAnySubmit) { + $postTierIds = (array) Tools::getValue('htl_tier_id', array()); + $postTierMins = (array) Tools::getValue('htl_tier_min', array()); + $postTierMaxs = (array) Tools::getValue('htl_tier_max', array()); + $postTierValues = (array) Tools::getValue('htl_tier_value', array()); + foreach ($postTierIds as $i => $tierId) { + $tiers[] = array( + 'id_tier' => (int) $tierId, + 'min_amount' => isset($postTierMins[$i]) ? $postTierMins[$i] : '', + 'max_amount' => isset($postTierMaxs[$i]) ? $postTierMaxs[$i] : '', + 'tax_value' => isset($postTierValues[$i]) ? $postTierValues[$i] : '', + ); + } + + $postChildIds = (array) Tools::getValue('htl_child_id', array()); + $postChildMins = (array) Tools::getValue('htl_child_min', array()); + $postChildMaxs = (array) Tools::getValue('htl_child_max', array()); + $postChildValues = (array) Tools::getValue('htl_child_value', array()); + foreach ($postChildIds as $i => $childId) { + $childRanges[] = array( + 'id_child_range' => (int) $childId, + 'min_age' => isset($postChildMins[$i]) ? $postChildMins[$i] : '', + 'max_age' => isset($postChildMaxs[$i]) ? $postChildMaxs[$i] : '', + 'tax_value' => isset($postChildValues[$i]) ? $postChildValues[$i] : '', + ); + } + } + + $taxType = (int) Tools::getValue('htl_tax_type', 0); + $baseTaxValue = (float) Tools::getValue('htl_tax_value', $subtype ? $subtype->tax_value : 0); + $allDayKeys = array('mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'); + if ($subtype && (int) $subtype->is_special_days_exists && $subtype->special_days) { + $defaultSpecialDays = json_decode($subtype->special_days, true) ?: $allDayKeys; + } else { + $defaultSpecialDays = $allDayKeys; + } + $specialDaysChecked = Tools::getValue('htl_valid_days', $defaultSpecialDays); + if (!is_array($specialDaysChecked)) { + $specialDaysChecked = $allDayKeys; + } + + $initialTypeSign = ($taxType == 1) ? '%' : $currencySign; + $requiredMarker = ' *'; + $taxValueInputHtml = ' +
    + ' . $initialTypeSign . ' + +
    '; + $rateCurrentValue = (float) Tools::getValue('rate', (isset($this->object) && $this->object) ? $this->object->rate : 0); + $rateInputHtml = ' +
    + % + +
    '; + $priceBracketsHtml = ' + + + + + + + + '; + foreach ($tiers as $tierRowIndex => $tier) { + $priceBracketsHtml .= $this->buildTierRow($tierRowIndex, $tier); + } + $priceBracketsHtml .= ' + +
    ' . $this->l('Min price (from)') . $requiredMarker . '' . $this->l('Max price (up to, 0 = no cap)') . $requiredMarker . '' . $this->l('Tax value') . $requiredMarker . '
    +
    + +
    + '; + + $ageBandsHtml = ' + + + + + + + + '; + foreach ($childRanges as $rangeRowIndex => $range) { + $ageBandsHtml .= $this->buildChildRangeRow($rangeRowIndex, $range); + } + $ageBandsHtml .= ' + +
    ' . $this->l('Min age') . '' . $this->l('Max age (0 = no cap)') . '' . $this->l('Tax value') . $requiredMarker . '
    +
    + +
    + '; + + $validFromValue = Tools::getValue('htl_valid_from', $subtype ? ($subtype->valid_from ?: '') : ''); + $validToValue = Tools::getValue('htl_valid_to', $subtype ? ($subtype->valid_to ?: '') : ''); + $weekdayLabels = array( + 'mon' => $this->l('Mon'), 'tue' => $this->l('Tue'), 'wed' => $this->l('Wed'), + 'thu' => $this->l('Thu'), 'fri' => $this->l('Fri'), 'sat' => $this->l('Sat'), + 'sun' => $this->l('Sun'), + ); + + $weekdayCheckboxesHtml = '
    '; + foreach ($weekdayLabels as $dayKey => $label) { + $checked = in_array($dayKey, $specialDaysChecked) ? ' checked="checked"' : ''; + $weekdayCheckboxesHtml .= ' + '; + } + $weekdayCheckboxesHtml .= '
    '; + + $validityInputHtml = ' +
    +
    +
    + ' . $this->l('From') . ' + + +
    +
    +
    +
    + ' . $this->l('To') . ' + + +
    + +
    +
    '; + $this->fields_form = array( 'legend' => array( 'title' => $this->l('Taxes'), - 'icon' => 'icon-money' + 'icon' => 'icon-money', ), 'input' => array( + array( + 'type' => 'switch', + 'label' => $this->l('Is tourism tax'), + 'name' => 'is_tourism_tax', + 'is_bool' => true, + 'hint' => $this->l('When enabled, this tax is used as a tourism / city tax. Rate is forced to 0 and parameters below apply.'), + 'values' => array( + array('id' => 'is_tourism_tax_on', 'value' => 1, 'label' => $this->l('Yes')), + array('id' => 'is_tourism_tax_off', 'value' => 0, 'label' => $this->l('No')), + ), + ), array( 'type' => 'text', 'label' => $this->l('Name'), 'name' => 'name', 'required' => true, 'lang' => true, - 'hint' => $this->l('Tax name to display in carts and on invoices (e.g. "VAT").').' - '.$this->l('Invalid characters').' <>;=#{}' + 'hint' => $this->l('Tax name to display in carts and on invoices (e.g. "City Tax").') + . ' - ' . $this->l('Invalid characters') . ' <>;=#{}', ), array( - 'type' => 'text', + 'type' => 'select', + 'label' => $this->l('Tax Calculation Type'), + 'name' => 'htl_tax_type', + 'form_group_class' => 'tt-field', + 'options' => array( + 'query' => array( + array('id' => 0, 'name' => $this->l('Fixed amount per unit')), + array('id' => 1, 'name' => $this->l('Percentage of room price (excl. VAT)')), + ), + 'id' => 'id', + 'name' => 'name', + ), + ), + array( + 'type' => 'html', + 'label' => $this->l('Tax value'), + 'name' => 'htl_tax_value_wrap', + 'html_content' => $taxValueInputHtml, + 'hint' => $this->l('Leave 0 and enable tiered pricing below for bracket-based rates.'), + 'form_group_class' => 'tt-field', + ), + array( + 'type' => 'html', 'label' => $this->l('Rate'), - 'name' => 'rate', - 'maxlength' => 6, - 'required' => true, - 'hint' => $this->l('Format: XX.XX or XX.XXX (e.g. 19.60 or 13.925)').' - '.$this->l('Invalid characters').' <>;=#{}' + 'name' => 'rate_wrap', + 'html_content' => $rateInputHtml, + 'hint' => $this->l('Format: XX.XX or XX.XXX (e.g. 19.60 or 13.925)') + . ' - ' . $this->l('Invalid characters') . ' <>;=#{}', + 'form_group_class' => 'tt-non-tourism', + ), + array( + 'type' => 'select', + 'label' => $this->l('Price calculation method'), + 'name' => 'htl_is_per_night', + 'hint' => $this->l('Select whether the tax amount is applied per night or once for the entire booking range.'), + 'form_group_class' => 'tt-field', + 'options' => array( + 'query' => array( + array('id' => 1, 'name' => $this->l('Add per each night of booking')), + array('id' => 0, 'name' => $this->l('Add once for the booking range')), + ), + 'id' => 'id', + 'name' => 'name', + ), ), array( 'type' => 'switch', @@ -229,15 +484,81 @@ class AdminTaxesControllerCore extends AdminController 'label' => $this->l('Disabled') ) ) - ) - ), - 'submit' => array( - 'title' => $this->l('Save') + ), + array( + 'type' => 'switch', + 'label' => $this->l('Per person (occupancy wise)'), + 'name' => 'htl_is_per_person', + 'is_bool' => true, + 'hint' => $this->l('When enabled, the tax amount multiplies by the number of adult occupants.'), + 'form_group_class' => 'tt-field', + 'values' => array( + array('id' => 'per_person_on', 'value' => 1, 'label' => $this->l('Yes')), + array('id' => 'per_person_off', 'value' => 0, 'label' => $this->l('No')), + ), + ), + array( + 'type' => 'switch', + 'label' => $this->l('Apply on child'), + 'name' => 'htl_has_child_rate', + 'is_bool' => true, + 'hint' => $this->l('Define the age band below. Children within the band pay the band\'s rate; all others pay the adult rate.'), + 'form_group_class' => 'tt-field', + 'values' => array( + array('id' => 'child_rate_on', 'value' => 1, 'label' => $this->l('Yes')), + array('id' => 'child_rate_off', 'value' => 0, 'label' => $this->l('No')), + ), + ), + array( + 'type' => 'html', + 'label' => $this->l('Child age bands'), + 'name' => 'htl_child_ranges', + 'html_content' => $ageBandsHtml, + 'form_group_class' => 'tt-field tt-child-field', + ), + array( + 'type' => 'switch', + 'label' => $this->l('Tiered pricing'), + 'name' => 'htl_is_tiered', + 'is_bool' => true, + 'hint' => $this->l('When a room price falls within a bracket, that bracket\'s value is used. Prices outside all brackets fall back to the base Tax value above.'), + 'form_group_class' => 'tt-field', + 'values' => array( + array('id' => 'tiered_on', 'value' => 1, 'label' => $this->l('Yes')), + array('id' => 'tiered_off', 'value' => 0, 'label' => $this->l('No')), + ), + ), + array( + 'type' => 'html', + 'label' => $this->l('Price brackets'), + 'name' => 'htl_tiers', + 'html_content' => $priceBracketsHtml, + 'form_group_class' => 'tt-field tt-tiered-field', + ), + array( + 'type' => 'html', + 'label' => $this->l('Valid'), + 'name' => 'htl_validity_wrap', + 'html_content' => $validityInputHtml, + 'hint' => $this->l('Leave blank for always active.'), + 'form_group_class' => 'tt-field', + ), + array( + 'type' => 'html', + 'label' => $this->l('Valid days'), + 'name' => 'htl_valid_days_wrap', + 'html_content' => $weekdayCheckboxesHtml + . '

    ' + . $this->l('Uncheck days when the tax should not apply within the date range.') + . '

    ', + 'form_group_class' => 'tt-field', + ), ), + 'submit' => array('title' => $this->l('Save')), 'buttons' => array( 'save-and-stay' => array( 'title' => $this->l('Save and stay'), - 'name' => 'submitAdd'.$this->table.'AndStay', + 'name' => 'submitAdd' . $this->table . 'AndStay', 'type' => 'submit', 'class' => 'btn btn-default pull-right', 'icon' => 'process-icon-save', @@ -248,25 +569,162 @@ class AdminTaxesControllerCore extends AdminController return parent::renderForm(); } + /** + * @param int|string $idx Row index or '__IDX__' for JS template + * @param array $tier + * @return string + */ + protected function buildTierRow($idx, array $tier) + { + $min = isset($tier['min_amount']) ? (float) $tier['min_amount'] : ''; + $max = isset($tier['max_amount']) ? (float) $tier['max_amount'] : ''; + $val = isset($tier['tax_value']) ? (float) $tier['tax_value'] : ''; + $id = isset($tier['id_tier']) ? (int) $tier['id_tier'] : 0; + + return ' + + + + + + + + +
    + + +
    + + + + + + + + '; + } + + /** + * @param int|string $idx Row index or '__IDX__' for JS template + * @param array $range + * @return string + */ + protected function buildChildRangeRow($idx, array $range) + { + $min = isset($range['min_age']) ? (int) $range['min_age'] : ''; + $max = isset($range['max_age']) ? (int) $range['max_age'] : ''; + $val = isset($range['tax_value']) ? (float) $range['tax_value'] : ''; + $id = isset($range['id_child_range']) ? (int) $range['id_child_range'] : 0; + + return ' + + + + + + + + +
    + + +
    + + + + + + + + '; + } + + /** + * List column callback: display a Yes/No badge for is_tourism_tax. + * + * @param mixed $value 1 or 0 + * @param array $row + * @return string + */ + public function displayTourismTaxBadge($value, $row) + { + if ($value) { + return '' . $this->l('Yes') . ''; + } + return '' . $this->l('No') . ''; + } + + /** + * List column callback: display tax rate with the correct symbol. + * Tourism-fixed taxes show currency sign; tourism-percent and regular taxes show %. + * + * @param mixed $value ps_tax.rate + * @param array $row Full list row (includes ht_tax_type, ht_tax_value from join) + * @return string + */ + public function displayRate($value, $row) + { + if (!(int) $row['is_tourism_tax']) { + return Tools::safeOutput($value) . '%'; + } + $displayValue = Tools::safeOutput((float) $row['ht_tax_value']); + if ((int) $row['ht_tax_type'] === 1) { + return $displayValue . '%'; + } + $activeCurrency = $this->context->currency; + $currencySign = Tools::safeOutput(trim($activeCurrency->prefix . $activeCurrency->suffix)); + return $currencySign . ' ' . $displayValue; + } + public function postProcess() { if ($this->action == 'save') { + if ((int) Tools::getValue('is_tourism_tax')) { + $_POST['rate'] = '0'; + } /* Checking fields validity */ $this->validateRules(); + $this->validateTourismTaxFields(); if (!count($this->errors)) { - $id = (int)(Tools::getValue('id_'.$this->table)); + $existingTaxId = (int)(Tools::getValue('id_'.$this->table)); + $isTourismTax = (int) Tools::getValue('is_tourism_tax'); /* Object update */ - if (isset($id) && !empty($id)) { + if (isset($existingTaxId) && !empty($existingTaxId)) { /** @var Tax $object */ - $object = new $this->className($id); + $object = new $this->className($existingTaxId); if (Validate::isLoadedObject($object)) { + $wasTourismTax = (int) $object->is_tourism_tax; $this->copyFromPost($object, $this->table); + if ($isTourismTax) { + $object->rate = 0; + } $result = $object->update(false, false); if (!$result) { $this->errors[] = Tools::displayError('An error occurred while updating an object.').' '.$this->table.''; } elseif ($this->postImage($object->id)) { + if ($object->id !== $existingTaxId) { + $oldSubtype = HotelTourismTax::getByTaxId($existingTaxId); + if ($oldSubtype) { + $oldSubtype->delete(); + } + } + if ($isTourismTax) { + $this->saveTourismTaxSubtype($object->id); + } + if ($wasTourismTax && !$isTourismTax) { + $this->cleanTaxFromTourismTrgs($object->id); + } + if (!$wasTourismTax && $isTourismTax) { + $this->cleanTaxFromVatTrgs($object->id); + } if (Tools::isSubmit('submitAdd'.$this->table.'AndStay')) { Tools::redirectAdmin(self::$currentIndex.'&update'.$this->table.'&id_'.$this->table.'='.$object->id.'&conf=4'.'&token='.$this->token); } else { @@ -283,15 +741,20 @@ class AdminTaxesControllerCore extends AdminController /** @var Tax $object */ $object = new $this->className(); $this->copyFromPost($object, $this->table); + if ($isTourismTax) { + $object->rate = 0; + } if (!$object->add()) { $this->errors[] = Tools::displayError('An error occurred while creating an object.').' '.$this->table.''; } elseif ($this->postImage($object->id)) { + if ($isTourismTax) { + $this->saveTourismTaxSubtype($object->id); + } if (Tools::isSubmit('submitAdd'.$this->table.'AndStay')) { Tools::redirectAdmin(self::$currentIndex.'&update'.$this->table.'&id_'.$this->table.'='.$object->id.'&conf=3'.'&token='.$this->token); } else { Tools::redirectAdmin(self::$currentIndex.'&conf=3&token='.$this->token); } - Tools::redirectAdmin(self::$currentIndex.'&id_'.$this->table.'='.$object->id.'&conf=3'.'&token='.$this->token); } } } else { @@ -302,6 +765,226 @@ class AdminTaxesControllerCore extends AdminController } } + /** + * Validate tourism-tax-specific fields and append to $this->errors. + */ + protected function validateTourismTaxFields() + { + if (!(int) Tools::getValue('is_tourism_tax')) { + return; + } + + $taxValue = Tools::getValue('htl_tax_value', ''); + if ($taxValue !== '') { + if (!is_numeric($taxValue)) { + $this->errors[] = $this->l('Tax value must be a valid number.'); + } elseif ((float) $taxValue < 0) { + $this->errors[] = $this->l('Tax value cannot be negative.'); + } + } + + $validFrom = trim(Tools::getValue('htl_valid_from', '')); + $validTo = trim(Tools::getValue('htl_valid_to', '')); + if ($validFrom === '0000-00-00') { $validFrom = ''; } + if ($validTo === '0000-00-00') { $validTo = ''; } + if ($validFrom !== '' && !Validate::isDate($validFrom)) { + $this->errors[] = $this->l('"Valid from" is not a valid date (use YYYY-MM-DD).'); + } + if ($validTo !== '' && !Validate::isDate($validTo)) { + $this->errors[] = $this->l('"Valid to" is not a valid date (use YYYY-MM-DD).'); + } + if ($validFrom !== '' && $validTo !== '' + && Validate::isDate($validFrom) && Validate::isDate($validTo) + && strtotime($validTo) < strtotime($validFrom) + ) { + $this->errors[] = $this->l('"Valid to" must be on or after "Valid from".'); + } + + if ((int) Tools::getValue('htl_has_child_rate')) { + $submittedBandMins = Tools::getValue('htl_child_min', array()); + $submittedBandMaxs = Tools::getValue('htl_child_max', array()); + $submittedBandValues = Tools::getValue('htl_child_value', array()); + $filledBands = array(); + foreach ($submittedBandValues as $rowIndex => $taxValue) { + if ($taxValue === '' || $taxValue === false) { + continue; + } + $rowNum = (int) $rowIndex + 1; + $bandMin = (isset($submittedBandMins[$rowIndex]) && $submittedBandMins[$rowIndex] !== '') + ? (int) $submittedBandMins[$rowIndex] : 0; + $bandMax = (isset($submittedBandMaxs[$rowIndex]) && $submittedBandMaxs[$rowIndex] !== '') + ? (int) $submittedBandMaxs[$rowIndex] : 0; + if (!is_numeric($taxValue)) { + $this->errors[] = sprintf($this->l('Child band #%d: tax value must be a valid number.'), $rowNum); + } elseif ((float) $taxValue < 0) { + $this->errors[] = sprintf($this->l('Child band #%d: tax value cannot be negative.'), $rowNum); + } + if ($bandMax > 0 && $bandMin > $bandMax) { + $this->errors[] = sprintf($this->l('Child band #%d: min age cannot be greater than max age.'), $rowNum); + } + $filledBands[] = array('minAge' => $bandMin, 'maxAge' => $bandMax, 'rowNum' => $rowNum); + } + $bandCount = count($filledBands); + for ($outerIdx = 0; $outerIdx < $bandCount; $outerIdx++) { + for ($innerIdx = $outerIdx + 1; $innerIdx < $bandCount; $innerIdx++) { + $outerBand = $filledBands[$outerIdx]; + $innerBand = $filledBands[$innerIdx]; + $outerMaxEffective = ($outerBand['maxAge'] == 0) ? PHP_INT_MAX : $outerBand['maxAge']; + $innerMaxEffective = ($innerBand['maxAge'] == 0) ? PHP_INT_MAX : $innerBand['maxAge']; + if ($outerBand['minAge'] <= $innerMaxEffective && $outerMaxEffective >= $innerBand['minAge']) { + $this->errors[] = sprintf( + $this->l('Child age bands #%d and #%d overlap. Each band must cover a distinct age range.'), + $outerBand['rowNum'], + $innerBand['rowNum'] + ); + break 2; + } + } + } + } + + if ((int) Tools::getValue('htl_is_tiered')) { + $submittedTierMins = Tools::getValue('htl_tier_min', array()); + $submittedTierMaxs = Tools::getValue('htl_tier_max', array()); + $submittedTierValues = Tools::getValue('htl_tier_value', array()); + $filledTiers = array(); + foreach ($submittedTierValues as $rowIndex => $taxValue) { + if ($taxValue === '' || $taxValue === false) { + continue; + } + $rowNum = (int) $rowIndex + 1; + $tierMin = (isset($submittedTierMins[$rowIndex]) && $submittedTierMins[$rowIndex] !== '') + ? (float) $submittedTierMins[$rowIndex] : 0.0; + $tierMax = (isset($submittedTierMaxs[$rowIndex]) && $submittedTierMaxs[$rowIndex] !== '') + ? (float) $submittedTierMaxs[$rowIndex] : 0.0; + if (!is_numeric($taxValue)) { + $this->errors[] = sprintf($this->l('Tier #%d: tax value must be a valid number.'), $rowNum); + } elseif ((float) $taxValue < 0) { + $this->errors[] = sprintf($this->l('Tier #%d: tax value cannot be negative.'), $rowNum); + } + if ($tierMax > 0 && $tierMin > $tierMax) { + $this->errors[] = sprintf($this->l('Tier #%d: min price cannot be greater than max price.'), $rowNum); + } + $filledTiers[] = array('minPrice' => $tierMin, 'maxPrice' => $tierMax, 'rowNum' => $rowNum); + } + $tierCount = count($filledTiers); + for ($outerIdx = 0; $outerIdx < $tierCount; $outerIdx++) { + for ($innerIdx = $outerIdx + 1; $innerIdx < $tierCount; $innerIdx++) { + $outerTier = $filledTiers[$outerIdx]; + $innerTier = $filledTiers[$innerIdx]; + $outerMaxEffective = ($outerTier['maxPrice'] == 0) ? PHP_FLOAT_MAX : $outerTier['maxPrice']; + $innerMaxEffective = ($innerTier['maxPrice'] == 0) ? PHP_FLOAT_MAX : $innerTier['maxPrice']; + if ($outerTier['minPrice'] < $innerMaxEffective && $outerMaxEffective > $innerTier['minPrice']) { + $this->errors[] = sprintf( + $this->l('Price brackets #%d and #%d overlap. Each bracket must cover a distinct price range.'), + $outerTier['rowNum'], + $innerTier['rowNum'] + ); + break 2; + } + } + } + } + } + + public function setMedia() + { + parent::setMedia(); + if ($this->display == 'edit' || $this->display == 'add') { + $this->addCSS(_MODULE_DIR_ . 'hotelreservationsystem/views/css/TourismTaxAdmin.css'); + $this->addJs(_MODULE_DIR_ . 'hotelreservationsystem/views/js/TourismTaxForm.js'); + } + } + + /** + * @param int $idTax + */ + protected function saveTourismTaxSubtype($idTax) + { + $tourismTax = HotelTourismTax::getByTaxId((int) $idTax); + if (!$tourismTax) { + $tourismTax = new HotelTourismTax(); + $tourismTax->id = (int) $idTax; + } + $tourismTax->tax_type = (int) Tools::getValue('htl_tax_type', 0); + $tourismTax->is_per_night = (int) Tools::getValue('htl_is_per_night', 1); + $tourismTax->is_per_person = (int) Tools::getValue('htl_is_per_person', 0); + $tourismTax->tax_value = (float) Tools::getValue('htl_tax_value', 0); + $tourismTax->is_tiered = (int) Tools::getValue('htl_is_tiered', 0); + $tourismTax->has_child_rate = (int) Tools::getValue('htl_has_child_rate', 0); + $validFromRaw = trim(Tools::getValue('htl_valid_from', '')); + $validToRaw = trim(Tools::getValue('htl_valid_to', '')); + $tourismTax->valid_from = ($validFromRaw && $validFromRaw !== '0000-00-00') ? $validFromRaw : null; + $tourismTax->valid_to = ($validToRaw && $validToRaw !== '0000-00-00') ? $validToRaw : null; + + $submittedDays = Tools::getValue('htl_valid_days', array()); + if (!is_array($submittedDays)) { + $submittedDays = array(); + } + $validDayKeys = array('mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'); + $submittedDays = array_values(array_intersect($validDayKeys, $submittedDays)); + if (empty($submittedDays) || count($submittedDays) === 7) { + $tourismTax->is_special_days_exists = 0; + $tourismTax->special_days = null; + } else { + $tourismTax->is_special_days_exists = 1; + $tourismTax->special_days = json_encode($submittedDays); + } + + if (HotelTourismTax::existsForTax((int) $idTax)) { + $tourismTax->update(); + } else { + $tourismTax->add(); + } + + HotelTourismTaxTier::saveAll( + (int) $idTax, + Tools::getValue('htl_tier_min', array()), + Tools::getValue('htl_tier_max', array()), + Tools::getValue('htl_tier_value', array()) + ); + HotelTourismTaxChildRange::saveAll( + (int) $idTax, + Tools::getValue('htl_child_min', array()), + Tools::getValue('htl_child_max', array()), + Tools::getValue('htl_child_value', array()) + ); + } + + /** + * Remove this tax from all tourism TRGs when it is demoted from tourism to regular. + * Flashes a warning listing how many rule rows were deleted. + * + * @param int $idTax + */ + protected function cleanTaxFromTourismTrgs($idTax) + { + $count = HotelTourismTax::cleanFromTourismTrgs((int) $idTax); + if ($count) { + $this->warnings[] = sprintf( + $this->l('This tax was removed from %d tourism tax rules group(s) because it is no longer marked as a tourism tax.'), + $count + ); + } + } + + /** + * Remove this tax from all standard (non-tourism) TRGs when it is promoted to a tourism tax. + * Flashes a warning listing how many rule rows were deleted. + * + * @param int $idTax + */ + protected function cleanTaxFromVatTrgs($idTax) + { + $count = HotelTourismTax::cleanFromVatTrgs((int) $idTax); + if ($count) { + $this->warnings[] = sprintf( + $this->l('This tax was removed from %d standard tax rules group(s) because it is now marked as a tourism tax.'), + $count + ); + } + } + public function updateOptionPsUseEcotax($value) { $old_value = (int)Configuration::get('PS_USE_ECOTAX'); diff --git a/controllers/front/OrderConfirmationController.php b/controllers/front/OrderConfirmationController.php index f00addd43..30fd8fd62 100644 --- a/controllers/front/OrderConfirmationController.php +++ b/controllers/front/OrderConfirmationController.php @@ -135,6 +135,8 @@ class OrderConfirmationControllerCore extends FrontController $orderTotalInfo['total_paid_real'] = 0; $orderTotalInfo['total_wrapping'] = 0; $orderTotalInfo['total_order_amount'] = 0; + $orderTotalInfo['tourism_tax_online'] = 0.0; + $orderTotalInfo['tourism_tax_at_hotel'] = 0.0; $orders_has_invoice = 1; if ($cartOrders = Order::getAllOrdersByCartId($order->id_cart)) { @@ -432,6 +434,17 @@ class OrderConfirmationControllerCore extends FrontController $orderTotalInfo['total_tax'] += $objCartOrder->total_paid_tax_incl - $objCartOrder->total_paid_tax_excl; $orderTotalInfo['total_paid'] += $objCartOrder->total_paid; $orderTotalInfo['total_paid_real'] += $objCartOrder->total_paid_real; + + if (Configuration::get('QLO_USE_TOURISM_TAX')) { + $ttBreakdown = HotelOrderTourismTax::getBreakdownForInvoice($idOrder); + foreach ($ttBreakdown as $ttRow) { + if ((int) $ttRow['collection_type'] === 0) { + $orderTotalInfo['tourism_tax_online'] += (float) $ttRow['total_amount']; + } else { + $orderTotalInfo['tourism_tax_at_hotel'] += (float) $ttRow['total_amount']; + } + } + } } $totalTaxIncl = $orderTotalInfo['total_rooms_ti'] + $orderTotalInfo['total_services_ti'] + $orderTotalInfo['total_convenience_fee_ti'] + $orderTotalInfo['total_auto_add_services_ti'] + $orderTotalInfo['total_demands_price_ti'] + $orderTotalInfo['total_standalone_products_ti']; diff --git a/controllers/front/OrderDetailController.php b/controllers/front/OrderDetailController.php index 25111fbad..cf8ca6624 100644 --- a/controllers/front/OrderDetailController.php +++ b/controllers/front/OrderDetailController.php @@ -429,6 +429,7 @@ class OrderDetailControllerCore extends FrontController 'THEME_DIR' => _THEME_DIR_, 'total_convenience_fee_ti' => $total_convenience_fee_ti, 'total_convenience_fee_te' => $total_convenience_fee_te, + 'tourism_tax_breakdown' => Configuration::get('QLO_USE_TOURISM_TAX') ? HotelOrderTourismTax::getBreakdownForInvoice((int) $id_order) : array(), 'total_demands_price_ti' => $total_demands_price_ti, 'total_demands_price_te' => $total_demands_price_te, 'any_back_order' => $anyBackOrder, diff --git a/install/data/db_structure.sql b/install/data/db_structure.sql index 9520ec459..8c8869938 100644 --- a/install/data/db_structure.sql +++ b/install/data/db_structure.sql @@ -1277,6 +1277,7 @@ CREATE TABLE `PREFIX_order_detail` ( `tax_rate` DECIMAL(10,3) NOT NULL DEFAULT '0.000', `ecotax` decimal(21,6) NOT NULL DEFAULT '0.00', `ecotax_tax_rate` DECIMAL(5,3) NOT NULL DEFAULT '0.000', + `tourism_tax_amount` DECIMAL(21,6) NOT NULL DEFAULT '0.000000', `discount_quantity_applied` TINYINT(1) NOT NULL DEFAULT '0', `download_hash` varchar(255) DEFAULT NULL, `download_nb` int(10) unsigned DEFAULT '0', @@ -1527,6 +1528,7 @@ CREATE TABLE `PREFIX_product` ( `id_category_default` int(10) unsigned DEFAULT NULL, `id_shop_default` int(10) unsigned NOT NULL DEFAULT 1, `id_tax_rules_group` INT(11) UNSIGNED NOT NULL, + `id_tourism_tax_rules_group` INT(11) UNSIGNED NOT NULL DEFAULT '0', `on_sale` tinyint(1) unsigned NOT NULL DEFAULT '0', `online_only` tinyint(1) unsigned NOT NULL DEFAULT '0', `ean13` varchar(13) DEFAULT NULL, @@ -1590,6 +1592,7 @@ CREATE TABLE IF NOT EXISTS `PREFIX_product_shop` ( `id_shop` int(10) unsigned NOT NULL, `id_category_default` int(10) unsigned DEFAULT NULL, `id_tax_rules_group` INT(11) UNSIGNED NOT NULL, + `id_tourism_tax_rules_group` INT(11) UNSIGNED NOT NULL DEFAULT '0', `on_sale` tinyint(1) unsigned NOT NULL DEFAULT '0', `online_only` tinyint(1) unsigned NOT NULL DEFAULT '0', `ecotax` decimal(17,6) NOT NULL DEFAULT '0.000000', @@ -1988,6 +1991,7 @@ CREATE TABLE `PREFIX_tag_count` ( CREATE TABLE `PREFIX_tax` ( `id_tax` int(10) unsigned NOT NULL auto_increment, `rate` DECIMAL(10, 3) NOT NULL, + `is_tourism_tax` TINYINT(1) NOT NULL DEFAULT '0', `active` tinyint(1) unsigned NOT NULL DEFAULT '1', `deleted` tinyint(1) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id_tax`) @@ -2113,6 +2117,7 @@ CREATE TABLE `PREFIX_tax_rules_group` ( `id_tax_rules_group` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, `name` VARCHAR( 50 ) NOT NULL, `active` INT NOT NULL, +`is_tourism_tax_rule` TINYINT(1) NOT NULL DEFAULT '0', `deleted` TINYINT(1) UNSIGNED NOT NULL, `date_add` DATETIME NOT NULL, `date_upd` DATETIME NOT NULL diff --git a/modules/hotelreservationsystem/classes/HotelBookingDetail.php b/modules/hotelreservationsystem/classes/HotelBookingDetail.php index 68086cde4..f4529ec1c 100644 --- a/modules/hotelreservationsystem/classes/HotelBookingDetail.php +++ b/modules/hotelreservationsystem/classes/HotelBookingDetail.php @@ -2517,6 +2517,35 @@ class HotelBookingDetail extends ObjectModel $data['is_cancelled'] = (int) $is_cancelled; } + $tourismBookingIds = array(); + if ((int) $is_refunded && Configuration::get('QLO_USE_TOURISM_TAX')) { + if ($id_rooms) { + foreach ($id_rooms as $val_rm) { + $bid = (int) Db::getInstance()->getValue( + 'SELECT `id` FROM `'._DB_PREFIX_.'htl_booking_detail` + WHERE `id_order` = '.(int) $id_order.' + AND `id_room` = '.(int) $val_rm['id_room'].' + AND `date_from` = \''.pSQL($date_from).'\' + AND `date_to` = \''.pSQL($date_to).'\' + AND `is_refunded` = 0' + ); + if ($bid) { + $tourismBookingIds[] = $bid; + } + } + } else { + $rows = Db::getInstance()->executeS( + 'SELECT `id` FROM `'._DB_PREFIX_.'htl_booking_detail` + WHERE `id_order` = '.(int) $id_order.' AND `is_refunded` = 0' + ); + if ($rows) { + foreach ($rows as $row) { + $tourismBookingIds[] = (int) $row['id']; + } + } + } + } + if ($id_rooms) { foreach ($id_rooms as $key_rm => $val_rm) { $where = 'id_order='.(int)$id_order.' AND id_room = '.(int)$val_rm['id_room'].' AND `date_from`= \''. @@ -2527,6 +2556,12 @@ class HotelBookingDetail extends ObjectModel $result = Db::getInstance()->update($table, $data, 'id_order='.(int)$id_order); } + if ($result && $tourismBookingIds) { + foreach ($tourismBookingIds as $bid) { + HotelOrderTourismTax::setRefunded($bid); + } + } + // if automatic overbooking resolution is enabled if ($result && Configuration::get('PS_OVERBOOKING_AUTO_RESOLVE') && $is_refunded) { // if room is getting free and this room is not already in back order then resolve the overbookings for this free room diff --git a/modules/hotelreservationsystem/classes/HotelBookingTaxExemption.php b/modules/hotelreservationsystem/classes/HotelBookingTaxExemption.php new file mode 100644 index 000000000..1db4a235b --- /dev/null +++ b/modules/hotelreservationsystem/classes/HotelBookingTaxExemption.php @@ -0,0 +1,68 @@ + 'htl_booking_tax_exemption', + 'primary' => 'id_exemption', + 'fields' => array( + 'id_order_tourism_tax' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true), + 'id_htl_booking' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true), + 'id_order' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true), + 'id_employee' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true), + 'date_add' => array('type' => self::TYPE_DATE, 'validate' => 'isDate'), + ), + ); + + /** + * Return all exemption records for a given htl_order_tourism_tax row. + * + * @param int $idOrderTourismTax + * @return array + */ + public static function getByOrderTourismTax($idOrderTourismTax) + { + return Db::getInstance()->executeS( + 'SELECT e.* FROM `' . _DB_PREFIX_ . 'htl_booking_tax_exemption` e + WHERE e.`id_order_tourism_tax` = ' . (int) $idOrderTourismTax . ' + ORDER BY e.`date_add` ASC' + ); + } + +} diff --git a/modules/hotelreservationsystem/classes/HotelBranchInformation.php b/modules/hotelreservationsystem/classes/HotelBranchInformation.php index 0797c7035..4acff2916 100644 --- a/modules/hotelreservationsystem/classes/HotelBranchInformation.php +++ b/modules/hotelreservationsystem/classes/HotelBranchInformation.php @@ -39,6 +39,7 @@ class HotelBranchInformation extends ObjectModel public $map_input_text; public $active_refund; public $fax; + public $tourism_tax_collection_type = 0; public $date_add; public $date_upd; @@ -61,6 +62,7 @@ class HotelBranchInformation extends ObjectModel 'map_input_text' => array('type' => self::TYPE_STRING, 'validate' => 'isString'), 'active_refund' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), 'fax' => array('type' => self::TYPE_STRING, 'validate' => 'isGenericName'), + 'tourism_tax_collection_type' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'), 'date_add' => array('type' => self::TYPE_DATE, 'validate' => 'isDate', 'copy_post' => false), 'date_upd' => array('type' => self::TYPE_DATE, 'validate' => 'isDate', 'copy_post' => false), diff --git a/modules/hotelreservationsystem/classes/HotelHelper.php b/modules/hotelreservationsystem/classes/HotelHelper.php index f7f4f1c2b..a5ea104db 100644 --- a/modules/hotelreservationsystem/classes/HotelHelper.php +++ b/modules/hotelreservationsystem/classes/HotelHelper.php @@ -1069,6 +1069,8 @@ class HotelHelper Configuration::updateValue('PS_MIN_BOOKING_OFFSET', 0); Configuration::updateValue('HTL_FEATURE_PRICING_PRIORITY', 'specific_date;special_day;date_range'); + Configuration::updateValue('QLO_USE_TOURISM_TAX', 0); + Configuration::updateValue('QLO_TOURISM_TAX_GROSSED_UP', 0); Configuration::updateValue('WK_GOOGLE_ACTIVE_MAP', 0); Configuration::updateValue('WK_MAP_HOTEL_ACTIVE_ONLY', 1); diff --git a/modules/hotelreservationsystem/classes/HotelOrderTourismTax.php b/modules/hotelreservationsystem/classes/HotelOrderTourismTax.php new file mode 100644 index 000000000..4d7b15ed0 --- /dev/null +++ b/modules/hotelreservationsystem/classes/HotelOrderTourismTax.php @@ -0,0 +1,565 @@ + 'htl_order_tourism_tax', + 'primary' => 'id_order_tourism_tax', + 'fields' => array( + 'id_order' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true), + 'id_order_detail' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true), + 'id_htl_booking' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true), + 'id_hotel' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true), + 'id_tax' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true), + 'id_currency' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true), + 'tax_name' => array('type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'size' => 100, 'required' => true), + 'num_nights' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'), + 'num_adults' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'), + 'total_amount' => array('type' => self::TYPE_FLOAT, 'validate' => 'isUnsignedFloat'), + 'collection_type' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'), + 'is_exempted' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), + 'is_refunded' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), + 'date_add' => array('type' => self::TYPE_DATE, 'validate' => 'isDate'), + 'date_upd' => array('type' => self::TYPE_DATE, 'validate' => 'isDate'), + ), + ); + + /** + * @param int $idOrderDetail + * @return array + */ + public static function getByOrderDetail($idOrderDetail) + { + return Db::getInstance()->executeS( + 'SELECT * FROM `' . _DB_PREFIX_ . 'htl_order_tourism_tax` + WHERE `id_order_detail` = ' . (int) $idOrderDetail + ); + } + + /** + * @param int $idOrder + * @return array + */ + public static function getByOrder($idOrder) + { + return Db::getInstance()->executeS( + 'SELECT * FROM `' . _DB_PREFIX_ . 'htl_order_tourism_tax` + WHERE `id_order` = ' . (int) $idOrder + ); + } + + /** + * Return online-collected, non-exempted, non-refunded tourism tax totals for all bookings + * of an order, keyed by id_htl_booking. One query per order — avoids N+1 in the booking loop. + * + * @param int $idOrder + * @return array [id_htl_booking => float total_amount] + */ + public static function getOnlineTotalsByBooking($idOrder) + { + $rows = Db::getInstance()->executeS( + 'SELECT `id_htl_booking`, SUM(`total_amount`) AS total_amount + FROM `' . _DB_PREFIX_ . 'htl_order_tourism_tax` + WHERE `id_order` = ' . (int) $idOrder . ' + AND `collection_type` = 0 + AND `is_exempted` = 0 + AND `is_refunded` = 0 + GROUP BY `id_htl_booking`' + ); + $result = array(); + if ($rows) { + foreach ($rows as $row) { + $result[(int) $row['id_htl_booking']] = (float) $row['total_amount']; + } + } + return $result; + } + + /** + * Sum of total_amount for online-collected (collection_type=0), non-exempted rows for an order. + * + * @param int $idOrder + * @return float + */ + public static function getOnlineTotalForOrder($idOrder) + { + $total = Db::getInstance()->getValue( + 'SELECT SUM(`total_amount`) FROM `' . _DB_PREFIX_ . 'htl_order_tourism_tax` + WHERE `id_order` = ' . (int) $idOrder . ' + AND `collection_type` = 0 + AND `is_exempted` = 0 + AND `is_refunded` = 0' + ); + return (float) $total; + } + + /** + * Mark all rows for a booking as refunded (used on cancellation). + * + * @param int $idHtlBooking + * @return bool + */ + public static function setRefunded($idHtlBooking) + { + return Db::getInstance()->execute( + 'UPDATE `' . _DB_PREFIX_ . 'htl_order_tourism_tax` + SET `is_refunded` = 1, `date_upd` = NOW() + WHERE `id_htl_booking` = ' . (int) $idHtlBooking + ); + } + + /** + * Delete all snapshot rows for an order_detail. + * + * @param int $idOrderDetail + * @return bool + */ + public static function deleteByOrderDetail($idOrderDetail) + { + return Db::getInstance()->execute( + 'DELETE FROM `' . _DB_PREFIX_ . 'htl_order_tourism_tax` + WHERE `id_order_detail` = ' . (int) $idOrderDetail + ); + } + + /** + * Delete all snapshot rows for a single booking. + * Used by computeAndSave idempotency step — scoped to id_htl_booking so that + * multiple bookings sharing one order_detail do not erase each other. + * + * @param int $idHtlBooking + * @return bool + */ + public static function deleteByHtlBooking($idHtlBooking) + { + return Db::getInstance()->execute( + 'DELETE FROM `' . _DB_PREFIX_ . 'htl_order_tourism_tax` + WHERE `id_htl_booking` = ' . (int) $idHtlBooking + ); + } + + /** + * Return breakdown for invoice/PDF — grouped by id_tax, excluding exempted rows. + * + * @param int $idOrder + * @return array + */ + public static function getBreakdownForInvoice($idOrder) + { + return Db::getInstance()->executeS( + 'SELECT `id_tax`, `tax_name`, `collection_type`, + SUM(`num_nights`) AS num_nights, + SUM(`num_adults`) AS num_adults, + SUM(`total_amount`) AS total_amount + FROM `' . _DB_PREFIX_ . 'htl_order_tourism_tax` + WHERE `id_order` = ' . (int) $idOrder . ' + AND `is_exempted` = 0 + GROUP BY `id_tax`, `tax_name`, `collection_type`' + ); + } + + /** + * Return tourism tax status for a single booking. + * STATUS_NONE — no rows exist for this booking yet + * STATUS_APPLIED — rows exist and at least one is not exempted + * STATUS_EXEMPTED — rows exist but all are exempted + * + * @param int $idHtlBooking + * @return string + */ + public static function getStatusForBooking($idHtlBooking) + { + $rows = Db::getInstance()->executeS( + 'SELECT `is_exempted` FROM `' . _DB_PREFIX_ . 'htl_order_tourism_tax` + WHERE `id_htl_booking` = ' . (int) $idHtlBooking . ' + AND `is_refunded` = 0' + ); + if (empty($rows)) { + return self::STATUS_NONE; + } + foreach ($rows as $row) { + if (!(int) $row['is_exempted']) { + return self::STATUS_APPLIED; + } + } + return self::STATUS_EXEMPTED; + } + + /** + * Sum of total_amount for all non-exempted tourism tax rows for a booking. + * Returns 0 when the booking has no rows or all rows are exempted. + * + * @param int $idHtlBooking + * @return float + */ + public static function getTotalForBooking($idHtlBooking) + { + return (float) Db::getInstance()->getValue( + 'SELECT SUM(`total_amount`) FROM `' . _DB_PREFIX_ . 'htl_order_tourism_tax` + WHERE `id_htl_booking` = ' . (int) $idHtlBooking . ' + AND `is_exempted` = 0 + AND `is_refunded` = 0' + ); + } + + /** + * Mark all non-refunded rows for a booking as exempted. + * Runs inside a DB transaction with SELECT FOR UPDATE to prevent concurrent double-exemption. + * Writes one HotelBookingTaxExemption audit row per htl_order_tourism_tax row exempted. + * + * @param int $idHtlBooking + * @param int $idEmployee PS employee id (0 = resolved from Context) + * @return bool + */ + public static function exemptForBooking($idHtlBooking, $idEmployee = 0) + { + $db = Db::getInstance(); + $db->execute('START TRANSACTION'); + try { + $rows = $db->executeS( + 'SELECT `id_order_tourism_tax`, `id_order`, `id_order_detail`, `collection_type`, `total_amount` + FROM `' . _DB_PREFIX_ . 'htl_order_tourism_tax` + WHERE `id_htl_booking` = ' . (int) $idHtlBooking . ' + AND `is_exempted` = 0 + AND `is_refunded` = 0 + FOR UPDATE' + ); + + if (empty($rows)) { + $db->execute('COMMIT'); + return true; + } + + $db->execute( + 'UPDATE `' . _DB_PREFIX_ . 'htl_order_tourism_tax` + SET `is_exempted` = 1, `date_upd` = NOW() + WHERE `id_htl_booking` = ' . (int) $idHtlBooking . ' + AND `is_refunded` = 0' + ); + + $idOrderDetail = (int) $rows[0]['id_order_detail']; + $idOrder = (int) $rows[0]['id_order']; + $onlineAmount = 0.0; + $totalExemptedAmount = 0.0; + $idEmployeeResolved = $idEmployee ? (int) $idEmployee : (int) Context::getContext()->employee->id; + + foreach ($rows as $row) { + $totalExemptedAmount += (float) $row['total_amount']; + if ((int) $row['collection_type'] === 0) { + $onlineAmount += (float) $row['total_amount']; + } + $exemption = new HotelBookingTaxExemption(); + $exemption->id_order_tourism_tax = (int) $row['id_order_tourism_tax']; + $exemption->id_htl_booking = (int) $idHtlBooking; + $exemption->id_order = $idOrder; + $exemption->id_employee = $idEmployeeResolved; + if (!$exemption->add()) { + throw new Exception('Failed to save exemption audit row'); + } + } + + $db->execute( + 'UPDATE `' . _DB_PREFIX_ . 'order_detail` + SET `tourism_tax_amount` = GREATEST(0, `tourism_tax_amount` - ' . (float) $totalExemptedAmount . '), + `total_price_tax_incl` = `total_price_tax_incl` - ' . (float) $onlineAmount . ' + WHERE `id_order_detail` = ' . $idOrderDetail + ); + + if ($onlineAmount > 0) { + $db->execute( + 'UPDATE `' . _DB_PREFIX_ . 'orders` + SET `total_paid_tax_incl` = `total_paid_tax_incl` - ' . (float) $onlineAmount . ', + `total_paid_tax_excl` = `total_paid_tax_excl` - ' . (float) $onlineAmount . ', + `total_paid` = `total_paid` - ' . (float) $onlineAmount . ' + WHERE `id_order` = ' . $idOrder + ); + } + + $db->execute('COMMIT'); + return true; + } catch (Exception $e) { + $db->execute('ROLLBACK'); + return false; + } + } + + /** + * Remove exemption for all rows of a booking (restore applied state). + * Runs inside a DB transaction with SELECT FOR UPDATE to prevent concurrent double-restore. + * + * @param int $idHtlBooking + * @return bool + */ + public static function unExemptForBooking($idHtlBooking) + { + $db = Db::getInstance(); + $db->execute('START TRANSACTION'); + try { + $rows = $db->executeS( + 'SELECT `id_order`, `id_order_detail`, `collection_type`, `total_amount` + FROM `' . _DB_PREFIX_ . 'htl_order_tourism_tax` + WHERE `id_htl_booking` = ' . (int) $idHtlBooking . ' + AND `is_exempted` = 1 + AND `is_refunded` = 0 + FOR UPDATE' + ); + + if (empty($rows)) { + $db->execute('COMMIT'); + return true; + } + + $db->execute( + 'UPDATE `' . _DB_PREFIX_ . 'htl_order_tourism_tax` + SET `is_exempted` = 0, `date_upd` = NOW() + WHERE `id_htl_booking` = ' . (int) $idHtlBooking . ' + AND `is_refunded` = 0' + ); + + $idOrderDetail = (int) $rows[0]['id_order_detail']; + $idOrder = (int) $rows[0]['id_order']; + $onlineAmount = 0.0; + + foreach ($rows as $row) { + if ((int) $row['collection_type'] === 0) { + $onlineAmount += (float) $row['total_amount']; + } + } + + $restored = (float) $db->getValue( + 'SELECT SUM(`total_amount`) + FROM `' . _DB_PREFIX_ . 'htl_order_tourism_tax` + WHERE `id_order_detail` = ' . $idOrderDetail . ' + AND `is_refunded` = 0' + ); + + $db->execute( + 'UPDATE `' . _DB_PREFIX_ . 'order_detail` + SET `tourism_tax_amount` = ' . $restored . ', + `total_price_tax_incl` = `total_price_tax_incl` + ' . (float) $onlineAmount . ' + WHERE `id_order_detail` = ' . $idOrderDetail + ); + + if ($onlineAmount > 0) { + $db->execute( + 'UPDATE `' . _DB_PREFIX_ . 'orders` + SET `total_paid_tax_incl` = `total_paid_tax_incl` + ' . (float) $onlineAmount . ', + `total_paid_tax_excl` = `total_paid_tax_excl` + ' . (float) $onlineAmount . ', + `total_paid` = `total_paid` + ' . (float) $onlineAmount . ' + WHERE `id_order` = ' . $idOrder + ); + } + + $db->execute('COMMIT'); + return true; + } catch (Exception $e) { + $db->execute('ROLLBACK'); + return false; + } + } + + /** + * Apply or restore tourism tax for a single booking based on its current status. + * STATUS_EXEMPTED → removes the exemption. + * STATUS_NONE → computes fresh rows, then adjusts order totals. + * STATUS_APPLIED → no-op (returns APPLY_OK). + * + * @param int $idHtlBooking + * @return int APPLY_OK | APPLY_ERROR_RESTORE | APPLY_ERROR_NO_RULE + */ + public static function applyForBooking($idHtlBooking) + { + $idHtlBooking = (int) $idHtlBooking; + $status = self::getStatusForBooking($idHtlBooking); + + if ($status === self::STATUS_EXEMPTED) { + return self::unExemptForBooking($idHtlBooking) ? self::APPLY_OK : self::APPLY_ERROR_RESTORE; + } + + if ($status === self::STATUS_NONE) { + // If refunded rows exist, the booking was cancelled — do not create new snapshot rows. + $hasRefundedRows = (bool) Db::getInstance()->getValue( + 'SELECT COUNT(*) FROM `' . _DB_PREFIX_ . 'htl_order_tourism_tax` + WHERE `id_htl_booking` = ' . $idHtlBooking . ' AND `is_refunded` = 1' + ); + if ($hasRefundedRows) { + return self::APPLY_ERROR_REFUNDED; + } + + if (!HotelTourismTaxCalculator::applyFromExistingBooking($idHtlBooking)) { + return self::APPLY_ERROR_NO_RULE; + } + self::adjustOrderTotalsForNewBooking($idHtlBooking); + } + + return self::APPLY_OK; + } + + /** + * Apply tourism tax for all bookings of an order. + * Returns array of ['booking_id' => int, 'error' => int] for each failure. + * + * @param int $idOrder + * @return array + */ + public static function applyForOrder($idOrder) + { + $failures = array(); + $objBookingDetail = new HotelBookingDetail(); + $bookings = $objBookingDetail->getBookingDataByOrderId((int) $idOrder); + if ($bookings) { + foreach ($bookings as $booking) { + $result = self::applyForBooking($booking['id']); + if ($result !== self::APPLY_OK) { + $failures[] = array('booking_id' => (int) $booking['id'], 'error' => $result); + } + } + } + return $failures; + } + + /** + * Exempt tourism tax for a booking only if it is currently STATUS_APPLIED. + * Returns EXEMPT_OK on success or when already exempted, EXEMPT_ERROR_NO_RULE when tourism tax + * was never applied to this booking (room type has no tourism TRG), EXEMPT_ERROR_SAVE on DB failure. + * + * @param int $idHtlBooking + * @param int $idEmployee + * @return int EXEMPT_OK | EXEMPT_ERROR_NO_RULE | EXEMPT_ERROR_SAVE + */ + public static function exemptIfApplied($idHtlBooking, $idEmployee = 0) + { + $idHtlBooking = (int) $idHtlBooking; + $status = self::getStatusForBooking($idHtlBooking); + if ($status === self::STATUS_NONE) { + $hasRefundedRows = (bool) Db::getInstance()->getValue( + 'SELECT COUNT(*) FROM `' . _DB_PREFIX_ . 'htl_order_tourism_tax` + WHERE `id_htl_booking` = ' . $idHtlBooking . ' AND `is_refunded` = 1' + ); + return $hasRefundedRows ? self::EXEMPT_ERROR_REFUNDED : self::EXEMPT_ERROR_NO_RULE; + } + if ($status === self::STATUS_APPLIED) { + return self::exemptForBooking($idHtlBooking, $idEmployee) + ? self::EXEMPT_OK + : self::EXEMPT_ERROR_SAVE; + } + return self::EXEMPT_OK; + } + + /** + * Exempt tourism tax for all applicable bookings of an order. + * Returns array of ['booking_id' => int, 'error' => int] for each failure. + * + * @param int $idOrder + * @param int $idEmployee + * @return array + */ + public static function exemptForOrder($idOrder, $idEmployee = 0) + { + $failures = array(); + $objBookingDetail = new HotelBookingDetail(); + $bookings = $objBookingDetail->getBookingDataByOrderId((int) $idOrder); + if ($bookings) { + foreach ($bookings as $booking) { + $result = self::exemptIfApplied($booking['id'], $idEmployee); + if ($result !== self::EXEMPT_OK) { + $failures[] = array('booking_id' => (int) $booking['id'], 'error' => $result); + } + } + } + return $failures; + } + + /** + * After fresh-applying tourism tax to a STATUS_NONE booking, add the online + * portion to the order totals (it was absent from the original order total). + * + * @param int $idHtlBooking + * @return void + */ + private static function adjustOrderTotalsForNewBooking($idHtlBooking) + { + $booking = new HotelBookingDetail((int) $idHtlBooking); + if (!Validate::isLoadedObject($booking)) { + return; + } + $onlineTotal = (float) Db::getInstance()->getValue( + 'SELECT SUM(`total_amount`) + FROM `' . _DB_PREFIX_ . 'htl_order_tourism_tax` + WHERE `id_htl_booking` = ' . (int) $idHtlBooking . ' + AND `collection_type` = 0 + AND `is_exempted` = 0 + AND `is_refunded` = 0' + ); + if ($onlineTotal > 0) { + Db::getInstance()->execute( + 'UPDATE `' . _DB_PREFIX_ . 'orders` + SET `total_paid_tax_incl` = `total_paid_tax_incl` + ' . $onlineTotal . ', + `total_paid_tax_excl` = `total_paid_tax_excl` + ' . $onlineTotal . ', + `total_paid` = `total_paid` + ' . $onlineTotal . ' + WHERE `id_order` = ' . (int) $booking->id_order + ); + } + } +} diff --git a/modules/hotelreservationsystem/classes/HotelReservationSystemDb.php b/modules/hotelreservationsystem/classes/HotelReservationSystemDb.php index ca44a2826..57023c05d 100644 --- a/modules/hotelreservationsystem/classes/HotelReservationSystemDb.php +++ b/modules/hotelreservationsystem/classes/HotelReservationSystemDb.php @@ -69,6 +69,7 @@ class HotelReservationSystemDb `map_input_text` text NOT NULL, `active_refund` tinyint(1) unsigned NOT NULL DEFAULT '0', `fax` varchar(255) DEFAULT NULL, + `tourism_tax_collection_type` tinyint(1) unsigned NOT NULL DEFAULT '0', `date_add` datetime NOT NULL, `date_upd` datetime NOT NULL, PRIMARY KEY (`id`) @@ -513,6 +514,81 @@ class HotelReservationSystemDb PRIMARY KEY (`id_room_type_bed_type`) ) ENGINE="._MYSQL_ENGINE_." DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;", + "CREATE TABLE IF NOT EXISTS `"._DB_PREFIX_."htl_tourism_tax` ( + `id_tax` INT(11) UNSIGNED NOT NULL, + `tax_type` TINYINT(1) NOT NULL DEFAULT '0', + `is_per_night` TINYINT(1) NOT NULL DEFAULT '1', + `is_per_person` TINYINT(1) NOT NULL DEFAULT '0', + `tax_value` DECIMAL(20,6) NOT NULL DEFAULT '0.000000', + `is_tiered` TINYINT(1) NOT NULL DEFAULT '0', + `has_child_rate` TINYINT(1) NOT NULL DEFAULT '0', + `child_tax_value` DECIMAL(20,6) NOT NULL DEFAULT '0.000000', + `valid_from` DATE NULL DEFAULT NULL, + `valid_to` DATE NULL DEFAULT NULL, + `is_special_days_exists` TINYINT(1) NOT NULL DEFAULT '0', + `special_days` TEXT NULL DEFAULT NULL, + PRIMARY KEY (`id_tax`) + ) ENGINE="._MYSQL_ENGINE_." DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;", + + "CREATE TABLE IF NOT EXISTS `"._DB_PREFIX_."htl_tourism_tax_tier` ( + `id_tier` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `id_tax` INT(11) UNSIGNED NOT NULL, + `min_amount` DECIMAL(20,6) NOT NULL DEFAULT '0.000000', + `max_amount` DECIMAL(20,6) NOT NULL DEFAULT '0.000000', + `tax_value` DECIMAL(20,6) NOT NULL DEFAULT '0.000000', + `position` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0', + PRIMARY KEY (`id_tier`), + KEY `id_tax` (`id_tax`) + ) ENGINE="._MYSQL_ENGINE_." DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;", + + "CREATE TABLE IF NOT EXISTS `"._DB_PREFIX_."htl_tourism_tax_child_range` ( + `id_child_range` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `id_tax` INT(11) UNSIGNED NOT NULL, + `min_age` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0', + `max_age` TINYINT(3) UNSIGNED NOT NULL DEFAULT '17', + `tax_value` DECIMAL(20,6) NOT NULL DEFAULT '0.000000', + `position` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0', + PRIMARY KEY (`id_child_range`), + KEY `id_tax` (`id_tax`) + ) ENGINE="._MYSQL_ENGINE_." DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;", + + "CREATE TABLE IF NOT EXISTS `"._DB_PREFIX_."htl_order_tourism_tax` ( + `id_order_tourism_tax` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `id_order` INT(11) UNSIGNED NOT NULL, + `id_order_detail` INT(11) UNSIGNED NOT NULL, + `id_htl_booking` INT(11) UNSIGNED NOT NULL, + `id_hotel` INT(11) UNSIGNED NOT NULL, + `id_tax` INT(11) UNSIGNED NOT NULL, + `id_currency` INT(11) UNSIGNED NOT NULL, + `tax_name` VARCHAR(100) NOT NULL, + `num_nights` SMALLINT(5) UNSIGNED NOT NULL DEFAULT '1', + `num_adults` TINYINT(3) UNSIGNED NOT NULL DEFAULT '1', + `total_amount` DECIMAL(20,6) NOT NULL DEFAULT '0.000000', + `collection_type` TINYINT(1) NOT NULL DEFAULT '0', + `is_exempted` TINYINT(1) NOT NULL DEFAULT '0', + `is_refunded` TINYINT(1) NOT NULL DEFAULT '0', + `date_add` DATETIME NOT NULL, + `date_upd` DATETIME NOT NULL, + PRIMARY KEY (`id_order_tourism_tax`), + UNIQUE KEY `biz_key` (`id_order_detail`, `id_tax`), + KEY `id_order` (`id_order`), + KEY `id_htl_booking` (`id_htl_booking`), + KEY `id_hotel` (`id_hotel`) + ) ENGINE="._MYSQL_ENGINE_." DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;", + + "CREATE TABLE IF NOT EXISTS `"._DB_PREFIX_."htl_booking_tax_exemption` ( + `id_exemption` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `id_order_tourism_tax` INT(11) UNSIGNED NOT NULL, + `id_htl_booking` INT(11) UNSIGNED NOT NULL, + `id_order` INT(11) UNSIGNED NOT NULL, + `id_employee` INT(11) UNSIGNED NOT NULL, + `date_add` DATETIME NOT NULL, + PRIMARY KEY (`id_exemption`), + KEY `id_order_tourism_tax` (`id_order_tourism_tax`), + KEY `id_htl_booking` (`id_htl_booking`), + KEY `id_order` (`id_order`) + ) ENGINE="._MYSQL_ENGINE_." DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;", + "INSERT INTO `"._DB_PREFIX_."htl_settings_link` (`id_settings_link`, `icon`, `link`, `new_window`, `position`, `unremovable`, `active`, `date_add`, `date_upd`) VALUES (1, 'icon-cogs', 'index.php?controller=AdminHotelGeneralSettings', 0, 0, 1, 1, NOW(), NOW()), (2, 'icon-dollar', 'index.php?controller=AdminHotelFeaturePricesSettings', 0, 2, 1, 1, NOW(), NOW()), @@ -615,7 +691,12 @@ 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_tourism_tax`, + `'._DB_PREFIX_.'htl_tourism_tax_tier`, + `'._DB_PREFIX_.'htl_tourism_tax_child_range`, + `'._DB_PREFIX_.'htl_order_tourism_tax`, + `'._DB_PREFIX_.'htl_booking_tax_exemption`' ); } } diff --git a/modules/hotelreservationsystem/classes/HotelRoomTypeFeaturePricing.php b/modules/hotelreservationsystem/classes/HotelRoomTypeFeaturePricing.php index dd4f7da37..6ae745eda 100644 --- a/modules/hotelreservationsystem/classes/HotelRoomTypeFeaturePricing.php +++ b/modules/hotelreservationsystem/classes/HotelRoomTypeFeaturePricing.php @@ -433,6 +433,53 @@ class HotelRoomTypeFeaturePricing extends ObjectModel $totalPrice['total_price_tax_incl'] = Tools::processPriceRounding($totalPrice['total_price_tax_incl'], $quantity); $totalPrice['total_price_tax_excl'] = Tools::processPriceRounding($totalPrice['total_price_tax_excl'], $quantity); + $totalPrice['tourism_tax_online'] = 0.0; + $totalPrice['tourism_tax_at_hotel'] = 0.0; + if (Configuration::get('QLO_USE_TOURISM_TAX') + && is_array($occupancy) && !empty($occupancy) && isset($occupancy[0]['adults']) + ) { + $idTourismTrg = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + 'SELECT `id_tourism_tax_rules_group` + FROM `' . _DB_PREFIX_ . 'product_shop` product_shop + WHERE `id_product` = ' . (int) $id_product + . Shop::addSqlRestriction(false) + ); + if ($idTourismTrg) { + $idHotel = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + 'SELECT `id_hotel` FROM `' . _DB_PREFIX_ . 'htl_room_type` + WHERE `id_product` = ' . (int) $id_product + ); + $branchObj = new HotelBranchInformation($idHotel); + $collectionType = (int) $branchObj->tourism_tax_collection_type; + $checkInDt = new DateTime($date_from); + $checkOutDt = new DateTime($date_to); + $numNights = max(1, (int) $checkInDt->diff($checkOutDt)->days); + $unitPriceTe = (float) $totalPrice['total_price_tax_excl'] / $numNights; + $idLang = (int) Context::getContext()->language->id; + foreach ($occupancy as $roomOcc) { + $childAges = !empty($roomOcc['child_ages']) ? (array) $roomOcc['child_ages'] : array(); + $rows = HotelTourismTaxCalculator::compute( + $idTourismTrg, + $objAddress, + $unitPriceTe, + $date_from, + $numNights, + (int) $roomOcc['adults'], + $childAges, + $id_currency, + $collectionType, + $idLang + ); + $amount = (float) array_sum(array_column($rows, 'total_amount')); + if ($collectionType === 0) { + $totalPrice['tourism_tax_online'] += $amount; + } else { + $totalPrice['tourism_tax_at_hotel'] += $amount; + } + } + } + } + return $totalPrice; } diff --git a/modules/hotelreservationsystem/classes/HotelTourismTax.php b/modules/hotelreservationsystem/classes/HotelTourismTax.php new file mode 100644 index 000000000..dfc5a1ac7 --- /dev/null +++ b/modules/hotelreservationsystem/classes/HotelTourismTax.php @@ -0,0 +1,192 @@ +id before calling add(). + */ +class HotelTourismTax extends ObjectModel +{ + public $id_tax; + public $tax_type; + public $is_per_night; + public $is_per_person; + public $tax_value; + public $is_tiered; + public $has_child_rate; + public $child_tax_value; + public $valid_from; + public $valid_to; + public $is_special_days_exists; + public $special_days; + + public static $definition = array( + 'table' => 'htl_tourism_tax', + 'primary' => 'id_tax', + 'fields' => array( + 'tax_type' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'), + 'is_per_night' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), + 'is_per_person' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), + 'tax_value' => array('type' => self::TYPE_FLOAT, 'validate' => 'isUnsignedFloat'), + 'is_tiered' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), + 'has_child_rate' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), + 'child_tax_value' => array('type' => self::TYPE_FLOAT, 'validate' => 'isUnsignedFloat'), + 'valid_from' => array('type' => self::TYPE_DATE, 'validate' => 'isDate'), + 'valid_to' => array('type' => self::TYPE_DATE, 'validate' => 'isDate'), + 'is_special_days_exists' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), + 'special_days' => array('type' => self::TYPE_STRING), + ), + ); + + /** + * Override add() because id_tax is a FK-as-PK (not auto-increment). + * PrestaShop's ObjectModel::add() never writes the primary key column, so + * MySQL would default it to 0 in non-strict mode, creating phantom rows. + * + * @param bool $auto_date ignored — table has no date_add/date_upd + * @param bool $null_values ignored + * @return bool + */ + public function add($auto_date = true, $null_values = false) + { + if (!(int) $this->id) { + return false; + } + $result = Db::getInstance()->insert( + 'htl_tourism_tax', + array( + 'id_tax' => (int) $this->id, + 'tax_type' => (int) $this->tax_type, + 'is_per_night' => (int) $this->is_per_night, + 'is_per_person' => (int) $this->is_per_person, + 'tax_value' => (float) $this->tax_value, + 'is_tiered' => (int) $this->is_tiered, + 'has_child_rate' => (int) $this->has_child_rate, + 'child_tax_value' => (float) $this->child_tax_value, + 'valid_from' => $this->valid_from ? pSQL($this->valid_from) : null, + 'valid_to' => $this->valid_to ? pSQL($this->valid_to) : null, + 'is_special_days_exists' => (int) $this->is_special_days_exists, + 'special_days' => $this->special_days ? pSQL($this->special_days) : null, + ) + ); + if ($result) { + $this->id_tax = (int) $this->id; + } + return $result; + } + + /** + * @param int $idTax + * @return HotelTourismTax|false + */ + public static function getByTaxId($idTax) + { + $obj = new HotelTourismTax((int) $idTax); + return Validate::isLoadedObject($obj) ? $obj : false; + } + + /** + * @param int $idTax + * @return bool + */ + public static function existsForTax($idTax) + { + return (bool) Db::getInstance()->getValue( + 'SELECT COUNT(*) FROM `' . _DB_PREFIX_ . 'htl_tourism_tax` + WHERE `id_tax` = ' . (int) $idTax + ); + } + + /** + * Remove this tax from all tourism TRGs. + * Returns the count of deleted tax_rule rows so the caller can flash a warning. + * + * @param int $idTax + * @return int + */ + public static function cleanFromTourismTrgs($idTax) + { + $count = (int) Db::getInstance()->getValue( + 'SELECT COUNT(*) FROM `' . _DB_PREFIX_ . 'tax_rule` tr + INNER JOIN `' . _DB_PREFIX_ . 'tax_rules_group` trg + ON trg.`id_tax_rules_group` = tr.`id_tax_rules_group` + WHERE tr.`id_tax` = ' . (int) $idTax . ' + AND trg.`is_tourism_tax_rule` = 1' + ); + Db::getInstance()->execute( + 'DELETE tr FROM `' . _DB_PREFIX_ . 'tax_rule` tr + INNER JOIN `' . _DB_PREFIX_ . 'tax_rules_group` trg + ON trg.`id_tax_rules_group` = tr.`id_tax_rules_group` + WHERE tr.`id_tax` = ' . (int) $idTax . ' + AND trg.`is_tourism_tax_rule` = 1' + ); + return $count; + } + + /** + * Remove this tax from all standard (non-tourism) TRGs when it is promoted to a tourism tax. + * Symmetric counterpart of cleanFromTourismTrgs(). + * Returns the count of deleted tax_rule rows so the caller can flash a warning. + * + * @param int $idTax + * @return int + */ + public static function cleanFromVatTrgs($idTax) + { + $count = (int) Db::getInstance()->getValue( + 'SELECT COUNT(*) FROM `' . _DB_PREFIX_ . 'tax_rule` tr + INNER JOIN `' . _DB_PREFIX_ . 'tax_rules_group` trg + ON trg.`id_tax_rules_group` = tr.`id_tax_rules_group` + WHERE tr.`id_tax` = ' . (int) $idTax . ' + AND trg.`is_tourism_tax_rule` = 0' + ); + Db::getInstance()->execute( + 'DELETE tr FROM `' . _DB_PREFIX_ . 'tax_rule` tr + INNER JOIN `' . _DB_PREFIX_ . 'tax_rules_group` trg + ON trg.`id_tax_rules_group` = tr.`id_tax_rules_group` + WHERE tr.`id_tax` = ' . (int) $idTax . ' + AND trg.`is_tourism_tax_rule` = 0' + ); + return $count; + } + + /** + * Delete the subtype row and all related tiers/child-ranges. + */ + public function delete() + { + Db::getInstance()->execute( + 'DELETE FROM `' . _DB_PREFIX_ . 'htl_tourism_tax_tier` + WHERE `id_tax` = ' . (int) $this->id + ); + Db::getInstance()->execute( + 'DELETE FROM `' . _DB_PREFIX_ . 'htl_tourism_tax_child_range` + WHERE `id_tax` = ' . (int) $this->id + ); + return parent::delete(); + } +} diff --git a/modules/hotelreservationsystem/classes/HotelTourismTaxCalculator.php b/modules/hotelreservationsystem/classes/HotelTourismTaxCalculator.php new file mode 100644 index 000000000..58b05c094 --- /dev/null +++ b/modules/hotelreservationsystem/classes/HotelTourismTaxCalculator.php @@ -0,0 +1,513 @@ +format('N') - 1; + + $results = array(); + + foreach ($rows as $row) { + $taxParams = $row['params']; + + if (!self::isValidForDate($taxParams, $checkInDate, $isoDayIndex)) { + continue; + } + + $isTiered = (bool) $taxParams['is_tiered']; + $taxType = (int) $taxParams['tax_type']; + $isPerNight = (bool) $taxParams['is_per_night']; + $isPerPerson = (bool) $taxParams['is_per_person']; + $hasChildRate = (bool) $taxParams['has_child_rate']; + + $baseValue = (float) $taxParams['tax_value']; + if ($isTiered) { + $tier = HotelTourismTaxTier::matchTier((int) $row['id_tax'], (float) $unitPriceTaxExcl); + if (!$tier) { + continue; + } + $baseValue = (float) $tier['tax_value']; + } + + $adultMultiplier = 1; + if ($isPerNight) { + $adultMultiplier *= (int) $numNights; + } + if ($isPerPerson) { + $adultMultiplier *= (int) $numAdults; + } + + if ($taxType === 0) { + $unitAmountAdult = $baseValue; + $totalAmountAdult = $baseValue * $adultMultiplier; + } else { + $unitAmountAdult = (float) $unitPriceTaxExcl * ($baseValue / 100); + $totalAmountAdult = $unitAmountAdult * $adultMultiplier; + } + + $unitAmountChild = 0.0; + $totalAmountChild = 0.0; + $numQualifyingChildren = 0; + + if ($hasChildRate && !empty($childrenAges)) { + $contribution = HotelTourismTaxChildRange::computeChildContribution( + (int) $row['id_tax'], + $childrenAges, + $taxType, + (float) $unitPriceTaxExcl, + $baseValue + ); + $numQualifyingChildren = (int) $contribution['count']; + if ($numQualifyingChildren > 0) { + $nightMultiplier = $isPerNight ? (int) $numNights : 1; + $totalAmountChild = $contribution['total'] * $nightMultiplier; + $unitAmountChild = $contribution['total'] / $numQualifyingChildren; + } + } + + $unitAmountAdult = Tools::convertPrice($unitAmountAdult, $idCurrency); + $unitAmountChild = Tools::convertPrice($unitAmountChild, $idCurrency); + $totalAmountAdult = Tools::convertPrice($totalAmountAdult, $idCurrency); + $totalAmountChild = Tools::convertPrice($totalAmountChild, $idCurrency); + $totalAmount = round($totalAmountAdult + $totalAmountChild, 6); + + $results[] = array( + 'id_tax' => (int) $row['id_tax'], + 'tax_name' => (string) $row['tax_name'], + 'num_nights' => (int) $numNights, + 'num_adults' => (int) $numAdults, + 'total_amount' => $totalAmount, + 'collection_type' => (int) $collectionType, + ); + } + + return $results; + } + + /** + * Compute and persist tourism tax rows for a confirmed order_detail line. + * Runs inside a single DB transaction. Safe to call more than once (idempotent via DELETE+INSERT). + * + * Returns the sum of total_amount where collection_type=0 (for Cart::getOrderTotal). + * + * @param int $idOrder + * @param int $idOrderDetail + * @param int $idHtlBooking + * @param int $idHotel + * @param int $idTaxRulesGroup + * @param Address $address + * @param float $unitPriceTaxExcl + * @param string $checkInDate + * @param int $numNights + * @param int $numAdults + * @param array $childrenAges + * @param int $idCurrency + * @param int $collectionType + * @param int $idLang + * @return float Online-charged tourism tax total for this order detail + */ + public static function computeAndSave( + $idOrder, + $idOrderDetail, + $idHtlBooking, + $idHotel, + $idTaxRulesGroup, + Address $address, + $unitPriceTaxExcl, + $checkInDate, + $numNights, + $numAdults, + array $childrenAges, + $idCurrency, + $collectionType, + $idLang + ) { + $rows = self::compute( + $idTaxRulesGroup, + $address, + $unitPriceTaxExcl, + $checkInDate, + $numNights, + $numAdults, + $childrenAges, + $idCurrency, + $collectionType, + $idLang + ); + + $db = Db::getInstance(); + $db->execute('START TRANSACTION'); + + try { + // Capture per-booking previous totals before deleting rows. + // Both queries scoped to id_htl_booking so multiple bookings sharing one + // order_detail row accumulate independently and do not overwrite each other. + $prevOnlineTotal = (float) $db->getValue( + 'SELECT COALESCE(SUM(`total_amount`), 0) + FROM `' . _DB_PREFIX_ . 'htl_order_tourism_tax` + WHERE `id_htl_booking` = ' . (int) $idHtlBooking . ' + AND `collection_type` = 0 + AND `is_exempted` = 0 + AND `is_refunded` = 0' + ); + $prevTotalTourismTax = (float) $db->getValue( + 'SELECT COALESCE(SUM(`total_amount`), 0) + FROM `' . _DB_PREFIX_ . 'htl_order_tourism_tax` + WHERE `id_htl_booking` = ' . (int) $idHtlBooking + ); + + HotelOrderTourismTax::deleteByHtlBooking((int) $idHtlBooking); + + $now = date('Y-m-d H:i:s'); + $totalTourismTax = 0.0; + $newOnlineTotal = 0.0; + foreach ($rows as $row) { + $db->insert('htl_order_tourism_tax', array( + 'id_order' => (int) $idOrder, + 'id_order_detail' => (int) $idOrderDetail, + 'id_htl_booking' => (int) $idHtlBooking, + 'id_hotel' => (int) $idHotel, + 'id_tax' => (int) $row['id_tax'], + 'id_currency' => (int) $idCurrency, + 'tax_name' => pSQL($row['tax_name']), + 'num_nights' => (int) $row['num_nights'], + 'num_adults' => (int) $row['num_adults'], + 'total_amount' => (float) $row['total_amount'], + 'collection_type' => (int) $row['collection_type'], + 'is_exempted' => 0, + 'is_refunded' => 0, + 'date_add' => pSQL($now), + 'date_upd' => pSQL($now), + )); + $totalTourismTax += (float) $row['total_amount']; + if ((int) $row['collection_type'] === 0) { + $newOnlineTotal += (float) $row['total_amount']; + } + } + + // Delta-update both fields so multiple bookings on one order_detail accumulate. + // tourism_tax_amount: subtract this booking's old total, add new total. + // total_price_tax_incl: subtract old online total, add new online total. + $db->execute( + 'UPDATE `' . _DB_PREFIX_ . 'order_detail` + SET `tourism_tax_amount` = GREATEST(0, `tourism_tax_amount` + - ' . (float) $prevTotalTourismTax . ' + + ' . (float) $totalTourismTax . '), + `total_price_tax_incl` = `total_price_tax_incl` + - ' . (float) $prevOnlineTotal . ' + + ' . (float) $newOnlineTotal . ' + WHERE `id_order_detail` = ' . (int) $idOrderDetail + ); + + $db->execute('COMMIT'); + } catch (Exception $e) { + $db->execute('ROLLBACK'); + throw $e; + } + + return $newOnlineTotal; + } + + /** + * Resolve the applicable ps_tax + htl_tourism_tax rows for a TRG and hotel address. + * Follows the same geo-routing logic as TaxRulesTaxManagerCore::getTaxCalculator(). + * behavior=0 on the most specific row → stop after first match. + * behavior=1 → accumulate all matched rows. + * + * @param int $idTaxRulesGroup + * @param Address $address + * @param int $idLang + * @return array Each entry: ['id_tax', 'tax_name', 'params' => htl_tourism_tax row, 'behavior'] + */ + protected static function resolveApplicableTaxes($idTaxRulesGroup, Address $address, $idLang) + { + $postcode = !empty($address->postcode) ? $address->postcode : '0'; + + $taxRows = Db::getInstance()->executeS( + 'SELECT tr.`id_tax`, tr.`behavior`, + tl.`name` AS tax_name, + ht.* + FROM `' . _DB_PREFIX_ . 'tax_rule` tr + JOIN `' . _DB_PREFIX_ . 'tax` t + ON t.`id_tax` = tr.`id_tax` + AND t.`active` = 1 + AND t.`deleted` = 0 + AND t.`is_tourism_tax` = 1 + JOIN `' . _DB_PREFIX_ . 'tax_rules_group` trg + ON trg.`id_tax_rules_group` = tr.`id_tax_rules_group` + AND trg.`active` = 1 + AND trg.`is_tourism_tax_rule` = 1 + LEFT JOIN `' . _DB_PREFIX_ . 'tax_lang` tl + ON tl.`id_tax` = t.`id_tax` + AND tl.`id_lang` = ' . (int) $idLang . ' + JOIN `' . _DB_PREFIX_ . 'htl_tourism_tax` ht + ON ht.`id_tax` = t.`id_tax` + WHERE tr.`id_tax_rules_group` = ' . (int) $idTaxRulesGroup . ' + AND tr.`id_country` = ' . (int) $address->id_country . ' + AND tr.`id_state` IN (0, ' . (int) $address->id_state . ') + AND tr.`id_tax` != 0 + AND (\'' . pSQL($postcode) . '\' BETWEEN tr.`zipcode_from` AND tr.`zipcode_to` + OR (tr.`zipcode_to` = 0 AND tr.`zipcode_from` IN (0, \'' . pSQL($postcode) . '\'))) + ORDER BY tr.`zipcode_from` DESC, tr.`zipcode_to` DESC, + tr.`id_state` DESC, tr.`id_country` DESC' + ); + + if (empty($taxRows)) { + return array(); + } + + $resolved = array(); + $firstBehavior = null; + + foreach ($taxRows as $taxRow) { + if ($firstBehavior === null) { + $firstBehavior = (int) $taxRow['behavior']; + } + + $resolved[] = array( + 'id_tax' => (int) $taxRow['id_tax'], + 'tax_name' => (string) ($taxRow['tax_name'] ?: ''), + 'params' => $taxRow, + 'behavior' => (int) $taxRow['behavior'], + ); + + if ($firstBehavior === 0) { + break; + } + } + + return $resolved; + } + + /** + * Check whether a tourism tax is valid for the given check-in date. + * + * @param array $params htl_tourism_tax row + * @param string $checkInDate 'YYYY-MM-DD' + * @param int $isoDayIndex 0=Mon…6=Sun (DateTime::format('N') - 1) + * @return bool + */ + protected static function isValidForDate(array $params, $checkInDate, $isoDayIndex) + { + $checkIn = new DateTime($checkInDate); + + if (!empty($params['valid_from']) && $params['valid_from'] !== '0000-00-00') { + $from = new DateTime($params['valid_from']); + if ($checkIn < $from) { + return false; + } + } + + if (!empty($params['valid_to']) && $params['valid_to'] !== '0000-00-00') { + $to = new DateTime($params['valid_to']); + if ($checkIn > $to) { + return false; + } + } + + if ((int) $params['is_special_days_exists']) { + $dayNames = array('mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'); + $specialDays = json_decode($params['special_days'], true); + if (!is_array($specialDays) || !in_array($dayNames[$isoDayIndex], $specialDays)) { + return false; + } + } + + return true; + } + + /** + * Apply (compute + save) tourism tax for a booking on an already-confirmed order. + * Fetches all required parameters from the existing DB records. + * Returns false when the booking has no tourism TRG assigned or data is missing. + * + * @param int $idHtlBooking + * @return bool + */ + public static function applyFromExistingBooking($idHtlBooking) + { + $booking = new HotelBookingDetail((int) $idHtlBooking); + if (!Validate::isLoadedObject($booking)) { + return false; + } + + $order = new Order((int) $booking->id_order); + if (!Validate::isLoadedObject($order)) { + return false; + } + + $idTaxRulesGroup = (int) Db::getInstance()->getValue( + 'SELECT `id_tourism_tax_rules_group` + FROM `' . _DB_PREFIX_ . 'product_shop` + WHERE `id_product` = ' . (int) $booking->id_product . ' + AND `id_shop` = ' . (int) $order->id_shop + ); + if (!$idTaxRulesGroup) { + return false; + } + + $address = new Address((int) $order->id_address_tax); + if (!Validate::isLoadedObject($address)) { + return false; + } + + $objBranch = new HotelBranchInformation((int) $booking->id_hotel); + if (!Validate::isLoadedObject($objBranch)) { + return false; + } + $collectionType = (int) $objBranch->tourism_tax_collection_type; + + $numNights = (int) HotelHelper::getNumberOfDays($booking->date_from, $booking->date_to); + if ($numNights <= 0) { + return false; + } + + $unitPriceTaxExcl = round((float) $booking->total_price_tax_excl / $numNights, 6); + + $childrenAges = array(); + if ($booking->child_ages) { + $decoded = json_decode($booking->child_ages, true); + if (is_array($decoded)) { + $childrenAges = array_map('intval', $decoded); + } + } + + self::computeAndSave( + (int) $booking->id_order, + (int) $booking->id_order_detail, + (int) $idHtlBooking, + (int) $booking->id_hotel, + $idTaxRulesGroup, + $address, + $unitPriceTaxExcl, + $booking->date_from, + $numNights, + (int) $booking->adults, + $childrenAges, + (int) $order->id_currency, + $collectionType, + (int) Context::getContext()->language->id + ); + + return true; + } + + /** + * Compute cart-time total for display (does not save). + * Used by Cart::getOrderTotal() before order confirmation. + * + * @param int $idTaxRulesGroup + * @param Address $address + * @param float $unitPriceTaxExcl + * @param string $checkInDate + * @param int $numNights + * @param int $numAdults + * @param array $childrenAges + * @param int $idCurrency + * @param int $collectionType + * @param int $idLang + * @return float Online-charged total only + */ + public static function computeOnlineTotal( + $idTaxRulesGroup, + Address $address, + $unitPriceTaxExcl, + $checkInDate, + $numNights, + $numAdults, + array $childrenAges, + $idCurrency, + $collectionType, + $idLang + ) { + if ((int) $collectionType !== 0) { + return 0.0; + } + $rows = self::compute( + $idTaxRulesGroup, $address, $unitPriceTaxExcl, $checkInDate, + $numNights, $numAdults, $childrenAges, $idCurrency, $collectionType, $idLang + ); + $total = 0.0; + foreach ($rows as $row) { + $total += (float) $row['total_amount']; + } + return $total; + } +} diff --git a/modules/hotelreservationsystem/classes/HotelTourismTaxChildRange.php b/modules/hotelreservationsystem/classes/HotelTourismTaxChildRange.php new file mode 100644 index 000000000..6149e6409 --- /dev/null +++ b/modules/hotelreservationsystem/classes/HotelTourismTaxChildRange.php @@ -0,0 +1,168 @@ + 'htl_tourism_tax_child_range', + 'primary' => 'id_child_range', + 'fields' => array( + 'id_tax' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true), + 'min_age' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'), + 'max_age' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'), + 'tax_value' => array('type' => self::TYPE_FLOAT, 'validate' => 'isUnsignedFloat'), + 'position' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'), + ), + ); + + /** + * Return all child age ranges for a tax sorted by position. + * + * @param int $idTax + * @return array + */ + public static function getByTaxId($idTax) + { + return Db::getInstance()->executeS( + 'SELECT * FROM `' . _DB_PREFIX_ . 'htl_tourism_tax_child_range` + WHERE `id_tax` = ' . (int) $idTax . ' + ORDER BY `position` ASC, `min_age` ASC' + ); + } + + /** + * Replace all child range rows for $idTax with the submitted arrays. + * Skips entries where tax_value is empty. + * + * @param int $idTax + * @param array $mins + * @param array $maxs + * @param array $values + * @return void + */ + public static function saveAll($idTax, array $mins, array $maxs, array $values) + { + Db::getInstance()->execute( + 'DELETE FROM `' . _DB_PREFIX_ . 'htl_tourism_tax_child_range` + WHERE `id_tax` = ' . (int) $idTax + ); + foreach ($values as $i => $value) { + if ($value === '' || $value === false) { + continue; + } + $range = new HotelTourismTaxChildRange(); + $range->id_tax = (int) $idTax; + $range->min_age = isset($mins[$i]) ? (int) $mins[$i] : 0; + $range->max_age = isset($maxs[$i]) ? (int) $maxs[$i] : 0; + $range->tax_value = (float) $value; + $range->position = (int) $i; + $range->add(); + } + } + + /** + * Sum per-band tax contributions for the given child ages. + * Each child is matched to the first qualifying band; unmatched children pay the adult rate + * when $adultBaseValue is provided, otherwise they contribute 0. + * + * Returns array: ['total' => float, 'count' => int] + * + * @param int $idTax + * @param int[] $ages Ages at check-in date + * @param int $taxType 0=fixed, 1=percentage + * @param float $unitPrice Room unit_price_tax_excl (used for percentage type) + * @param float|null $adultBaseValue Adult tax_value (fixed amount or percentage); null = exempt unmatched children + * @return array + */ + public static function computeChildContribution($idTax, array $ages, $taxType, $unitPrice, $adultBaseValue = null) + { + $ranges = self::getByTaxId((int) $idTax); + $total = 0.0; + $count = 0; + foreach ($ages as $age) { + $matched = false; + foreach ($ranges as $range) { + $inLower = ((int) $age >= (int) $range['min_age']); + $inUpper = ((int) $range['max_age'] == 0 || (int) $age <= (int) $range['max_age']); + if ($inLower && $inUpper) { + $bandValue = (float) $range['tax_value']; + $total += ((int) $taxType === 0) + ? $bandValue + : ((float) $unitPrice * ($bandValue / 100)); + $count++; + $matched = true; + break; + } + } + if (!$matched && $adultBaseValue !== null) { + $total += ((int) $taxType === 0) + ? (float) $adultBaseValue + : ((float) $unitPrice * ((float) $adultBaseValue / 100)); + $count++; + } + } + return array('total' => $total, 'count' => $count); + } + + /** + * Check whether a new range [newMin, newMax] overlaps any existing range for the same tax. + * + * @param int $idTax + * @param int $newMin + * @param int $newMax + * @param int $excludeId Range id to exclude (for edit validation) + * @return bool + */ + public static function hasOverlap($idTax, $newMin, $newMax, $excludeId = 0) + { + $ranges = self::getByTaxId((int) $idTax); + foreach ($ranges as $range) { + if ($excludeId && (int) $range['id_child_range'] === (int) $excludeId) { + continue; + } + $exMax = (int) $range['max_age']; + $newMaxEff = ($newMax == 0) ? PHP_INT_MAX : (int) $newMax; + $exMaxEff = ($exMax == 0) ? PHP_INT_MAX : $exMax; + if ((int) $newMin <= $exMaxEff && $newMaxEff >= (int) $range['min_age']) { + return true; + } + } + return false; + } +} diff --git a/modules/hotelreservationsystem/classes/HotelTourismTaxTier.php b/modules/hotelreservationsystem/classes/HotelTourismTaxTier.php new file mode 100644 index 000000000..693d42180 --- /dev/null +++ b/modules/hotelreservationsystem/classes/HotelTourismTaxTier.php @@ -0,0 +1,145 @@ + 'htl_tourism_tax_tier', + 'primary' => 'id_tier', + 'fields' => array( + 'id_tax' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true), + 'min_amount' => array('type' => self::TYPE_FLOAT, 'validate' => 'isUnsignedFloat'), + 'max_amount' => array('type' => self::TYPE_FLOAT, 'validate' => 'isUnsignedFloat'), + 'tax_value' => array('type' => self::TYPE_FLOAT, 'validate' => 'isUnsignedFloat'), + 'position' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'), + ), + ); + + /** + * Return all tiers for a tax sorted by position. + * + * @param int $idTax + * @return array + */ + public static function getByTaxId($idTax) + { + return Db::getInstance()->executeS( + 'SELECT * FROM `' . _DB_PREFIX_ . 'htl_tourism_tax_tier` + WHERE `id_tax` = ' . (int) $idTax . ' + ORDER BY `position` ASC, `min_amount` ASC' + ); + } + + /** + * Find the matching tier for a given unit price. + * Returns the tier row or false when no tier matches. + * + * @param int $idTax + * @param float $unitPrice + * @return array|false + */ + public static function matchTier($idTax, $unitPrice) + { + $tiers = self::getByTaxId((int) $idTax); + foreach ($tiers as $tier) { + $inLower = ((float) $unitPrice >= (float) $tier['min_amount']); + $inUpper = ((float) $tier['max_amount'] == 0 || (float) $unitPrice < (float) $tier['max_amount']); + if ($inLower && $inUpper) { + return $tier; + } + } + return false; + } + + /** + * Replace all tier rows for $idTax with the submitted arrays. + * Skips entries where tax_value is empty. + * + * @param int $idTax + * @param array $mins + * @param array $maxs + * @param array $values + * @return void + */ + public static function saveAll($idTax, array $mins, array $maxs, array $values) + { + Db::getInstance()->execute( + 'DELETE FROM `' . _DB_PREFIX_ . 'htl_tourism_tax_tier` + WHERE `id_tax` = ' . (int) $idTax + ); + foreach ($values as $i => $value) { + if ($value === '' || $value === false) { + continue; + } + $tier = new HotelTourismTaxTier(); + $tier->id_tax = (int) $idTax; + $tier->min_amount = isset($mins[$i]) ? (float) $mins[$i] : 0; + $tier->max_amount = isset($maxs[$i]) ? (float) $maxs[$i] : 0; + $tier->tax_value = (float) $value; + $tier->position = (int) $i; + $tier->add(); + } + } + + /** + * Check whether a new tier [newMin, newMax) overlaps any existing tier for the same tax. + * + * @param int $idTax + * @param float $newMin + * @param float $newMax 0 = open-ended + * @param int $excludeId Tier id to exclude (for edit validation) + * @return bool + */ + public static function hasOverlap($idTax, $newMin, $newMax, $excludeId = 0) + { + $tiers = self::getByTaxId((int) $idTax); + foreach ($tiers as $tier) { + if ($excludeId && (int) $tier['id_tier'] === (int) $excludeId) { + continue; + } + $exMin = (float) $tier['min_amount']; + $exMax = (float) $tier['max_amount']; + $newMaxEff = ((float) $newMax == 0) ? PHP_FLOAT_MAX : (float) $newMax; + $exMaxEff = ($exMax == 0) ? PHP_FLOAT_MAX : $exMax; + if ($newMin < $exMaxEff && $newMaxEff > $exMin) { + return true; + } + } + return false; + } +} diff --git a/modules/hotelreservationsystem/controllers/admin/AdminAddHotelController.php b/modules/hotelreservationsystem/controllers/admin/AdminAddHotelController.php index 868012630..42d52a6fd 100644 --- a/modules/hotelreservationsystem/controllers/admin/AdminAddHotelController.php +++ b/modules/hotelreservationsystem/controllers/admin/AdminAddHotelController.php @@ -236,6 +236,8 @@ class AdminAddHotelController extends ModuleAdminController $smartyVars['PS_MAX_CHECKOUT_OFFSET'] = (int) Configuration::get('PS_MAX_CHECKOUT_OFFSET'); $smartyVars['PS_MIN_BOOKING_OFFSET'] = (int) Configuration::get('PS_MIN_BOOKING_OFFSET'); $smartyVars['WK_ORDER_REFUND_ALLOWED'] = Configuration::get('WK_ORDER_REFUND_ALLOWED'); + $smartyVars['tourism_tax_collection_type'] = (int) $this->object->tourism_tax_collection_type; + $smartyVars['use_tourism_tax'] = (int) Configuration::get('QLO_USE_TOURISM_TAX'); $this->context->smarty->assign($smartyVars); @@ -518,6 +520,7 @@ class AdminAddHotelController extends ModuleAdminController } $objHotelBranch->active = $active; $objHotelBranch->active_refund = $activeRefund; + $objHotelBranch->tourism_tax_collection_type = (int) Tools::getValue('tourism_tax_collection_type', 0); $objHotelBranch->fax = $fax; // lang fields diff --git a/modules/hotelreservationsystem/define.php b/modules/hotelreservationsystem/define.php index e8a7cc909..7f65da344 100644 --- a/modules/hotelreservationsystem/define.php +++ b/modules/hotelreservationsystem/define.php @@ -61,5 +61,12 @@ require_once 'classes/ServiceProductOption.php'; require_once 'classes/HotelSettingsLink.php'; require_once 'classes/HotelBookingDocument.php'; +require_once 'classes/HotelTourismTax.php'; +require_once 'classes/HotelTourismTaxTier.php'; +require_once 'classes/HotelTourismTaxChildRange.php'; +require_once 'classes/HotelOrderTourismTax.php'; +require_once 'classes/HotelBookingTaxExemption.php'; +require_once 'classes/HotelTourismTaxCalculator.php'; + // Web services classes require_once 'classes/WebserviceSpecificManagementHotelAri.php'; diff --git a/modules/hotelreservationsystem/hotelreservationsystem.php b/modules/hotelreservationsystem/hotelreservationsystem.php index 5246337d7..b7b15581e 100644 --- a/modules/hotelreservationsystem/hotelreservationsystem.php +++ b/modules/hotelreservationsystem/hotelreservationsystem.php @@ -504,15 +504,46 @@ class HotelReservationSystem extends Module public function HookActionCartSummary($params) { - // $objCartBookingData = new HotelCartBookingData(); - // $totalFacilityCostTI = $objCartBookingData->getCartExtraDemands($params['cart']->id, 0, 0, 0, 0, 1, 0, 1); - // $totalFacilityCostTE = $objCartBookingData->getCartExtraDemands($params['cart']->id, 0, 0, 0, 0, 1, 0, 0); - // return array( - // 'additional_facilities_tax' => ($totalFacilityCostTI - $totalFacilityCostTE), - // 'totalFacilityCostTE' => $totalFacilityCostTE, - // 'totalFacilityCostTI' => $totalFacilityCostTI, - // ); - return array(); + if (!Configuration::get('QLO_USE_TOURISM_TAX') || !Configuration::get('PS_TAX')) { + return array(); + } + + $cart = $this->context->cart; + if (!Validate::isLoadedObject($cart)) { + return array(); + } + + $tourismTaxAtHotel = 0.0; + $objCartBookingData = new HotelCartBookingData(); + $cartItems = $objCartBookingData->getCartCurrentDataByCartId($cart->id); + if ($cartItems) { + foreach ($cartItems as $item) { + $occupancy = array( + array( + 'adults' => (int) $item['adults'], + 'children' => (int) $item['children'], + 'child_ages' => json_decode($item['child_ages']), + ) + ); + $roomTotalPrice = HotelRoomTypeFeaturePricing::getRoomTypeTotalPrice( + $item['id_product'], + $item['date_from'], + $item['date_to'], + $occupancy, + Group::getCurrent()->id, + $item['id_cart'], + $item['id_guest'], + $item['id_room'], + 0 + ); + $tourismTaxAtHotel += (float) $roomTotalPrice['tourism_tax_at_hotel']; + } + } + + return array( + 'tourism_tax_at_hotel' => $tourismTaxAtHotel, + 'tourism_tax_grossed_up' => (bool) Configuration::get('QLO_TOURISM_TAX_GROSSED_UP'), + ); } public function callInstallTab() @@ -648,7 +679,9 @@ class HotelReservationSystem extends Module 'WK_HOTEL_NAME_ENABLE', 'WK_CUSTOMER_SUPPORT_PHONE_NUMBER', 'WK_CUSTOMER_SUPPORT_EMAIL', - 'WK_DISPLAY_CONTACT_PAGE_HOTEL_LIST' + 'WK_DISPLAY_CONTACT_PAGE_HOTEL_LIST', + 'QLO_USE_TOURISM_TAX', + 'QLO_TOURISM_TAX_GROSSED_UP', ); foreach ($configKeys as $key) { if (!Configuration::deleteByName($key)) { diff --git a/modules/hotelreservationsystem/views/css/TourismTaxAdmin.css b/modules/hotelreservationsystem/views/css/TourismTaxAdmin.css new file mode 100644 index 000000000..fa36a93b6 --- /dev/null +++ b/modules/hotelreservationsystem/views/css/TourismTaxAdmin.css @@ -0,0 +1,73 @@ +/** + * 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 + */ + +.tt-child-field, +.tt-tiered-field { + display: none; +} + +.tt-subtable { + width: 100%; + margin-bottom: 8px; + border-collapse: collapse; +} + +.tt-subtable thead th { + background: #f5f5f5; + font-weight: 600; + padding: 8px 10px; + border: 1px solid #ddd; + white-space: nowrap; +} + +.tt-subtable tbody td { + padding: 5px 8px; + border: 1px solid #e8e8e8; + vertical-align: middle; +} + +.tt-subtable .form-control { + height: 30px; + padding: 4px 8px; + font-size: 13px; +} + +.tt-subtable .btn-remove-row { + padding: 3px 8px; +} + +.tt-add-row-btn { + margin-top: 8px; +} + +.tt-req-star { + color: #c0392b; + font-weight: bold; + font-size: 15px; + margin-left: 2px; +} + +.form-control.tt-field-error, +.form-control.tt-field-error:focus { + border-color: #a94442; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 4px #ce8483; +} diff --git a/modules/hotelreservationsystem/views/js/TourismTaxForm.js b/modules/hotelreservationsystem/views/js/TourismTaxForm.js new file mode 100644 index 000000000..1213d3a0a --- /dev/null +++ b/modules/hotelreservationsystem/views/js/TourismTaxForm.js @@ -0,0 +1,185 @@ +/** + * 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 + */ + +$(document).ready(function () { + var tierIdx = $('#tt-tiers-body tr').length; + var childIdx = $('#tt-child-body tr').length; + + $('#htl_valid_from').datepicker({ + onSelect: function () { + $('#htl_valid_to').datepicker('option', 'minDate', $(this).datepicker('getDate')); + validateDates(); + } + }); + + $('#htl_valid_to').datepicker({ onSelect: validateDates }); + + function validateDates() { + var from = $('#htl_valid_from').val(); + var to = $('#htl_valid_to').val(); + var $err = $('#tt-date-error'); + if (from && to) { + try { + var fromTs = $.datepicker.parseDate('yy-mm-dd', from).getTime(); + var toTs = $.datepicker.parseDate('yy-mm-dd', to).getTime(); + if (toTs < fromTs) { + $err.text('"Valid to" must be on or after "Valid from".').show(); + return false; + } + } catch (e) {} + } + $err.hide(); + return true; + } + + function isTourismOn() { return $('input[name="is_tourism_tax"]:checked').val() == '1'; } + function isChildRateOn() { return $('input[name="htl_has_child_rate"]:checked').val() == '1'; } + function isTieredOn() { return $('input[name="htl_is_tiered"]:checked').val() == '1'; } + + function setVisible($el, visible, animate) { + if (animate) { + visible ? $el.slideDown(250) : $el.slideUp(250); + } else { + visible ? $el.show() : $el.hide(); + } + } + + function ensureOneRow(bodyId, tplId, idxRef) { + if ($('#' + bodyId + ' tr').length === 0) { + var tpl = $('#' + tplId).html().replace(/__IDX__/g, idxRef.val); + var $row = $(tpl); + $row.find('.tt-row-type-sign').text(currentTypeSign()); + $('#' + bodyId).append($row); + idxRef.val++; + } + } + + var tierIdxRef = { val: tierIdx }; + var childIdxRef = { val: childIdx }; + + function updateAllVisibility(animate) { + var t = isTourismOn(); + setVisible($('.tt-field').not('.tt-child-field, .tt-tiered-field'), t, animate); + setVisible($('.tt-non-tourism'), !t, animate); + + var showChild = t && isChildRateOn(); + setVisible($('.tt-child-field'), showChild, animate); + if (showChild) { + ensureOneRow('tt-child-body', 'tt-child-row-tpl', childIdxRef); + } + + var showTiered = t && isTieredOn(); + setVisible($('.tt-tiered-field'), showTiered, animate); + if (showTiered) { + ensureOneRow('tt-tiers-body', 'tt-tier-row-tpl', tierIdxRef); + } + } + + updateAllVisibility(false); + updateTypeSign(); + + $('input[name="is_tourism_tax"], input[name="htl_has_child_rate"], input[name="htl_is_tiered"]').on('change', function () { + setTimeout(function () { updateAllVisibility(true); }, 0); + }); + + $(document).on('click', '.prestashop-switch .slide-button', function () { + setTimeout(function () { updateAllVisibility(true); }, 0); + }); + + function currentTypeSign() { + var $sign = $('#tt-type-sign'); + return $('select[name="htl_tax_type"]').val() == 1 ? '%' : $sign.data('currency'); + } + + function updateTypeSign() { + var sign = currentTypeSign(); + $('#tt-type-sign').text(sign); + $('.tt-row-type-sign').text(sign); + } + + $('select[name="htl_tax_type"]').on('change', updateTypeSign); + + $(document).on('click', '#tt-add-tier', function (e) { + e.preventDefault(); + var tpl = $('#tt-tier-row-tpl').html().replace(/__IDX__/g, tierIdxRef.val); + var $row = $(tpl); + $row.find('.tt-row-type-sign').text(currentTypeSign()); + $('#tt-tiers-body').append($row); + tierIdxRef.val++; + }); + + $(document).on('click', '#tt-add-child', function (e) { + e.preventDefault(); + var tpl = $('#tt-child-row-tpl').html().replace(/__IDX__/g, childIdxRef.val); + var $row = $(tpl); + $row.find('.tt-row-type-sign').text(currentTypeSign()); + $('#tt-child-body').append($row); + childIdxRef.val++; + }); + + $(document).on('click', '.tt-remove-row', function (e) { + e.preventDefault(); + $(this).closest('tr').remove(); + }); + + $('form.defaultForm').on('submit', function () { + var hasError = false; + + if (isChildRateOn()) { + $('#tt-child-body tr').each(function () { + $(this).find('.tt-req').each(function () { + if ($.trim($(this).val()) === '') { + $(this).addClass('tt-field-error'); + hasError = true; + } + }); + }); + } + + if (isTieredOn()) { + $('#tt-tiers-body tr').each(function () { + $(this).find('.tt-req').each(function () { + if ($.trim($(this).val()) === '') { + $(this).addClass('tt-field-error'); + hasError = true; + } + }); + }); + } + + if (hasError) { + setTimeout(function () { + var $first = $('.tt-field-error').first(); + if ($first.length) { + $first[0].scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }, 50); + } + return !hasError; + }); + + $(document).on('input', '.tt-req', function () { + if ($.trim($(this).val()) !== '') { + $(this).removeClass('tt-field-error'); + } + }); +}); diff --git a/modules/hotelreservationsystem/views/templates/admin/add_hotel/helpers/form/form.tpl b/modules/hotelreservationsystem/views/templates/admin/add_hotel/helpers/form/form.tpl index 978872548..4fdc902bc 100644 --- a/modules/hotelreservationsystem/views/templates/admin/add_hotel/helpers/form/form.tpl +++ b/modules/hotelreservationsystem/views/templates/admin/add_hotel/helpers/form/form.tpl @@ -98,6 +98,14 @@ {l s='Features' mod='hotelreservationsystem'} + {if $use_tourism_tax} +
  • + + + {l s='Tourism Tax' mod='hotelreservationsystem'} + +
  • + {/if} {hook h='displayAdminAddHotelFormTab' id_hotel=$hook_arg_id_hotel}
    @@ -631,6 +639,25 @@ {hook h='displayAdminAddHotelFormFeaturesTabAfter' id_hotel=$hook_arg_id_hotel}
    + {if $use_tourism_tax} +
    +
    + +
    + +
    +
    +
    + {/if} {hook h='displayAdminAddHotelFormTabContent' id_hotel=$hook_arg_id_hotel}
    diff --git a/modules/hotelreservationsystem/views/templates/admin/order_refund_requests/helpers/view/view.tpl b/modules/hotelreservationsystem/views/templates/admin/order_refund_requests/helpers/view/view.tpl index d8cadf9e9..058394fdd 100644 --- a/modules/hotelreservationsystem/views/templates/admin/order_refund_requests/helpers/view/view.tpl +++ b/modules/hotelreservationsystem/views/templates/admin/order_refund_requests/helpers/view/view.tpl @@ -170,7 +170,7 @@ {assign var="is_full_date" value=($show_full_date && ($booking['date_from']|date_format:'%D' == $booking['date_to']|date_format:'%D'))} {dateFormat date=$booking['date_from'] full=$is_full_date} {l s='To' mod='hotelreservationsystem'} {dateFormat date=$booking['date_to'] full=$is_full_date} - {displayPrice price=($booking['total_price_tax_incl'] + $booking['extra_service_total_price_tax_incl']) currency=$orderCurrency['id']} + {displayPrice price=($booking['total_price_tax_incl'] + $booking['extra_service_total_price_tax_incl'] + $booking['tourism_tax_online_amount']) currency=$orderCurrency['id']}   @@ -183,10 +183,16 @@ {displayPrice price=$booking['extra_service_total_price_tax_incl'] currency=$orderCurrency['id']} + {if $booking['tourism_tax_online_amount'] > 0} +
    + + {displayPrice price=$booking['tourism_tax_online_amount'] currency=$orderCurrency['id']} +
    + {/if} - {displayPrice price=($booking['room_paid_amount'] + $booking['extra_service_total_paid_amount']) currency=$orderCurrency['id']} + {displayPrice price=($booking['room_paid_amount'] + $booking['extra_service_total_paid_amount'] + $booking['tourism_tax_paid_amount']) currency=$orderCurrency['id']}   @@ -199,6 +205,12 @@ {displayPrice price=$booking['extra_service_total_paid_amount'] currency=$orderCurrency['id']} + {if $booking['tourism_tax_paid_amount'] > 0} +
    + + {displayPrice price=$booking['tourism_tax_paid_amount'] currency=$orderCurrency['id']} +
    + {/if} {if !$isRefundCompleted} @@ -216,7 +228,7 @@ {displayPrice price=$booking['refunded_amount'] currency=$orderCurrency['id']} {else} {$orderCurrency['sign']|escape:'html':'UTF-8'} - + {l s='tax incl.' mod='hotelreservationsystem'} {/if} diff --git a/pdf/invoice.tax-tab.tpl b/pdf/invoice.tax-tab.tpl index 511d8fe54..c0cedb038 100644 --- a/pdf/invoice.tax-tab.tpl +++ b/pdf/invoice.tax-tab.tpl @@ -57,6 +57,30 @@ {foreach $tax_breakdowns as $label => $bd} {assign var=label_printed value=false} + {* Tourism tax rows have no percentage rate — render them as a fixed-amount section *} + {if $label == 'tourism_tax'} + {foreach $bd as $line} + {assign var=has_line value=true} + + {if !$label_printed} + {l s='Tourism Tax' pdf='true'} + {assign var=label_printed value=true} + {/if} + {if isset($showTaxName) && $showTaxName} + {$line.tax_name|escape:'html':'UTF-8'} + {/if} + — + {if $display_tax_bases_in_breakdowns} + — + {/if} + + {displayPrice currency=$order->id_currency price=$line.total_amount} + + + {/foreach} + {continue} + {/if} + {foreach $bd as $line} {if $line.rate == 0} {continue} diff --git a/pdf/invoice.total-tab.tpl b/pdf/invoice.total-tab.tpl index b0a42eac7..e2653c1eb 100644 --- a/pdf/invoice.total-tab.tpl +++ b/pdf/invoice.total-tab.tpl @@ -116,6 +116,26 @@ {/if} + {if isset($footer.tourism_tax_online) && $footer.tourism_tax_online > 0} + + + {l s='Tourism Tax' pdf='true'} + + + {displayPrice currency=$order->id_currency price=$footer.tourism_tax_online} + + + {/if} + {if isset($footer.tourism_tax_at_hotel) && $footer.tourism_tax_at_hotel > 0} + + + {l s='Tourism Tax (payable at hotel)' pdf='true'} + + + {displayPrice currency=$order->id_currency price=$footer.tourism_tax_at_hotel} + + + {/if} {if $footer.product_discounts_tax_incl > 0} diff --git a/themes/hotel-reservation-theme/cart-total-block.tpl b/themes/hotel-reservation-theme/cart-total-block.tpl index 4b4e1b2fd..3ad92ada5 100644 --- a/themes/hotel-reservation-theme/cart-total-block.tpl +++ b/themes/hotel-reservation-theme/cart-total-block.tpl @@ -95,6 +95,20 @@ {displayPrice price=($total_tax_without_discount)}

    {/if} + {if $show_taxes && ((!isset($tourism_tax_grossed_up) || !$tourism_tax_grossed_up) || ($use_taxes && $priceDisplay != 0))} + {if isset($tourism_tax_online) && $tourism_tax_online > 0} +

    + {l s='Tourism Tax'} + {displayPrice price=$tourism_tax_online} +

    + {/if} + {if isset($tourism_tax_at_hotel) && $tourism_tax_at_hotel > 0} +

    + {l s='Tourism Tax (payable at hotel)'} + {displayPrice price=$tourism_tax_at_hotel} +

    + {/if} + {/if}

    {if $display_tax_label} diff --git a/themes/hotel-reservation-theme/order-confirmation.tpl b/themes/hotel-reservation-theme/order-confirmation.tpl index e8cc450cc..d29fd97b6 100644 --- a/themes/hotel-reservation-theme/order-confirmation.tpl +++ b/themes/hotel-reservation-theme/order-confirmation.tpl @@ -481,6 +481,26 @@ {displayWtPriceWithCurrency price=$orderTotalInfo['total_tax_without_discount'] currency=$objOrderCurrency convert=1} + {if isset($orderTotalInfo['tourism_tax_online']) && $orderTotalInfo['tourism_tax_online'] > 0} + + + {l s='Tourism Tax'} + + + {displayWtPriceWithCurrency price=$orderTotalInfo['tourism_tax_online'] currency=$objOrderCurrency} + + + {/if} + {if isset($orderTotalInfo['tourism_tax_at_hotel']) && $orderTotalInfo['tourism_tax_at_hotel'] > 0} + + + {l s='Tourism Tax (payable at hotel)'} + + + {displayWtPriceWithCurrency price=$orderTotalInfo['tourism_tax_at_hotel'] currency=$objOrderCurrency} + + + {/if} {if $order->total_discounts > 0} diff --git a/themes/hotel-reservation-theme/order-detail.tpl b/themes/hotel-reservation-theme/order-detail.tpl index efa462dc0..558a37f31 100644 --- a/themes/hotel-reservation-theme/order-detail.tpl +++ b/themes/hotel-reservation-theme/order-detail.tpl @@ -380,6 +380,22 @@ + {if isset($tourism_tax_breakdown) && $tourism_tax_breakdown} + {foreach from=$tourism_tax_breakdown item=ttRow} + + + {$ttRow.tax_name|escape:'htmlall':'UTF-8'} + {if $ttRow.collection_type == 1} +  ({l s='payable at hotel'}) + {/if} + + + {displayWtPriceWithCurrency price=$ttRow.total_amount currency=$currency} + + + {/foreach} + {/if} + {if $order->total_discounts > 0} {l s='Total Vouchers'} From 921b02cb85f66dd7ac497b2713aab3097356fff3 Mon Sep 17 00:00:00 2001 From: shazar rahman Date: Tue, 7 Jul 2026 10:28:08 +0530 Subject: [PATCH 05/78] Added: Dynamic homepage header with image/video slider for flexible media display Co-authored-by: Name sumit201@webkul.in --- classes/controller/FrontController.php | 36 + .../classes/HotelHeaderImage.php | 289 ++++++++ .../classes/HotelHelper.php | 57 +- .../classes/HotelReservationSystemDb.php | 31 +- .../AdminHotelGeneralSettingsController.php | 54 -- .../admin/AdminHotelHeaderImageController.php | 634 ++++++++++++++++++ modules/hotelreservationsystem/define.php | 1 + .../hotelreservationsystem.php | 39 +- .../views/css/HotelReservationAdmin.css | 152 +++++ .../views/css/HotelReservationFront.css | 303 ++++++++- .../views/js/HotelHeaderImageAdmin.js | 583 ++++++++++++++++ .../views/js/HotelHeaderMediaFront.js | 166 +++++ .../hotel_header_image/helpers/view/view.tpl | 586 ++++++++++++++++ .../templates/hook/headerHotelDescBlock.tpl | 10 +- themes/hotel-reservation-theme/header.tpl | 57 +- 15 files changed, 2910 insertions(+), 88 deletions(-) create mode 100644 modules/hotelreservationsystem/classes/HotelHeaderImage.php create mode 100644 modules/hotelreservationsystem/controllers/admin/AdminHotelHeaderImageController.php create mode 100644 modules/hotelreservationsystem/views/js/HotelHeaderImageAdmin.js create mode 100644 modules/hotelreservationsystem/views/js/HotelHeaderMediaFront.js create mode 100644 modules/hotelreservationsystem/views/templates/admin/hotel_header_image/helpers/view/view.tpl diff --git a/classes/controller/FrontController.php b/classes/controller/FrontController.php index 5252400ca..daf8c31fa 100644 --- a/classes/controller/FrontController.php +++ b/classes/controller/FrontController.php @@ -1216,6 +1216,42 @@ class FrontControllerCore extends Controller 'WK_DISPLAY_PROPERTIES_LINK_IN_HEADER' => Configuration::get('WK_DISPLAY_PROPERTIES_LINK_IN_HEADER'), )); + if ($this->php_self === 'index') { + $mediaTypeInt = (int)(Configuration::get('QLO_HEADER_MEDIA_TYPE') ?: HotelHeaderImage::MEDIA_TYPE_IMAGE); + if ($mediaTypeInt === HotelHeaderImage::MEDIA_TYPE_VIDEO) { + $videoConfig = HotelHeaderImage::getVideoConfig(); + if ($videoConfig) { + $mimeMap = array('mp4' => 'video/mp4', 'webm' => 'video/webm', 'ogg' => 'video/ogg'); + if ($videoConfig['source_type'] === 'upload') { + $ext = strtolower(pathinfo($videoConfig['name'], PATHINFO_EXTENSION)); + } else { + $urlPath = parse_url($videoConfig['name'], PHP_URL_PATH); + $ext = strtolower(pathinfo($urlPath ?: '', PATHINFO_EXTENSION)); + } + $videoConfig['mime_type'] = isset($mimeMap[$ext]) ? $mimeMap[$ext] : 'video/mp4'; + $headerMediaItems = array($videoConfig); + } else { + $headerMediaItems = array(); + } + } else { + $headerMediaItems = HotelHeaderImage::getItems(); + } + $this->context->smarty->assign(array( + 'QLO_HEADER_MEDIA_TYPE' => $mediaTypeInt, + 'QLO_HEADER_MEDIA_TYPE_IMAGE' => HotelHeaderImage::MEDIA_TYPE_IMAGE, + 'QLO_HEADER_MEDIA_TYPE_VIDEO' => HotelHeaderImage::MEDIA_TYPE_VIDEO, + 'WK_HEADER_NAV_TYPE_DOTS' => HotelHeaderImage::NAV_TYPE_DOTS, + 'QLO_HEADER_ANIM_TYPE_SLIDE' => HotelHeaderImage::ANIM_TYPE_SLIDE, + 'headerMediaItems' => $headerMediaItems, + 'headerSliderConfig' => array( + 'nav_type' => (int)(Configuration::get('QLO_HEADER_SLIDER_NAV_TYPE') ?: HotelHeaderImage::NAV_TYPE_DOTS), + 'auto_play' => (int)Configuration::get('QLO_HEADER_SLIDER_AUTO_PLAY'), + 'interval' => (int)Configuration::get('QLO_HEADER_SLIDER_INTERVAL'), + 'anim_type' => (int)(Configuration::get('QLO_HEADER_SLIDER_ANIM_TYPE') ?: HotelHeaderImage::ANIM_TYPE_SLIDE), + ), + )); + } + $this->context->smarty->assign($this->initLogoAndFavicon()); } diff --git a/modules/hotelreservationsystem/classes/HotelHeaderImage.php b/modules/hotelreservationsystem/classes/HotelHeaderImage.php new file mode 100644 index 000000000..bdbd82ecc --- /dev/null +++ b/modules/hotelreservationsystem/classes/HotelHeaderImage.php @@ -0,0 +1,289 @@ + 'htl_header_image', + 'primary' => 'id_header_image', + 'multilang' => true, + 'fields' => array( + 'name' => array('type' => self::TYPE_STRING, 'size' => 512), + 'tag_line' => array('type' => self::TYPE_STRING, 'lang' => true, 'size' => 512), + 'tag_line_color' => array('type' => self::TYPE_STRING, 'validate' => 'isColor', 'size' => 7), + 'tag_line_font_size' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'), + 'tag_line_font_weight' => array('type' => self::TYPE_STRING, 'size' => 10), + 'position' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'), + 'active' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), + 'date_add' => array('type' => self::TYPE_DATE, 'validate' => 'isDate'), + 'date_upd' => array('type' => self::TYPE_DATE, 'validate' => 'isDate'), + ), + ); + + public function __construct($id = null, $id_lang = null, $id_shop = null) + { + $this->image_dir = _PS_IMG_DIR_.'hotel_header_media/'; + parent::__construct($id, $id_lang, $id_shop); + } + + /** + * @inheritdoc + */ + public function delete() + { + if (!$this->deleteImageFile() + || !parent::delete() + || !$this->cleanPositions() + ) { + return false; + } + return true; + } + + /** + * Deletes the physical image file from disk. + * + * @return bool + */ + public function deleteImageFile() + { + if (!$this->name) { + return true; + } + $filePath = $this->image_dir.$this->name; + if (file_exists($filePath) && !unlink($filePath)) { + return false; + } + return true; + } + + /** + * Fetches image rows, optionally filtered by active status. + * + * @param int|null $active 1 = active only, 0 = inactive only, null = all + * @param int|null $idLang Language id for tag_line; null = context default + * @param bool $withAllLangs true = also include tag_lines[id_lang] map (admin edit forms) + * @return array + */ + public static function getItems($active = 1, $idLang = null, $withAllLangs = false) + { + if (!$idLang) { + $idLang = (int)Context::getContext()->language->id; + } + + if (!$withAllLangs) { + $sql = 'SELECT m.*, IFNULL(ml.`tag_line`, \'\') AS `tag_line` + FROM `'._DB_PREFIX_.'htl_header_image` m + LEFT JOIN `'._DB_PREFIX_.'htl_header_image_lang` ml + ON (m.`id_header_image` = ml.`id_header_image` + AND ml.`id_lang` = '.(int)$idLang.')'; + if ($active !== null) { + $sql .= ' WHERE m.`active` = '.(int)$active; + } + $sql .= ' ORDER BY m.`position` ASC'; + return Db::getInstance()->executeS($sql) ?: array(); + } + + $sql = 'SELECT m.*, ml.`id_lang`, IFNULL(ml.`tag_line`, \'\') AS `lang_tag_line` + FROM `'._DB_PREFIX_.'htl_header_image` m + LEFT JOIN `'._DB_PREFIX_.'htl_header_image_lang` ml + ON m.`id_header_image` = ml.`id_header_image`'; + if ($active !== null) { + $sql .= ' WHERE m.`active` = '.(int)$active; + } + $sql .= ' ORDER BY m.`position` ASC, ml.`id_lang` ASC'; + + $rows = Db::getInstance()->executeS($sql); + if (!$rows) { + return array(); + } + + $itemsMap = array(); + foreach ($rows as $row) { + $id = (int)$row['id_header_image']; + if (!isset($itemsMap[$id])) { + $itemsMap[$id] = $row; + $itemsMap[$id]['tag_line'] = ''; + $itemsMap[$id]['tag_lines'] = array(); + unset($itemsMap[$id]['id_lang'], $itemsMap[$id]['lang_tag_line']); + } + if (isset($row['id_lang'])) { + $langId = (int)$row['id_lang']; + $tagLine = $row['lang_tag_line']; + $itemsMap[$id]['tag_lines'][$langId] = $tagLine; + if ($langId === (int)$idLang) { + $itemsMap[$id]['tag_line'] = $tagLine; + } + } + } + + return array_values($itemsMap); + } + + /** + * Returns the next available position value (MAX + 1). + * + * @return int + */ + public function getHigherPosition() + { + $position = Db::getInstance()->getValue( + 'SELECT MAX(`position`) FROM `'._DB_PREFIX_.'htl_header_image`' + ); + return (is_numeric($position) ? (int)$position : -1) + 1; + } + + /** + * Resequences position values to be contiguous (0-based) after a deletion. + * Uses a single CASE...WHEN UPDATE instead of N individual queries. + * + * @return bool + */ + public function cleanPositions() + { + $items = Db::getInstance()->executeS( + 'SELECT `id_header_image` FROM `'._DB_PREFIX_.'htl_header_image` ORDER BY `position` ASC' + ); + if (!$items) { + return true; + } + $cases = ''; + $ids = array(); + foreach ($items as $i => $item) { + $id = (int)$item['id_header_image']; + $cases .= ' WHEN '.$id.' THEN '.$i; + $ids[] = $id; + } + return (bool)Db::getInstance()->execute( + 'UPDATE `'._DB_PREFIX_.'htl_header_image` + SET `position` = CASE `id_header_image`'.$cases.' END + WHERE `id_header_image` IN ('.implode(',', $ids).')' + ); + } + + /** + * Returns the stored video configuration, or null when no video is set. + * + * @return array|null ['source_type' => 'upload'|'url', 'name' => string] + */ + public static function getVideoConfig() + { + $name = Configuration::get('QLO_HEADER_VIDEO_NAME'); + $sourceType = Configuration::get('QLO_HEADER_VIDEO_SOURCE_TYPE'); + if (!$sourceType || !$name) { + return null; + } + return array('source_type' => $sourceType, 'name' => $name); + } + + /** + * Persists video source type and name/url to Configuration. + * + * @param string $sourceType 'upload' or 'url' + * @param string $name Filename (upload) or full URL (url) + * @return bool + */ + public static function saveVideoConfig($sourceType, $name) + { + return Configuration::updateValue('QLO_HEADER_VIDEO_SOURCE_TYPE', pSQL($sourceType)) + && Configuration::updateValue('QLO_HEADER_VIDEO_NAME', pSQL($name)); + } + + /** + * Clears video configuration and deletes the uploaded file from disk if applicable. + * + * @return bool + */ + public static function deleteVideoConfig() + { + $sourceType = Configuration::get('QLO_HEADER_VIDEO_SOURCE_TYPE'); + $name = Configuration::get('QLO_HEADER_VIDEO_NAME'); + if ($sourceType === 'upload' && $name) { + $filePath = _PS_IMG_DIR_.'hotel_header_media/'.$name; + if (file_exists($filePath)) { + unlink($filePath); + } + } + Configuration::updateValue('QLO_HEADER_VIDEO_SOURCE_TYPE', ''); + Configuration::updateValue('QLO_HEADER_VIDEO_NAME', ''); + return true; + } + + /** + * Creates the upload directory with security controls if it does not exist. + * + * @return bool + */ + public static function createMediaDirectory() + { + $dir = _PS_IMG_DIR_.'hotel_header_media/'; + if (!is_dir($dir) && !mkdir($dir, 0755, true)) { + return false; + } + if (!file_exists($dir.'index.php') && file_exists(_PS_IMG_DIR_.'index.php')) { + copy(_PS_IMG_DIR_.'index.php', $dir.'index.php'); + } + if (!file_exists($dir.'.htaccess')) { + file_put_contents( + $dir.'.htaccess', + "Options -ExecCGI\nAddHandler cgi-script .php .php3 .php4 .php5 .phtml .pl .py .jsp .asp .cgi\nphp_flag engine off\n" + ); + } + return true; + } +} diff --git a/modules/hotelreservationsystem/classes/HotelHelper.php b/modules/hotelreservationsystem/classes/HotelHelper.php index f7f4f1c2b..0687b9c0d 100644 --- a/modules/hotelreservationsystem/classes/HotelHelper.php +++ b/modules/hotelreservationsystem/classes/HotelHelper.php @@ -1057,7 +1057,48 @@ class HotelHelper Configuration::updateValue('WK_TITLE_HEADER_BLOCK', $home_banner_default_title); Configuration::updateValue('WK_CONTENT_HEADER_BLOCK', $home_banner_default_content); - Configuration::updateValue('WK_HOTEL_HEADER_IMAGE', 'hotel_header_image.jpg'); + Configuration::updateValue('QLO_HEADER_MEDIA_TYPE', HotelHeaderImage::MEDIA_TYPE_IMAGE); + Configuration::updateValue('QLO_HEADER_SLIDER_NAV_TYPE', HotelHeaderImage::NAV_TYPE_DOTS); + Configuration::updateValue('QLO_HEADER_SLIDER_AUTO_PLAY', 1); + Configuration::updateValue('QLO_HEADER_SLIDER_INTERVAL', 5000); + Configuration::updateValue('QLO_HEADER_SLIDER_ANIM_TYPE', HotelHeaderImage::ANIM_TYPE_SLIDE); + Configuration::updateValue('QLO_HOTEL_NAME_ENABLE', 0); + Configuration::updateValue('QLO_HEADER_CONTENT_ALIGN', HotelHeaderImage::CONTENT_ALIGN_CENTER); + Configuration::updateValue('QLO_HEADER_VIDEO_SOURCE_TYPE', ''); + Configuration::updateValue('QLO_HEADER_VIDEO_NAME', ''); + HotelHeaderImage::createMediaDirectory(); + $defaultImgSrc = _PS_IMG_DIR_.'hotel_header_image.jpg'; + $defaultImgDest = 'default_hotel_header_image.jpg'; + $mediaDir = _PS_IMG_DIR_.'hotel_header_media/'; + if (file_exists($defaultImgSrc) && !file_exists($mediaDir.$defaultImgDest)) { + @copy($defaultImgSrc, $mediaDir.$defaultImgDest); + } + if (file_exists($mediaDir.$defaultImgDest)) { + Db::getInstance()->execute( + 'INSERT INTO `'._DB_PREFIX_.'htl_header_image` + (`name`, `position`, `active`, `date_add`, `date_upd`) + VALUES (\''.pSQL($defaultImgDest).'\', 0, 1, NOW(), NOW())' + ); + $newId = (int)Db::getInstance()->Insert_ID(); + if ($newId) { + $defaultTagLines = array( + 'en' => 'A place where comfort and luxury are blended with nature!', + 'nl' => 'Een plek waar comfort en luxe worden gecombineerd met de natuur!', + 'fr' => 'Un endroit où le confort et le luxe se mêlent à la nature!', + 'de' => 'Ein Ort, an dem Komfort und Luxus mit der Natur verschmelzen!', + 'ru' => 'Место, где комфорт и роскошь сочетаются с природой!', + 'es' => '¡Un lugar donde el confort y el lujo se mezclan con la naturaleza!', + ); + foreach (Language::getLanguages(false) as $lang) { + $tagLine = isset($defaultTagLines[$lang['iso_code']]) ? $defaultTagLines[$lang['iso_code']] : $defaultTagLines['en']; + Db::getInstance()->execute( + 'INSERT INTO `'._DB_PREFIX_.'htl_header_image_lang` + (`id_header_image`, `id_lang`, `tag_line`) + VALUES ('.$newId.', '.(int)$lang['id_lang'].', \''.pSQL($tagLine).'\')' + ); + } + } + } Configuration::updateValue('WK_ALLOW_ADVANCED_PAYMENT', 1); Configuration::updateValue('WK_ADVANCED_PAYMENT_GLOBAL_MIN_AMOUNT', 10); Configuration::updateValue('WK_ADVANCED_PAYMENT_INC_TAX', 1); @@ -1080,14 +1121,6 @@ class HotelHelper // lang fields $languages = Language::getLanguages(false); - $htlTagLineLang = array( - 'en' => 'A place where comfort and luxury are blended with nature!', - 'nl' => 'Een plek waar comfort en luxe worden gecombineerd met de natuur!', - 'fr' => 'Un endroit où le confort et le luxe se mêlent à la nature!', - 'de' => 'Ein Ort, an dem Komfort und Luxus mit der Natur verschmelzen!', - 'ru' => 'Место, где комфорт и роскошь сочетаются с природой!', - 'es' => '¡Un lugar donde el confort y el lujo se mezclan con la naturaleza!', - ); $htlShortDescLang = array( 'en' => 'We offer elegant rooms, gourmet dining, and attentive service for a memorable stay.', @@ -1106,25 +1139,21 @@ class HotelHelper 'es' => 'ft', ); $WK_HTL_CHAIN_NAME = array(); - $WK_HTL_TAG_LINE = array(); $WK_HTL_SHORT_DESC = array(); $defaultDimensionUnit = array(); foreach ($languages as $lang) { - if (isset($htlTagLineLang[$lang['iso_code']])) { - $WK_HTL_TAG_LINE[$lang['id_lang']] = $htlTagLineLang[$lang['iso_code']]; + if (isset($htlShortDescLang[$lang['iso_code']])) { $WK_HTL_SHORT_DESC[$lang['id_lang']] = $htlShortDescLang[$lang['iso_code']]; $WK_HTL_CHAIN_NAME[$lang['id_lang']] = $homeBannerTitleLang[$lang['iso_code']]; $defaultDimensionUnit[$lang['id_lang']] = $defaultDimensionUnitLang[$lang['iso_code']]; } else { $defaultDimensionUnit[$lang['id_lang']] = $defaultDimensionUnitLang['en']; $WK_HTL_CHAIN_NAME[$lang['id_lang']] = $homeBannerTitleLang['en']; - $WK_HTL_TAG_LINE[$lang['id_lang']] = $htlTagLineLang['en']; $WK_HTL_SHORT_DESC[$lang['id_lang']] = $htlShortDescLang['en']; } } Configuration::updateValue('WK_HTL_CHAIN_NAME', $WK_HTL_CHAIN_NAME); - Configuration::updateValue('WK_HTL_TAG_LINE', $WK_HTL_TAG_LINE); Configuration::updateValue('WK_HTL_SHORT_DESC', $WK_HTL_SHORT_DESC); Configuration::updateValue('WK_DIMENSION_UNIT', $defaultDimensionUnit); diff --git a/modules/hotelreservationsystem/classes/HotelReservationSystemDb.php b/modules/hotelreservationsystem/classes/HotelReservationSystemDb.php index ca44a2826..b3fef07db 100644 --- a/modules/hotelreservationsystem/classes/HotelReservationSystemDb.php +++ b/modules/hotelreservationsystem/classes/HotelReservationSystemDb.php @@ -513,12 +513,32 @@ class HotelReservationSystemDb PRIMARY KEY (`id_room_type_bed_type`) ) ENGINE="._MYSQL_ENGINE_." DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;", + "CREATE TABLE IF NOT EXISTS `"._DB_PREFIX_."htl_header_image` ( + `id_header_image` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `name` VARCHAR(512) NOT NULL, + `tag_line_color` VARCHAR(7) NOT NULL DEFAULT '#ffffff', + `tag_line_font_size` TINYINT(3) UNSIGNED NOT NULL DEFAULT '16', + `tag_line_font_weight` VARCHAR(10) NOT NULL DEFAULT '400', + `position` INT(10) UNSIGNED NOT NULL DEFAULT '0', + `active` TINYINT(1) UNSIGNED NOT NULL DEFAULT '1', + `date_add` DATETIME NOT NULL, + `date_upd` DATETIME NOT NULL, + PRIMARY KEY (`id_header_image`) + ) ENGINE="._MYSQL_ENGINE_." DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;", + "CREATE TABLE IF NOT EXISTS `"._DB_PREFIX_."htl_header_image_lang` ( + `id_header_image` INT(10) UNSIGNED NOT NULL, + `id_lang` INT(11) NOT NULL, + `tag_line` VARCHAR(512) NOT NULL DEFAULT '', + PRIMARY KEY (`id_header_image`, `id_lang`) + ) ENGINE="._MYSQL_ENGINE_." DEFAULT CHARSET=utf8;", + "INSERT INTO `"._DB_PREFIX_."htl_settings_link` (`id_settings_link`, `icon`, `link`, `new_window`, `position`, `unremovable`, `active`, `date_add`, `date_upd`) VALUES (1, 'icon-cogs', 'index.php?controller=AdminHotelGeneralSettings', 0, 0, 1, 1, NOW(), NOW()), (2, 'icon-dollar', 'index.php?controller=AdminHotelFeaturePricesSettings', 0, 2, 1, 1, NOW(), NOW()), (3, 'icon-plus-square', 'index.php?controller=AdminRoomTypeGlobalDemand', 0, 3, 1, 1, NOW(), NOW()), (4, 'icon-file-text', 'index.php?controller=AdminAboutHotelBlockSetting', 0, 4, 0, 1, NOW(), NOW()), - (5, 'icon-th-list', 'index.php?controller=AdminFeaturesModuleSetting', 0, 5, 0, 1, NOW(), NOW());", + (5, 'icon-th-list', 'index.php?controller=AdminFeaturesModuleSetting', 0, 5, 0, 1, NOW(), NOW()), + (6, 'icon-picture-o', 'index.php?controller=AdminHotelHeaderImage', 0, 6, 1, 1, NOW(), NOW());", "CREATE TABLE IF NOT EXISTS `"._DB_PREFIX_."htl_settings_link_lang` ( `id_settings_link` int(10) unsigned NOT NULL, @@ -552,6 +572,11 @@ 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`;", + + "INSERT INTO `"._DB_PREFIX_."htl_settings_link_lang` (`id_settings_link`, `id_lang`, `name`, `hint`) + SELECT 6, `id_lang`, 'Landing Page Header Media', 'Configure and manage header images or videos displayed on the home page.' + FROM `"._DB_PREFIX_."lang` + ORDER BY `id_lang`;", ); } @@ -615,7 +640,9 @@ 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_header_image`, + `'._DB_PREFIX_.'htl_header_image_lang`' ); } } diff --git a/modules/hotelreservationsystem/controllers/admin/AdminHotelGeneralSettingsController.php b/modules/hotelreservationsystem/controllers/admin/AdminHotelGeneralSettingsController.php index c59bd79a8..42523f12d 100644 --- a/modules/hotelreservationsystem/controllers/admin/AdminHotelGeneralSettingsController.php +++ b/modules/hotelreservationsystem/controllers/admin/AdminHotelGeneralSettingsController.php @@ -30,10 +30,6 @@ class AdminHotelGeneralSettingsController extends ModuleAdminController $this->bootstrap = true; parent::__construct(); - $psImgUrl = $this->context->link->getMediaLink(_PS_IMG_.Configuration::get('WK_HOTEL_HEADER_IMAGE')); - if ($imgExist = (bool)Tools::file_get_contents($psImgUrl)) { - $image = ''; - } $objHotelInfo = new HotelBranchInformation(); if (!$hotelsInfo = $objHotelInfo->hotelBranchesInfo(false, 1)) { $hotelsInfo = array(); @@ -136,14 +132,6 @@ class AdminHotelGeneralSettingsController extends ModuleAdminController 'validation' => 'isGenericName', 'hint' => $this->l('Enter Hotel name in case of single hotel or enter your hotels chain name in case of multiple hotels.'), ), - 'WK_HTL_TAG_LINE' => array( - 'title' => $this->l('Hotel Tag Line'), - 'type' => 'textareaLang', - 'lang' => true, - 'required' => true, - 'validation' => 'isGenericName', - 'hint' => $this->l('This will display hotel tag line in hotel page.'), - ), 'WK_HTL_SHORT_DESC' => array( 'title' => $this->l('Hotel Short Description'), 'type' => 'textareaLang', @@ -166,14 +154,6 @@ class AdminHotelGeneralSettingsController extends ModuleAdminController 'hint' => $this->l('The year when your hotel site was launched.'), 'type' => 'text', 'class' => 'fixed-width-xxl', - ), - 'WK_HTL_HEADER_IMAGE' => array( - 'title' => $this->l('Header Background Image'), - 'type' => 'file', - 'image' => $imgExist ? $image : false, - 'hint' => $this->l('This image appears as header background image on home page.'), - 'name' => 'WK_HOTEL_HEADER_IMAGE', - 'url' => _PS_IMG_, ), 'WK_DISPLAY_PROPERTIES_LINK_IN_HEADER' => array( 'title' => $this->l('Display Our Properties link in Header'), @@ -569,17 +549,6 @@ class AdminHotelGeneralSettingsController extends ModuleAdminController } } } - if (!trim(Tools::getValue('WK_HTL_TAG_LINE_'.$defaultLangId))) { - $this->errors[] = $this->l('Hotel tag line is required at least in ').$objDefaultLanguage['name']; - } else { - foreach ($languages as $lang) { - if (trim(Tools::getValue('WK_HTL_TAG_LINE_'.$lang['id_lang']))) { - if (!Validate::isGenericName(Tools::getValue('WK_HTL_TAG_LINE_'.$lang['id_lang']))) { - $this->errors[] = $this->l('Invalid Hotel tag line in ').$lang['name']; - } - } - } - } if (!trim(Tools::getValue('WK_HTL_SHORT_DESC_'.$defaultLangId))) { $this->errors[] = $this->l('Hotel short description is required at least in '). $objDefaultLanguage['name']; @@ -592,24 +561,6 @@ class AdminHotelGeneralSettingsController extends ModuleAdminController } } } - if ($_FILES['WK_HOTEL_HEADER_IMAGE']['name']) { - if ($error = ImageManager::validateUpload($_FILES['WK_HOTEL_HEADER_IMAGE'], Tools::getMaxUploadSize())) { - $this->errors[] = $error; - } - - if (!count($this->errors)) { - $file_name = 'hotel_header_image_'.time().'.jpg'; - $img_path = _PS_IMG_DIR_.$file_name; - - if (ImageManager::resize($_FILES['WK_HOTEL_HEADER_IMAGE']['tmp_name'], $img_path)) { - $olderHeaderImg = _PS_IMG_DIR_.Configuration::get('WK_HOTEL_HEADER_IMAGE'); - Configuration::updateValue('WK_HOTEL_HEADER_IMAGE', $file_name); - Tools::deleteFile($olderHeaderImg); - } else { - $this->errors[] = $this->l('Some error occured while uoploading image.Please try again.'); - } - } - } if (!Validate::isUnsignedInt(Tools::getValue('WK_ADVANCED_PAYMENT_GLOBAL_MIN_AMOUNT'))) { $this->errors[] = $this->l('Invalid minimum partial payment percentage.'); } elseif (Tools::getValue('WK_ADVANCED_PAYMENT_GLOBAL_MIN_AMOUNT') <= 0) { @@ -662,11 +613,6 @@ class AdminHotelGeneralSettingsController extends ModuleAdminController Tools::getValue('WK_HTL_CHAIN_NAME_'.$defaultLangId) ); } - if (!trim(Tools::getValue('WK_HTL_TAG_LINE_'.$lang['id_lang']))) { - $_POST['WK_HTL_TAG_LINE_'.$lang['id_lang']] = trim( - Tools::getValue('WK_HTL_TAG_LINE_'.$defaultLangId) - ); - } if (!trim(Tools::getValue('WK_HTL_SHORT_DESC_'.$lang['id_lang']))) { $_POST['WK_HTL_SHORT_DESC_'.$lang['id_lang']] = trim( Tools::getValue('WK_HTL_SHORT_DESC_'.$defaultLangId) diff --git a/modules/hotelreservationsystem/controllers/admin/AdminHotelHeaderImageController.php b/modules/hotelreservationsystem/controllers/admin/AdminHotelHeaderImageController.php new file mode 100644 index 000000000..4427c0ce3 --- /dev/null +++ b/modules/hotelreservationsystem/controllers/admin/AdminHotelHeaderImageController.php @@ -0,0 +1,634 @@ +table = 'htl_header_image'; + $this->className = 'HotelHeaderImage'; + $this->bootstrap = true; + $this->identifier = 'id_header_image'; + $this->lang = true; + parent::__construct(); + + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->l('Delete selected'), + 'confirm' => $this->l('Delete selected images? This cannot be undone.'), + 'icon' => 'icon-trash', + ), + ); + } + + public function initContent() + { + if (!$this->ajax) { + $this->display = 'view'; + } + parent::initContent(); + } + + public function initToolbar() + { + parent::initToolbar(); + $this->page_header_toolbar_title = $this->l('Header Image Configuration'); + unset($this->toolbar_btn['back']); + } + + public function renderView() + { + $this->meta_title = $this->l('Header Image Configuration'); + $mediaType = (int)Tools::getValue( + 'QLO_HEADER_MEDIA_TYPE', + (int)(Configuration::get('QLO_HEADER_MEDIA_TYPE') ?: HotelHeaderImage::MEDIA_TYPE_IMAGE) + ); + $languages = Language::getLanguages(false); + $defaultLangId = (int)Configuration::get('PS_LANG_DEFAULT'); + + $imageItems = HotelHeaderImage::getItems(null, $defaultLangId, true); + $shopId = (int)$this->context->shop->id; + foreach ($imageItems as &$item) { + $item['tag_lines_json'] = json_encode((object)$item['tag_lines']); + $srcPath = _PS_IMG_DIR_.'hotel_header_media/'.$item['name']; + $cacheName = 'htl_header_image_mini_'.(int)$item['id_header_image'].'_'.$shopId.'.jpg'; + $item['thumb'] = ImageManager::thumbnail($srcPath, $cacheName, 45, 'jpg', false); + } + unset($item); + + $videoItem = HotelHeaderImage::getVideoConfig(); + $videoMimeType = 'video/mp4'; + if ($videoItem && $videoItem['source_type'] === 'upload') { + $ext = strtolower(pathinfo($videoItem['name'], PATHINFO_EXTENSION)); + $mimeMap = array('mp4' => 'video/mp4', 'webm' => 'video/webm', 'ogg' => 'video/ogg'); + $videoMimeType = isset($mimeMap[$ext]) ? $mimeMap[$ext] : 'video/mp4'; + } + + Media::addJsDef(array( + 'qloHmCurrentIndex' => self::$currentIndex, + 'qloHmToken' => $this->token, + 'qloHmMediaType' => $mediaType, + 'qloHmMediaTypeImage' => HotelHeaderImage::MEDIA_TYPE_IMAGE, + 'qloHmMediaTypeVideo' => HotelHeaderImage::MEDIA_TYPE_VIDEO, + 'qloHmMaxUpload' => Tools::getMaxUploadSize((int)Configuration::get('PS_LIMIT_UPLOAD_IMAGE_VALUE') * 1024 * 1024), + 'qloHmMaxVideoUpload' => Tools::getMaxUploadSize(), + 'qloHmDefaultLangId' => $defaultLangId, + 'qloHmI18n' => array( + 'noFileSelected' => $this->l('Please select at least one image file.'), + 'deleteFailed' => $this->l('Delete failed.'), + 'requestFailed' => $this->l('Request failed.'), + 'imageUploadedSuccess'=> $this->l('Image uploaded successfully.'), + 'imageUpdatedSuccess' => $this->l('Image updated successfully.'), + 'uploadFailed' => $this->l('Upload failed.'), + 'updateFailed' => $this->l('Update failed.'), + 'editLabel' => $this->l('Edit'), + 'deleteImageLabel' => $this->l('Delete this image'), + 'fileTooLarge' => $this->l('File exceeds the maximum allowed upload size.'), + 'switchToVideoConfirm'=> $this->l('Switching to Video will delete all images except the first one. Continue?'), + ), + )); + + $this->tpl_view_vars = array( + 'mediaType' => $mediaType, + 'imageItems' => $imageItems, + 'videoItem' => $videoItem, + 'videoMimeType' => $videoMimeType, + 'config' => array( + 'QLO_HEADER_MEDIA_TYPE' => $mediaType, + 'QLO_HEADER_SLIDER_NAV_TYPE' => (int)Tools::getValue('QLO_HEADER_SLIDER_NAV_TYPE', (int)(Configuration::get('QLO_HEADER_SLIDER_NAV_TYPE') ?: HotelHeaderImage::NAV_TYPE_DOTS)), + 'QLO_HEADER_SLIDER_AUTO_PLAY' => (int)Tools::getValue('QLO_HEADER_SLIDER_AUTO_PLAY', (int)Configuration::get('QLO_HEADER_SLIDER_AUTO_PLAY')), + 'QLO_HEADER_SLIDER_INTERVAL' => (int)Tools::getValue('QLO_HEADER_SLIDER_INTERVAL', (int)Configuration::get('QLO_HEADER_SLIDER_INTERVAL') ?: 5000), + 'QLO_HEADER_SLIDER_ANIM_TYPE' => (int)Tools::getValue('QLO_HEADER_SLIDER_ANIM_TYPE', (int)(Configuration::get('QLO_HEADER_SLIDER_ANIM_TYPE') ?: HotelHeaderImage::ANIM_TYPE_SLIDE)), + 'QLO_HOTEL_NAME_ENABLE' => (int)Tools::getValue('QLO_HOTEL_NAME_ENABLE', (int)Configuration::get('QLO_HOTEL_NAME_ENABLE')), + 'QLO_HEADER_CONTENT_ALIGN' => (int)Tools::getValue('QLO_HEADER_CONTENT_ALIGN', (int)(Configuration::get('QLO_HEADER_CONTENT_ALIGN') ?: HotelHeaderImage::CONTENT_ALIGN_CENTER)), + ), + 'languages' => $languages, + 'defaultLangId' => $defaultLangId, + 'imgBaseUrl' => $this->context->link->getMediaLink(_PS_IMG_.'hotel_header_media/'), + 'maxUpload' => Tools::formatBytes(Tools::getMaxUploadSize()), + 'maxImageUpload' => Tools::formatBytes(Tools::getMaxUploadSize((int)Configuration::get('PS_LIMIT_UPLOAD_IMAGE_VALUE') * 1024 * 1024)), + ); + + return parent::renderView(); + } + + public function postProcess() + { + if (Tools::isSubmit('submitHeaderMedia')) { + $this->processSaveSettings(); + } + parent::postProcess(); + } + + protected function processBulkDelete() + { + $ids = Tools::getValue($this->table.'Box', array()); + $activeImages = HotelHeaderImage::getItems(1); + $activeIds = array_column($activeImages, 'id_header_image'); + $activeToDelete = array_intersect(array_map('intval', (array)$ids), array_map('intval', $activeIds)); + + if (count($activeIds) - count($activeToDelete) < 1) { + $this->errors[] = $this->l('At least one active image must remain. Please enable another image before deleting the current active image.'); + return; + } + + parent::processBulkDelete(); + } + + protected function processBulkDisableSelection() + { + $ids = Tools::getValue($this->table.'Box', array()); + $activeImages = HotelHeaderImage::getItems(1); + $activeIds = array_column($activeImages, 'id_header_image'); + $activeToDisable = array_intersect(array_map('intval', (array)$ids), array_map('intval', $activeIds)); + + if (count($activeIds) - count($activeToDisable) < 1) { + $this->errors[] = $this->l('At least one active image must remain. Please enable another image before disabling the current active image.'); + return; + } + + parent::processBulkDisableSelection(); + } + + protected function processSaveSettings() + { + $mediaType = (int)Tools::getValue('QLO_HEADER_MEDIA_TYPE', HotelHeaderImage::MEDIA_TYPE_IMAGE); + $autoPlay = (int)(bool)Tools::getValue('QLO_HEADER_SLIDER_AUTO_PLAY', 1); + $previousMediaType = (int)Configuration::get('QLO_HEADER_MEDIA_TYPE'); + + if (!in_array($mediaType, array(HotelHeaderImage::MEDIA_TYPE_IMAGE, HotelHeaderImage::MEDIA_TYPE_VIDEO))) { + $this->errors[] = $this->l('Invalid media type selected.'); + return; + } + + $existingVideo = HotelHeaderImage::getVideoConfig(); + $hasNewVideoFile = isset($_FILES['header_video_file']) && !empty($_FILES['header_video_file']['size']); + $hasNewVideoUrl = (Tools::getValue('source_type', '') === 'url' && trim(Tools::getValue('video_url', '')) !== ''); + + if ($mediaType === HotelHeaderImage::MEDIA_TYPE_VIDEO && !$existingVideo && !$hasNewVideoFile && !$hasNewVideoUrl) { + $this->errors[] = $this->l('Please upload or link a video before switching the header to Video mode.'); + return; + } + if ($mediaType === HotelHeaderImage::MEDIA_TYPE_IMAGE && !HotelHeaderImage::getItems(1)) { + $this->errors[] = $this->l('Please add at least one active image before switching the header to Image mode.'); + return; + } + if ($autoPlay) { + $interval = Tools::getValue('QLO_HEADER_SLIDER_INTERVAL', 5000); + if ((string)$interval !== '' && (!Validate::isUnsignedInt($interval) || (int)$interval < 500)) { + $this->errors[] = $this->l('Auto Slide Interval must be at least 500 milliseconds.'); + return; + } + } + + $navType = (int)Tools::getValue('QLO_HEADER_SLIDER_NAV_TYPE', HotelHeaderImage::NAV_TYPE_DOTS); + if (!in_array($navType, array(HotelHeaderImage::NAV_TYPE_DOTS, HotelHeaderImage::NAV_TYPE_ARROWS, HotelHeaderImage::NAV_TYPE_BOTH))) { + $navType = HotelHeaderImage::NAV_TYPE_DOTS; + } + $animType = (int)Tools::getValue('QLO_HEADER_SLIDER_ANIM_TYPE', HotelHeaderImage::ANIM_TYPE_SLIDE); + if (!in_array($animType, array(HotelHeaderImage::ANIM_TYPE_SLIDE, HotelHeaderImage::ANIM_TYPE_FADE, HotelHeaderImage::ANIM_TYPE_ZOOM, HotelHeaderImage::ANIM_TYPE_BLUR))) { + $animType = HotelHeaderImage::ANIM_TYPE_SLIDE; + } + + if ($mediaType === HotelHeaderImage::MEDIA_TYPE_VIDEO) { + $imagesToDrop = array(); + if ($previousMediaType === HotelHeaderImage::MEDIA_TYPE_IMAGE) { + $imagesToDrop = HotelHeaderImage::getItems(null); + array_shift($imagesToDrop); + } + if ($imagesToDrop && Tools::getValue('confirm_delete_images') !== '1') { + $this->errors[] = $this->l('Switching to Video will delete all images except the first one. Please confirm this action.'); + return; + } + + $this->processSaveVideo(); + + if (!count($this->errors)) { + foreach ($imagesToDrop as $imgData) { + $obj = new HotelHeaderImage((int)$imgData['id_header_image']); + if (Validate::isLoadedObject($obj)) { + $obj->delete(); + } + } + } + } + + $contentAlign = (int)Tools::getValue('QLO_HEADER_CONTENT_ALIGN', HotelHeaderImage::CONTENT_ALIGN_CENTER); + if (!in_array($contentAlign, array(HotelHeaderImage::CONTENT_ALIGN_LEFT, HotelHeaderImage::CONTENT_ALIGN_CENTER, HotelHeaderImage::CONTENT_ALIGN_RIGHT))) { + $contentAlign = HotelHeaderImage::CONTENT_ALIGN_CENTER; + } + + if (!count($this->errors)) { + Configuration::updateValue('QLO_HEADER_MEDIA_TYPE', $mediaType); + Configuration::updateValue('QLO_HOTEL_NAME_ENABLE', (int)(bool)Tools::getValue('QLO_HOTEL_NAME_ENABLE', 0)); + Configuration::updateValue('QLO_HEADER_CONTENT_ALIGN', $contentAlign); + Configuration::updateValue('QLO_HEADER_SLIDER_NAV_TYPE', $navType); + Configuration::updateValue('QLO_HEADER_SLIDER_AUTO_PLAY', $autoPlay); + if ($autoPlay) { + Configuration::updateValue('QLO_HEADER_SLIDER_INTERVAL', (int)Tools::getValue('QLO_HEADER_SLIDER_INTERVAL', 5000)); + Configuration::updateValue('QLO_HEADER_SLIDER_ANIM_TYPE', $animType); + } + Tools::redirectAdmin(self::$currentIndex.'&conf=4&token='.$this->token); + } + } + + protected function processSaveVideo() + { + $sourceType = Tools::getValue('source_type', 'upload'); + $videoUrl = trim(Tools::getValue('video_url', '')); + $file = isset($_FILES['header_video_file']) ? $_FILES['header_video_file'] : null; + $hasNewFile = ($file && isset($file['error']) && $file['error'] === UPLOAD_ERR_OK && $file['size'] > 0); + $hasNewUrl = ($sourceType === 'url' && $videoUrl !== ''); + + if ($sourceType === 'url' && $videoUrl === '') { + $this->errors[] = $this->l('Please enter a video URL.'); + return; + } + if ($hasNewUrl && !Validate::isAbsoluteUrl($videoUrl)) { + $this->errors[] = $this->l('Please enter a valid video URL.'); + return; + } + if ($hasNewFile) { + $allowedExts = array('mp4', 'webm', 'ogg'); + $allowedMimes = array('video/mp4', 'video/webm', 'video/ogg', 'video/x-matroska'); + $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + if (!in_array($ext, $allowedExts)) { + $this->errors[] = $this->l('Only .mp4, .webm, and .ogg video formats are allowed.'); + return; + } + // Use actual disk size — $_FILES['size'] is user-supplied and can be spoofed. + if (filesize($file['tmp_name']) > Tools::getMaxUploadSize()) { + $this->errors[] = $this->l('Video file exceeds the maximum allowed upload size.'); + return; + } + if (!function_exists('finfo_open')) { + $this->errors[] = $this->l('Server cannot verify file type. Please enable the fileinfo PHP extension.'); + return; + } + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $detectedMime = finfo_file($finfo, $file['tmp_name']); + finfo_close($finfo); + if (!in_array($detectedMime, $allowedMimes)) { + $this->errors[] = $this->l('Invalid video file type detected.'); + return; + } + } + + if ($hasNewUrl) { + HotelHeaderImage::deleteVideoConfig(); + HotelHeaderImage::saveVideoConfig('url', $videoUrl); + } elseif ($hasNewFile) { + if (!HotelHeaderImage::createMediaDirectory()) { + $this->errors[] = $this->l('Could not create the media directory.'); + return; + } + $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + do { + $uniqueName = bin2hex(random_bytes(16)).'.'.$ext; + } while (file_exists(_PS_IMG_DIR_.'hotel_header_media/'.$uniqueName)); + if (!move_uploaded_file($file['tmp_name'], _PS_IMG_DIR_.'hotel_header_media/'.$uniqueName)) { + $this->errors[] = $this->l('Failed to save video file.'); + return; + } + HotelHeaderImage::deleteVideoConfig(); + HotelHeaderImage::saveVideoConfig('upload', $uniqueName); + } + } + + public function ajaxProcessUploadImage() + { + $response = array('errors' => array(), 'success' => false); + $file = isset($_FILES['header_image_file']) ? $_FILES['header_image_file'] : null; + + if (!$file || !$file['size']) { + $response['errors'][] = $this->l('No file received.'); + $this->ajaxDie(json_encode($response)); + } + + if ($error = ImageManager::validateUpload($file, Tools::getMaxUploadSize((int)Configuration::get('PS_LIMIT_UPLOAD_IMAGE_VALUE') * 1024 * 1024))) { + $response['errors'][] = $error; + $this->ajaxDie(json_encode($response)); + } + + if (!HotelHeaderImage::createMediaDirectory()) { + $response['errors'][] = $this->l('Could not create the media directory.'); + $this->ajaxDie(json_encode($response)); + } + + $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + $isGif = ($ext === 'gif'); + $outExt = $isGif ? 'gif' : 'jpg'; + do { + $uniqueName = bin2hex(random_bytes(16)).'.'.$outExt; + } while (file_exists(_PS_IMG_DIR_.'hotel_header_media/'.$uniqueName)); + + $destPath = _PS_IMG_DIR_.'hotel_header_media/'.$uniqueName; + $saved = $isGif + ? (bool)move_uploaded_file($file['tmp_name'], $destPath) + : (bool)ImageManager::resize($file['tmp_name'], $destPath); + + if (!$saved) { + $response['errors'][] = $this->l('Failed to save image file.'); + $this->ajaxDie(json_encode($response)); + } + + $tagLineByLang = array(); + foreach (Language::getLanguages(false) as $lang) { + $tagLineByLang[$lang['id_lang']] = trim(Tools::getValue('tag_line_'.$lang['id_lang'], '')); + } + + $tagLineColor = Tools::getValue('tag_line_color', '#ffffff'); + if (!preg_match('/^#[0-9a-fA-F]{6}$/', $tagLineColor)) { + $response['errors'][] = $this->l('Invalid tag line color. Use a 6-digit hex value (e.g. #ffffff).'); + $this->ajaxDie(json_encode($response)); + } + $tagLineFontSize = (int)Tools::getValue('tag_line_font_size', 16); + if ($tagLineFontSize < 8 || $tagLineFontSize > 72) { + $response['errors'][] = $this->l('Tag line font size must be between 8 and 72 pixels.'); + $this->ajaxDie(json_encode($response)); + } + $tagLineFontWeight = Tools::getValue('tag_line_font_weight', '400'); + if (!in_array($tagLineFontWeight, array('300', '400', '600', '700'))) { + $response['errors'][] = $this->l('Invalid tag line font weight.'); + $this->ajaxDie(json_encode($response)); + } + + $objImage = new HotelHeaderImage(); + $objImage->name = $uniqueName; + $objImage->position = $objImage->getHigherPosition(); + $objImage->active = (int)(bool)Tools::getValue('active', 1); + $objImage->tag_line = $tagLineByLang; + $objImage->tag_line_color = $tagLineColor; + $objImage->tag_line_font_size = $tagLineFontSize; + $objImage->tag_line_font_weight = $tagLineFontWeight; + + if (!$objImage->save()) { + if (file_exists($destPath)) { + unlink($destPath); + } + $response['errors'][] = $this->l('Failed to save image record.'); + $this->ajaxDie(json_encode($response)); + } + + $defaultLangId = (int)Configuration::get('PS_LANG_DEFAULT'); + $response['success'] = true; + $response['id'] = (int)$objImage->id; + $response['active'] = (int)$objImage->active; + $response['imgUrl'] = $this->context->link->getMediaLink(_PS_IMG_.'hotel_header_media/'.$uniqueName); + $response['tag_line'] = isset($tagLineByLang[$defaultLangId]) ? $tagLineByLang[$defaultLangId] : ''; + $response['tag_lines_json'] = json_encode((object)$tagLineByLang); + $response['tag_line_color'] = $tagLineColor; + $response['tag_line_font_size'] = $tagLineFontSize; + $response['tag_line_font_weight'] = $tagLineFontWeight; + $this->ajaxDie(json_encode($response)); + } + + public function ajaxProcessEditImage() + { + $response = array('errors' => array(), 'success' => false); + $id = (int)Tools::getValue('id_header_image'); + + if (!$id) { + $response['errors'][] = $this->l('Invalid item ID.'); + $this->ajaxDie(json_encode($response)); + } + + $objImage = new HotelHeaderImage($id); + if (!Validate::isLoadedObject($objImage)) { + $response['errors'][] = $this->l('Image not found.'); + $this->ajaxDie(json_encode($response)); + } + + $tagLineByLang = array(); + foreach (Language::getLanguages(false) as $lang) { + $tagLineByLang[$lang['id_lang']] = trim(Tools::getValue('tag_line_'.$lang['id_lang'], '')); + } + + $activeVal = Tools::getValue('active'); + if ($activeVal !== false) { + $objImage->active = (int)(bool)$activeVal; + } + + $tagLineColor = Tools::getValue('tag_line_color', $objImage->tag_line_color); + if (!preg_match('/^#[0-9a-fA-F]{6}$/', $tagLineColor)) { + $response['errors'][] = $this->l('Invalid tag line color. Use a 6-digit hex value (e.g. #ffffff).'); + $this->ajaxDie(json_encode($response)); + } + $tagLineFontSize = (int)Tools::getValue('tag_line_font_size', $objImage->tag_line_font_size); + if ($tagLineFontSize < 8 || $tagLineFontSize > 72) { + $response['errors'][] = $this->l('Tag line font size must be between 8 and 72 pixels.'); + $this->ajaxDie(json_encode($response)); + } + $tagLineFontWeight = Tools::getValue('tag_line_font_weight', $objImage->tag_line_font_weight); + if (!in_array($tagLineFontWeight, array('300', '400', '600', '700'))) { + $response['errors'][] = $this->l('Invalid tag line font weight.'); + $this->ajaxDie(json_encode($response)); + } + + $objImage->tag_line = $tagLineByLang; + $objImage->tag_line_color = $tagLineColor; + $objImage->tag_line_font_size = $tagLineFontSize; + $objImage->tag_line_font_weight = $tagLineFontWeight; + if (!$objImage->save()) { + $response['errors'][] = $this->l('Failed to update image.'); + $this->ajaxDie(json_encode($response)); + } + + $defaultLangId = (int)Configuration::get('PS_LANG_DEFAULT'); + $response['success'] = true; + $response['active'] = (int)$objImage->active; + $response['confirmations'] = $this->l('Image updated successfully.'); + $response['tag_line'] = isset($tagLineByLang[$defaultLangId]) ? $tagLineByLang[$defaultLangId] : ''; + $response['tag_lines_json'] = json_encode((object)$tagLineByLang); + $response['tag_line_color'] = $tagLineColor; + $response['tag_line_font_size'] = $tagLineFontSize; + $response['tag_line_font_weight'] = $tagLineFontWeight; + $this->ajaxDie(json_encode($response)); + } + + public function ajaxProcessDeleteMedia() + { + $response = array('errors' => array(), 'success' => false); + $id = (int)Tools::getValue('id_header_image'); + + if (!$id) { + $response['errors'][] = $this->l('Invalid item ID.'); + $this->ajaxDie(json_encode($response)); + } + + $objImage = new HotelHeaderImage($id); + if (!Validate::isLoadedObject($objImage)) { + $response['errors'][] = $this->l('Image not found.'); + $this->ajaxDie(json_encode($response)); + } + + if ($objImage->active) { + $activeImages = HotelHeaderImage::getItems(1); + if (is_array($activeImages) && count($activeImages) <= 1) { + $response['errors'][] = $this->l('At least one image must remain active. Enable another image before deleting this one.'); + $this->ajaxDie(json_encode($response)); + } + } + + if (!$objImage->delete()) { + $response['errors'][] = $this->l('Unable to delete image.'); + $this->ajaxDie(json_encode($response)); + } + + $response['success'] = true; + $response['confirmations'] = $this->l('Image deleted successfully.'); + $this->ajaxDie(json_encode($response)); + } + + public function ajaxProcessDeleteVideo() + { + $response = array('errors' => array(), 'success' => false); + + if (!HotelHeaderImage::getVideoConfig()) { + $response['errors'][] = $this->l('No video is currently set.'); + $this->ajaxDie(json_encode($response)); + } + + HotelHeaderImage::deleteVideoConfig(); + $response['success'] = true; + $response['confirmations'] = $this->l('Video deleted successfully.'); + $this->ajaxDie(json_encode($response)); + } + + public function ajaxProcessToggleImageActive() + { + $response = array('errors' => array(), 'success' => false); + $id = (int)Tools::getValue('id_header_image'); + $active = (int)(bool)Tools::getValue('active'); + + if (!$id) { + $response['errors'][] = $this->l('Invalid item ID.'); + $this->ajaxDie(json_encode($response)); + } + + $objImage = new HotelHeaderImage($id); + if (!Validate::isLoadedObject($objImage)) { + $response['errors'][] = $this->l('Image not found.'); + $this->ajaxDie(json_encode($response)); + } + + if ($active === 0) { + $activeImages = HotelHeaderImage::getItems(1); + if (is_array($activeImages) && count($activeImages) <= 1) { + $response['errors'][] = $this->l('At least one image must remain active.'); + $this->ajaxDie(json_encode($response)); + } + } + + $objImage->active = $active; + if (!$objImage->save()) { + $response['errors'][] = $this->l('Unable to update active status.'); + $this->ajaxDie(json_encode($response)); + } + + $response['success'] = true; + $response['confirmations'] = $this->l('The status has been successfully updated.'); + $this->ajaxDie(json_encode($response)); + } + + public function ajaxProcessSaveImagePositions() + { + $ids = Tools::getValue('image_ids', array()); + if (!is_array($ids)) { + $this->ajaxDie(json_encode(array('success' => false))); + } + + $sanitizedIds = array_values(array_filter(array_map('intval', $ids), function ($id) { + return $id > 0; + })); + if (count($sanitizedIds) !== count($ids)) { + $this->ajaxDie(json_encode(array('success' => false, 'errors' => array($this->l('Invalid image IDs.'))))); + } + + if ($sanitizedIds) { + $validRows = Db::getInstance()->executeS( + 'SELECT `id_header_image` FROM `'._DB_PREFIX_.'htl_header_image` + WHERE `id_header_image` IN ('.implode(',', $sanitizedIds).')' + ); + if (!$validRows || count($validRows) !== count($sanitizedIds)) { + $this->ajaxDie(json_encode(array('success' => false, 'errors' => array($this->l('One or more image IDs are invalid.'))))); + } + } + + foreach ($sanitizedIds as $position => $id) { + Db::getInstance()->execute( + 'UPDATE `'._DB_PREFIX_.'htl_header_image` + SET `position` = '.(int)$position.' + WHERE `id_header_image` = '.(int)$id + ); + } + $this->ajaxDie(json_encode(array( + 'success' => true, + 'confirmations' => $this->l('The selected images have successfully been moved.'), + ))); + } + + public function ajaxProcessBulkUpdateTagLines() + { + $response = array('errors' => array(), 'success' => false); + $languages = Language::getLanguages(false); + $tagLineByLang = array(); + foreach ($languages as $lang) { + $tagLineByLang[$lang['id_lang']] = trim(Tools::getValue('tag_line_'.$lang['id_lang'], '')); + } + + $ids = Db::getInstance()->executeS( + 'SELECT `id_header_image` FROM `'._DB_PREFIX_.'htl_header_image`' + ); + if (!$ids) { + $response['errors'][] = $this->l('No images found.'); + $this->ajaxDie(json_encode($response)); + } + + foreach ($ids as $row) { + $objImage = new HotelHeaderImage((int)$row['id_header_image']); + $objImage->tag_line = $tagLineByLang; + $objImage->save(); + } + + $defaultLangId = (int)Configuration::get('PS_LANG_DEFAULT'); + $response['success'] = true; + $response['tag_line'] = isset($tagLineByLang[$defaultLangId]) ? $tagLineByLang[$defaultLangId] : ''; + $response['tag_lines_json'] = json_encode((object)$tagLineByLang); + $response['confirmations'] = $this->l('Tag line updated for all images.'); + $this->ajaxDie(json_encode($response)); + } + + public function setMedia() + { + parent::setMedia(); + $this->addJqueryPlugin('tablednd'); + $this->addJqueryPlugin('colorpicker'); + $this->addJS(_MODULE_DIR_.'hotelreservationsystem/views/js/HotelHeaderImageAdmin.js'); + $this->addCSS(_MODULE_DIR_.'hotelreservationsystem/views/css/HotelReservationAdmin.css'); + } +} diff --git a/modules/hotelreservationsystem/define.php b/modules/hotelreservationsystem/define.php index e8a7cc909..6dd697d66 100644 --- a/modules/hotelreservationsystem/define.php +++ b/modules/hotelreservationsystem/define.php @@ -60,6 +60,7 @@ require_once 'classes/ServiceProductOption.php'; require_once 'classes/HotelSettingsLink.php'; require_once 'classes/HotelBookingDocument.php'; +require_once 'classes/HotelHeaderImage.php'; // Web services classes require_once 'classes/WebserviceSpecificManagementHotelAri.php'; diff --git a/modules/hotelreservationsystem/hotelreservationsystem.php b/modules/hotelreservationsystem/hotelreservationsystem.php index 5246337d7..b3b5b7926 100644 --- a/modules/hotelreservationsystem/hotelreservationsystem.php +++ b/modules/hotelreservationsystem/hotelreservationsystem.php @@ -74,6 +74,9 @@ class HotelReservationSystem extends Module } } //End + if (Tools::getValue('controller') == 'index') { + $this->context->controller->addJS($this->_path.'views/js/HotelHeaderMediaFront.js'); + } $this->context->controller->addCSS($this->_path.'/views/css/HotelReservationFront.css'); $this->context->controller->addJS($this->_path.'/views/js/HotelReservationFront.js'); } @@ -325,12 +328,20 @@ class HotelReservationSystem extends Module public function hookDisplayAfterHookTop() { if (Tools::getValue('controller') == 'index') { - $this->context->smarty->assign( - array( - 'WK_HTL_CHAIN_NAME' => Configuration::get('WK_HTL_CHAIN_NAME', $this->context->language->id), - 'WK_HTL_TAG_LINE' => Configuration::get('WK_HTL_TAG_LINE', $this->context->language->id), - ) - ); + $headerMediaItems = $this->context->smarty->getTemplateVars('headerMediaItems'); + $firstItem = ($headerMediaItems && !empty($headerMediaItems[0])) ? $headerMediaItems[0] : array(); + $tagLine = !empty($firstItem['tag_line']) ? $firstItem['tag_line'] : ''; + $this->context->smarty->assign(array( + 'WK_HTL_CHAIN_NAME' => Configuration::get('WK_HTL_CHAIN_NAME', $this->context->language->id), + 'wkHeaderMediaTagLine' => $tagLine, + 'wkTagLineColor' => !empty($firstItem['tag_line_color']) ? $firstItem['tag_line_color'] : '#ffffff', + 'wkTagLineFontSize' => !empty($firstItem['tag_line_font_size']) ? (int)$firstItem['tag_line_font_size'] : 16, + 'wkTagLineFontWeight' => !empty($firstItem['tag_line_font_weight']) ? $firstItem['tag_line_font_weight'] : '400', + 'QLO_HOTEL_NAME_ENABLE' => (int)Configuration::get('QLO_HOTEL_NAME_ENABLE'), + 'wkHeaderContentAlign' => (int)(Configuration::get('QLO_HEADER_CONTENT_ALIGN') ?: HotelHeaderImage::CONTENT_ALIGN_CENTER), + 'QLO_HEADER_MEDIA_TYPE' => (int)(Configuration::get('QLO_HEADER_MEDIA_TYPE') ?: HotelHeaderImage::MEDIA_TYPE_IMAGE), + 'QLO_HEADER_MEDIA_TYPE_VIDEO' => HotelHeaderImage::MEDIA_TYPE_VIDEO, + )); return $this->display(__FILE__, 'headerHotelDescBlock.tpl'); } } @@ -486,7 +497,6 @@ class HotelReservationSystem extends Module // update configuration keys $configKeys = array( 'WK_HTL_CHAIN_NAME', - 'WK_HTL_TAG_LINE', 'WK_HTL_SHORT_DESC', ); HotelHelper::updateConfigurationLangKeys($newIdLang, $configKeys); @@ -530,6 +540,7 @@ class HotelReservationSystem extends Module $this->installTab('AdminHotelGeneralSettings', 'Hotel General Configuration', 'AdminHotelConfigurationSetting', false); $this->installTab('AdminHotelFeaturePricesSettings', 'Advanced Price Rules', 'AdminHotelConfigurationSetting', false); $this->installTab('AdminRoomTypeGlobalDemand', 'Additional Demand Configuration', 'AdminHotelConfigurationSetting', false); + $this->installTab('AdminHotelHeaderImage', 'Header Image Configuration', 'AdminHotelConfigurationSetting', false); $this->installTab('AdminBookingDocument', 'Booking Documents', false, false); return true; @@ -637,9 +648,10 @@ class HotelReservationSystem extends Module 'WK_ROOM_LEFT_WARNING_NUMBER', 'WK_HTL_ESTABLISHMENT_YEAR', 'WK_HTL_CHAIN_NAME', + 'WK_HTL_TAG_LINE', 'WK_TITLE_HEADER_BLOCK', 'WK_CONTENT_HEADER_BLOCK', - 'WK_HTL_HEADER_IMAGE', + 'WK_HOTEL_HEADER_IMAGE', 'WK_ALLOW_ADVANCED_PAYMENT', 'WK_ADVANCED_PAYMENT_GLOBAL_MIN_AMOUNT', 'WK_ADVANCED_PAYMENT_INC_TAX', @@ -648,7 +660,16 @@ class HotelReservationSystem extends Module 'WK_HOTEL_NAME_ENABLE', 'WK_CUSTOMER_SUPPORT_PHONE_NUMBER', 'WK_CUSTOMER_SUPPORT_EMAIL', - 'WK_DISPLAY_CONTACT_PAGE_HOTEL_LIST' + 'WK_DISPLAY_CONTACT_PAGE_HOTEL_LIST', + 'QLO_HEADER_MEDIA_TYPE', + 'QLO_HOTEL_NAME_ENABLE', + 'QLO_HEADER_CONTENT_ALIGN', + 'QLO_HEADER_VIDEO_SOURCE_TYPE', + 'QLO_HEADER_VIDEO_NAME', + 'QLO_HEADER_SLIDER_NAV_TYPE', + 'QLO_HEADER_SLIDER_AUTO_PLAY', + 'QLO_HEADER_SLIDER_INTERVAL', + 'QLO_HEADER_SLIDER_ANIM_TYPE', ); foreach ($configKeys as $key) { if (!Configuration::deleteByName($key)) { diff --git a/modules/hotelreservationsystem/views/css/HotelReservationAdmin.css b/modules/hotelreservationsystem/views/css/HotelReservationAdmin.css index a207087a6..5dde2666f 100644 --- a/modules/hotelreservationsystem/views/css/HotelReservationAdmin.css +++ b/modules/hotelreservationsystem/views/css/HotelReservationAdmin.css @@ -635,3 +635,155 @@ body.adminhotelroomsbooking .ui-tooltip { .error-border { outline: rgb(210, 124, 130) solid 1px; border-radius: 2px;} + +.qlo-video-preview-card { + background: #111; + border-radius: 4px; + overflow: hidden; + margin-bottom: 10px; + max-width: 480px; + display: inline-block; + vertical-align: top; + width: 100%; +} + +.qlo-video-preview-player { + display: block; + width: 100%; + max-height: 240px; + background: #000; +} + +.qlo-video-url-preview { + padding: 14px 16px; + background: #f5f5f5; + border: 1px solid #ddd; + border-radius: 4px; + margin-bottom: 0; + word-break: break-all; + font-size: 13px; + color: #444; +} + +.qlo-video-url-preview .icon-link { + margin-right: 6px; + color: #5bc0de; + font-size: 14px; +} + +.wk-inline-hint { + display: inline-block; + margin: 6px 0 0 10px; + font-size: 12px; + vertical-align: middle; +} + +@keyframes wkSlideInLeft { + from { + opacity: 0; + transform: translateX(-16px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.qlo-anim-slide-left { + animation: wkSlideInLeft 0.22s ease forwards; +} + +.qlo-img-thumb { + width: 120px; + height: 75px; + -o-object-fit: cover; + object-fit: cover; + display: block; +} + +#qlo-image-table tbody tr.qlo-img-row:hover { + background-color: #f9f9f9; +} + +.qlo-img-tagline-cell { + max-width: 220px; + color: #555; + vertical-align: middle !important; +} + +.wk-row-placeholder { + background: #eef6ff !important; + border: 1px dashed #99c0e0 !important; +} + +.wk-row-placeholder td { + padding: 0 !important; + height: 85px; +} + + +/* ---- File list in add-image form ---- */ +.qlo-files-list { + list-style: none; + padding: 0; + margin: 8px 0 0; + max-width: 520px; +} +.qlo-files-list li { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + border: 1px solid #e0e0e0; + border-radius: 3px; + margin-bottom: 5px; + background: #f9f9f9; + font-size: 13px; +} +.qlo-files-list li:last-child { + margin-bottom: 0; +} +.qlo-files-list .qlo-file-name { + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-weight: 500; + color: #333; +} +.qlo-files-list .qlo-file-size { + font-size: 11px; + color: #999; + white-space: nowrap; + flex-shrink: 0; +} +.qlo-files-list .qlo-remove-file { + flex-shrink: 0; + padding: 3px 8px; + line-height: 1; + font-size: 13px; + color: #999; + border-color: #ccc; + background: #fff; +} +.qlo-files-list .qlo-remove-file:hover, +.qlo-files-list .qlo-remove-file:focus { + color: #c0392b; + border-color: #c0392b; + background: #fff5f5; +} + +/* ---- Edit preview image in form ---- */ +#qlo-img-edit-thumb { + max-width: 160px; + max-height: 100px; + object-fit: cover; + border-radius: 3px; +} + +/* ---- Inline multilingual input (inside input-group-btn) ---- */ +.wk-inline-lang-input { + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; +} diff --git a/modules/hotelreservationsystem/views/css/HotelReservationFront.css b/modules/hotelreservationsystem/views/css/HotelReservationFront.css index b268f77b8..4a81c829d 100644 --- a/modules/hotelreservationsystem/views/css/HotelReservationFront.css +++ b/modules/hotelreservationsystem/views/css/HotelReservationFront.css @@ -1,3 +1,69 @@ +/** +* 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 +*/ + +.header-ext-dots { + position: absolute; + bottom: 14px; + left: 0; + right: 0; + text-align: center; + z-index: 15; + pointer-events: none; +} +.header-ext-dots .owl-dot { + display: inline-block; + margin: 0 5px; + cursor: pointer; + pointer-events: all; +} +.header-ext-dots .owl-dot span { + display: block; + width: 10px; + height: 10px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.5); + border: 1px solid rgba(255, 255, 255, 0.8); + transition: background 0.25s ease; +} +.header-ext-dots .owl-dot.active span, +.header-ext-dots .owl-dot:hover span { + background: #ffffff; +} + +/* ===== Content alignment (1=left, 2=center, 3=right) ===== */ +.header-align-1 { text-align: left; } +.header-align-2 { text-align: center; } +.header-align-3 { text-align: right; } + +.header-align-1 .heasder-desc-hr-first, +.header-align-1 .heasder-desc-hr-second { margin-left: 0; margin-right: auto; } +.header-align-3 .heasder-desc-hr-first, +.header-align-3 .heasder-desc-hr-second { margin-left: auto; margin-right: 0; } + +@media (max-width: 767px) { + .header-align-1, + .header-align-3 { text-align: center; } +} + /* ===== headerHotelDescBlock.tpl ===== */ .header-desc-container { bottom: 0; @@ -48,13 +114,244 @@ } .heasder-desc-hr-second { - width: 190px; - margin-top: 30px; + display: none; } @media (max-width: 767px) { .topSubSecondaryBlock { - margin-top: 0px; } } + margin-top: 0px; } + +/* ===== Mobile header responsive ===== */ + +/* + * The index page sets height:100% via inline style on html/body/#page/.header-container/

    . + * On mobile portrait this makes the header fill the full viewport (~750px+), causing + * landscape hotel images to be cropped and zoomed beyond recognition. + * Reset ancestors to auto and lock the header to a landscape-ish fixed height. + */ +html, body { + height: auto !important; +} +#page, .header-container { + height: auto !important; +} +body#index #header { + height: 60vw !important; + min-height: 240px !important; + max-height: 420px !important; +} + +/* Force the full owl carousel height chain — owl JS sets inline height on + stage/items during responsive recalc, collapsing the background images. */ +body#index .header-media-layer, +body#index .header-media-layer .owl-carousel, +body#index .header-media-layer .owl-stage-outer, +body#index .header-media-layer .owl-stage, +body#index .header-media-layer .owl-item { + height: 100% !important; +} + +/* Keep background images filling the now-landscape slot */ +body#index .header-media-layer, +body#index .header-slide-img { + background-size: cover !important; + background-position: center center !important; +} + +/* Scale down oversized text */ +.header-hotel-name { + font-size: 22px !important; + margin-top: 0; + margin-bottom: 8px; +} +.js-header-tagline { + font-size: 13px !important; + line-height: 18px; +} +.header-desc-inner-wrapper { + padding-left: 10px; + padding-right: 10px; +} +/* Prev/next buttons — touch-friendly on mobile */ +.header-media-btn { + padding: 5px 9px; + font-size: 15px; +} +.js-header-media-prev { left: 6px; } +.js-header-media-next { right: 6px; } + +/* Dots clear from bottom edge */ +.header-ext-dots { + bottom: 10px; +} +} + +/* ==== Header Media (front-end) ==== */ + +#header { + background: transparent; +} + +body#index #header { + position: relative; +} + +/* Owl carousel slide animation classes */ +.wkFadeOut { animation: wkFadeOut 0.7s forwards; } +.wkFadeIn { animation: wkFadeIn 0.7s forwards; } +.wkZoomOut { animation: wkZoomOut 0.7s forwards; } +.wkZoomIn { animation: wkZoomIn 0.7s forwards; } +.wkBlurOut { animation: wkBlurOut 0.7s forwards; } +.wkBlurIn { animation: wkBlurIn 0.7s forwards; } +.wkSlideOut { animation: wkSlideOut 0.7s forwards; } +.wkSlideIn { animation: wkSlideIn 0.7s forwards; } + +@keyframes wkFadeOut { from { opacity:1; } to { opacity:0; } } +@keyframes wkFadeIn { from { opacity:0; } to { opacity:1; } } +@keyframes wkZoomOut { from { opacity:1; transform:scale(1); } to { opacity:0; transform:scale(1.15); } } +@keyframes wkZoomIn { from { opacity:0; transform:scale(0.88); } to { opacity:1; transform:scale(1); } } +@keyframes wkBlurOut { from { opacity:1; filter:blur(0); } to { opacity:0; filter:blur(10px); } } +@keyframes wkBlurIn { from { opacity:0; filter:blur(10px); } to { opacity:1; filter:blur(0); } } +@keyframes wkSlideOut { from { opacity:1; transform:translateX(0); } to { opacity:0; transform:translateX(-60px); } } +@keyframes wkSlideIn { from { opacity:0; transform:translateX(60px); } to { opacity:1; transform:translateX(0); } } + +/* Tag-line slide animation on slide change */ +.js-header-tagline { + transition: opacity 0.4s ease, transform 0.4s ease; +} +.js-header-tagline.wk-tagline-out { + opacity: 0; + transform: translateY(-10px); +} +.js-header-tagline.wk-tagline-in { + opacity: 1; + transform: translateY(0); +} + +.header-media-layer { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -11; + overflow: hidden; + background-size: cover; + background-position: center center; + background-repeat: no-repeat; +} + +.header-media-layer::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.28); + z-index: 1; + pointer-events: none; +} + +.header-media-layer .owl-carousel, +.header-media-layer .owl-stage-outer, +.header-media-layer .owl-stage, +.header-media-layer .owl-item { + height: 100%; +} + +.header-slide-img { + width: 100%; + height: 100%; + background-size: cover; + background-position: center center; + background-repeat: no-repeat; +} + +.header-video-layer { + background-color: #000; +} + +.header-bg-video { + position: absolute; + top: 50%; + left: 50%; + min-width: 100%; + min-height: 100%; + width: auto; + height: auto; + transform: translate(-50%, -50%); + object-fit: cover; +} + +.header-slide-video { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; +} + +.header-slide-video .header-bg-video { + position: absolute; + top: 50%; + left: 50%; + min-width: 100%; + min-height: 100%; + width: auto; + height: auto; + transform: translate(-50%, -50%); + object-fit: cover; +} + +.header-media-nav-wrapper { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1; + pointer-events: none; +} + +@media (min-width: 768px) { + .header-media-nav-wrapper { + z-index: 11; + } + /* Push dots above the search panel (which sits at bottom:0, ~90px tall) */ + .header-ext-dots { + bottom: 120px; + } +} + +.header-media-btn { + position: absolute; + top: 50%; + transform: translateY(-50%); + pointer-events: all; + background: rgba(0, 0, 0, 0.4); + color: #fff; + border: none; + border-radius: 2px; + padding: 8px 14px; + font-size: 20px; + line-height: 1; + cursor: pointer; + transition: background 0.2s; + outline: none; +} + +.header-media-btn:hover { + background: rgba(0, 0, 0, 0.65); + color: #fff; +} + +.js-header-media-prev { + left: 15px; +} + +.js-header-media-next { + right: 15px; +} /* ===== headerHotelDescBlock.tpl ===== */ diff --git a/modules/hotelreservationsystem/views/js/HotelHeaderImageAdmin.js b/modules/hotelreservationsystem/views/js/HotelHeaderImageAdmin.js new file mode 100644 index 000000000..07c69a935 --- /dev/null +++ b/modules/hotelreservationsystem/views/js/HotelHeaderImageAdmin.js @@ -0,0 +1,583 @@ +/** +* 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 +*/ + +$(document).ready(function () { + + $('#qlo-media-type').on('change', function () { + var type = parseInt($(this).val(), 10); + if (type === qloHmMediaTypeImage) { + $('#qlo-video-settings').hide(); + $('#qlo-image-settings, #qlo-image-panel, #qlo-slider-info').show(); + closeImgForm(); + } else { + closeImgForm(); + $('#qlo-image-settings, #qlo-image-panel, #qlo-slider-info').hide(); + $('#qlo-video-settings').show(); + } + }); + + $('#qlo-header-media-form').on('submit', function (e) { + var newType = parseInt($('#qlo-media-type').val(), 10); + if (newType === qloHmMediaTypeVideo && qloHmMediaType === qloHmMediaTypeImage) { + var imageCount = $('#qlo-image-tbody .qlo-img-row').length; + if (imageCount > 1) { + if (!confirm(qloHmI18n.switchToVideoConfirm)) { + e.preventDefault(); + return false; + } + $('#qlo-confirm-delete-images').val('1'); + } + } + }); + + $(document).on('change', 'input[name="QLO_HEADER_SLIDER_AUTO_PLAY"]', function () { + if ($(this).val() === '1') { + $('#qlo-slide-interval-group, #qlo-slide-anim-group').show(); + } else { + $('#qlo-slide-interval-group, #qlo-slide-anim-group').hide(); + } + }); + + $(document).on('change', '#qlo-img-form-file', function () { + renderFileListHm(this.files); + }); + + $(document).on('click', '.qlo-remove-file', function () { + var removeIdx = parseInt($(this).data('index'), 10); + var fileInput = document.getElementById('qlo-img-form-file'); + if (!fileInput || !fileInput.files) { return; } + try { + var dt = new DataTransfer(); + for (var i = 0; i < fileInput.files.length; i++) { + if (i !== removeIdx) { dt.items.add(fileInput.files[i]); } + } + fileInput.files = dt.files; + } catch (_e) { + } + renderFileListHm(fileInput.files); + }); + + function renderFileListHm(files) { + var $list = $('#qlo-img-files-list').empty(); + if (!files || !files.length) { $list.hide(); return; } + $list.show(); + for (var i = 0; i < files.length; i++) { + (function (idx, file) { + var size = file.size < 1048576 + ? (file.size / 1024).toFixed(1) + ' KB' + : (file.size / 1048576).toFixed(1) + ' MB'; + var $li = $( + '
  • ' + + '' + escapeHtmlHm(file.name) + '' + + '(' + size + ')' + + '' + + '
  • ' + ); + $list.append($li); + })(i, files[i]); + } + } + + hideOtherLanguage(qloHmDefaultLangId); + + $(document).on('change', '#qlo-source-type', function () { + if ($(this).val() === 'url') { + $('#qlo-vid-file-wrap').hide(); + $('#qlo-vid-url-wrap').show(); + $('#qlo-current-video-wrap').hide(); + } else { + $('#qlo-vid-file-wrap').show(); + $('#qlo-vid-url-wrap').hide(); + if ($('#qlo-current-video-wrap').length) { + $('#qlo-current-video-wrap').show(); + } + } + }); + + $(document).on('change', '#qlo-video-file-input', function () { + var file = this.files && this.files[0] ? this.files[0] : null; + var $hint = $('.qlo-vid-filename'); + if (file && typeof qloHmMaxVideoUpload !== 'undefined' && qloHmMaxVideoUpload > 0 && file.size > qloHmMaxVideoUpload) { + this.value = ''; + $hint.hide(); + showErrorMessage(qloHmI18n.fileTooLarge); + return; + } + if (file) { + $hint.text(file.name).show(); + } else { + $hint.hide(); + } + }); + + $(document).on('click', '#qlo-vid-file-add-btn', function () { + $('#qlo-video-file-input').trigger('click'); + }); + + $('#qlo-add-image-btn').on('click', function () { + openImgForm('add'); + }); + + $('#qlo-img-file-add-btn').on('click', function () { + $('#qlo-img-form-file').trigger('click'); + }); + + $(document).on('click', '.qlo-edit-img', function () { + var $row = $(this).closest('tr.qlo-img-row'); + var id = parseInt($row.data('id'), 10); + var tagLines = $row.data('tag-lines') || {}; + var isActive = $row.find('.list-action-enable').hasClass('action-enabled'); + var thumbSrc = $row.find('img.qlo-img-thumb').attr('src') || ''; + var tlColor = $row.attr('data-tag-line-color') || '#ffffff'; + var tlFontSize = parseInt($row.attr('data-tag-line-font-size') || 16, 10); + var tlFontWeight = $row.attr('data-tag-line-font-weight') || '400'; + openImgForm('edit', id, tagLines, isActive, thumbSrc, tlColor, tlFontSize, tlFontWeight); + }); + + $('#qlo-bulk-tagline-apply').on('click', function () { + var tagLines = {}; + $('.qlo-bulk-tagline-field').each(function () { + tagLines[$(this).data('lang')] = $.trim($(this).val()); + }); + var postData = { ajax: 1, action: 'bulkUpdateTagLines', token: qloHmToken }; + $.each(tagLines, function (langId, val) { + postData['tag_line_' + langId] = val; + }); + var $btn = $(this).prop('disabled', true); + $.ajax({ + url: qloHmCurrentIndex, + type: 'POST', + data: postData, + success: function (raw) { + $btn.prop('disabled', false); + var data = safeParseJsonHm(raw); + if (data && data.success) { + var updatedTagLines = safeParseJsonHm(data.tag_lines_json) || tagLines; + $('#qlo-image-tbody .qlo-img-row').each(function () { + $(this).attr('data-tag-lines', data.tag_lines_json); + $(this).data('tag-lines', updatedTagLines); + var display = data.tag_line || ''; + $(this).find('.qlo-img-tagline-cell').text( + display.length > 50 ? display.substring(0, 50) + '...' : (display || '—') + ); + }); + showSuccessMessage(data.confirmations); + } else { + showErrorMessage(data ? data.errors.join('
    ') : qloHmI18n.updateFailed); + } + }, + error: function () { $btn.prop('disabled', false); showErrorMessage(qloHmI18n.requestFailed); } + }); + }); + + $('#qlo-img-form-upload-btn').on('click', function () { + var fileInput = document.getElementById('qlo-img-form-file'); + var files = fileInput ? Array.prototype.slice.call(fileInput.files) : []; + if (!files.length) { + showErrorMessage((typeof qloHmI18n !== 'undefined') ? qloHmI18n.noFileSelected : 'Please select at least one image file.'); + return; + } + uploadImagesWithTagLine(files); + }); + + $('#qlo-img-form-save-btn').on('click', function () { + var id = parseInt($.trim($('#qlo-img-form-id').val()), 10); + if (id) { + saveEditedImage(id); + } + }); + + $(document).on('click', '.qlo-img-row .list-action-enable', function (e) { + e.preventDefault(); + var $link = $(this); + var $row = $link.closest('tr.qlo-img-row'); + var id = parseInt($row.data('id'), 10); + var active = $link.hasClass('action-enabled') ? 0 : 1; + $.ajax({ + url: qloHmCurrentIndex, + type: 'POST', + data: { ajax: 1, action: 'toggle_image_active', id_header_image: id, active: active, token: qloHmToken }, + success: function (raw) { + var data = safeParseJsonHm(raw); + if (data && data.success) { + $link.toggleClass('action-enabled action-disabled'); + $link.find('i').toggleClass('hidden'); + showSuccessMessage(data.confirmations); + } else { + showErrorMessage(data ? data.errors.join('
    ') : qloHmI18n.updateFailed); + } + }, + error: function () { showErrorMessage(qloHmI18n.requestFailed); } + }); + }); + + $(document).on('click', '.qlo-delete-img', function () { + var id = parseInt($(this).data('id'), 10); + $.ajax({ + url: qloHmCurrentIndex, + type: 'POST', + data: { ajax: 1, action: 'deleteMedia', id_header_image: id, token: qloHmToken }, + success: function (raw) { + var data = safeParseJsonHm(raw); + if (data && data.success) { + var $row = $('#qlo-image-tbody .qlo-img-row[data-id="' + id + '"]'); + if (parseInt($('#qlo-img-form-id').val(), 10) === id) { + closeImgForm(); + } + $row.fadeOut(250, function () { + $row.remove(); + var count = $('#qlo-image-tbody .qlo-img-row').length; + $('#qlo-image-count').text(count); + if (!count) { + $('#qlo-no-images').show(); + } + updateBulkActionsVisibility(); + }); + showSuccessMessage(data.confirmations || qloHmI18n.deleteFailed); + } else { + showErrorMessage(data ? data.errors.join('
    ') : qloHmI18n.deleteFailed); + } + }, + error: function () { showErrorMessage(qloHmI18n.requestFailed); } + }); + }); + + if ($('#qlo-image-table').length) { + var _hmDragOriginalOrder = null; + $('#qlo-image-table').tableDnD({ + dragHandle: 'dragHandle', + onDragClass: 'myDragClass', + onDragStart: function (table) { + _hmDragOriginalOrder = []; + $(table).find('tbody tr.qlo-img-row').each(function () { + _hmDragOriginalOrder.push(parseInt($(this).data('id'), 10)); + }); + }, + onDrop: function (table) { + var ids = []; + $(table).find('tbody tr.qlo-img-row').each(function (i) { + var id = parseInt($(this).data('id'), 10); + ids.push(id); + $(this).find('.positions').text(i + 1); + }); + if (_hmDragOriginalOrder && JSON.stringify(ids) === JSON.stringify(_hmDragOriginalOrder)) { + return; + } + $.post(qloHmCurrentIndex, { + ajax: 1, + action: 'saveImagePositions', + image_ids: ids, + token: qloHmToken + }, function (raw) { + var data = safeParseJsonHm(raw); + if (data && data.success) { + showSuccessMessage(data.confirmations); + } + }); + } + }); + } + + function openImgForm(mode, id, tagLines, isActive, imgUrl, tlColor, tlFontSize, tlFontWeight) { + $('#qlo-img-form-id').val(id || ''); + $('#qlo-form-upload-progress').hide(); + + if (mode === 'edit') { + $('#qlo-img-modal-title-add').hide(); + $('#qlo-img-modal-title-edit').show(); + $('#qlo-img-add-footer').hide(); + $('#qlo-img-edit-footer').show(); + $('#qlo-img-form-file-group').hide(); + $('#qlo-img-files-list').hide().empty(); + $('#qlo-img-form-add-active-group').hide(); + $('#qlo-img-form-edit-group').show(); + if (isActive) { + $('#qlo_img_active_edit_on').prop('checked', true); + } else { + $('#qlo_img_active_edit_off').prop('checked', true); + } + tagLines = tagLines || {}; + $('.qlo-form-tagline-field').each(function () { + var langId = $(this).data('lang'); + $(this).val(tagLines[langId] || ''); + }); + if (imgUrl) { + $('#qlo-img-edit-thumb').attr('src', imgUrl); + $('#qlo-img-edit-preview-group').show(); + } else { + $('#qlo-img-edit-preview-group').hide(); + } + $('#qlo-img-tl-color').val(tlColor || '#ffffff'); + $('#qlo-img-tl-font-size').val(tlFontSize || 16); + var fw = tlFontWeight || '400'; + $('#qlo-img-tl-font-weight').val(fw); + } else { + $('#qlo-img-modal-title-edit').hide(); + $('#qlo-img-modal-title-add').show(); + $('#qlo-img-edit-footer').hide(); + $('#qlo-img-add-footer').show(); + $('#qlo-img-form-file-group').show(); + $('#qlo-img-form-add-active-group').show(); + $('#qlo-img-form-edit-group').hide(); + $('#qlo-img-edit-preview-group').hide(); + var fileInput = document.getElementById('qlo-img-form-file'); + if (fileInput) { + fileInput.value = ''; + } + $('#qlo-img-files-list').hide().empty(); + $('#qlo_img_active_add_on').prop('checked', true); + $('.qlo-form-tagline-field').val(''); + $('#qlo-img-tl-color').val('#ffffff'); + $('#qlo-img-tl-font-size').val(16); + $('#qlo-img-tl-font-weight').val('400'); + } + + $('#qlo-img-tl-color').trigger('keyup'); + + hideOtherLanguage(id_language); + + $('#qlo-img-form-modal').modal('show'); + } + + function closeImgForm() { + $('#qlo-img-form-modal').modal('hide'); + } + + function getFormTagLines() { + var tagLines = {}; + $('.qlo-form-tagline-field').each(function () { + tagLines[$(this).data('lang')] = $.trim($(this).val()); + }); + return tagLines; + } + + function saveEditedImage(id) { + var tagLines = getFormTagLines(); + var active = parseInt($('input[name="qlo_img_active_edit"]:checked').val() || 0, 10); + var tlColor = $('#qlo-img-tl-color').val() || '#ffffff'; + var tlFontSize = parseInt($('#qlo-img-tl-font-size').val() || 16, 10); + var tlFontWeight = $('#qlo-img-tl-font-weight').val() || '400'; + var postData = { + ajax: 1, action: 'edit_image', id_header_image: id, active: active, token: qloHmToken, + tag_line_color: tlColor, tag_line_font_size: tlFontSize, tag_line_font_weight: tlFontWeight + }; + $.each(tagLines, function (langId, val) { + postData['tag_line_' + langId] = val; + }); + + $.ajax({ + url: qloHmCurrentIndex, + type: 'POST', + data: postData, + success: function (raw) { + var resp = safeParseJsonHm(raw); + if (resp && resp.success) { + var $row = $('#qlo-image-tbody .qlo-img-row[data-id="' + id + '"]'); + var updatedTagLines = safeParseJsonHm(resp.tag_lines_json) || tagLines; + $row.attr('data-tag-lines', resp.tag_lines_json); + $row.data('tag-lines', updatedTagLines); + $row.attr('data-tag-line-color', resp.tag_line_color || '#ffffff'); + $row.attr('data-tag-line-font-size', resp.tag_line_font_size || 16); + $row.attr('data-tag-line-font-weight', resp.tag_line_font_weight || '400'); + var display = resp.tag_line || ''; + $row.find('.qlo-img-tagline-cell').text( + display.length > 50 ? display.substring(0, 50) + '...' : (display || '—') + ); + var nowActive = parseInt(resp.active, 10); + var $toggle = $row.find('.list-action-enable'); + $toggle.toggleClass('action-enabled', !!nowActive) + .toggleClass('action-disabled', !nowActive); + $toggle.find('i.icon-check').toggleClass('hidden', !nowActive); + $toggle.find('i.icon-remove').toggleClass('hidden', !!nowActive); + $toggle.attr('href', $toggle.attr('href').replace(/id_header_image=\d+/, 'id_header_image=' + id) + .replace(/active=\d+/, 'active=' + (nowActive ? 0 : 1))); + closeImgForm(); + showSuccessMessage(resp.confirmations || qloHmI18n.imageUpdatedSuccess); + } else { + showErrorMessage(resp ? resp.errors.join('
    ') : qloHmI18n.updateFailed); + } + }, + error: function () { showErrorMessage(qloHmI18n.requestFailed); } + }); + } + + function uploadImagesWithTagLine(files) { + var index = 0; + var tagLines = getFormTagLines(); + var active = parseInt($('input[name="qlo_img_active_add"]:checked').val() || 1, 10); + var tlColor = $('#qlo-img-tl-color').val() || '#ffffff'; + var tlFontSize = parseInt($('#qlo-img-tl-font-size').val() || 16, 10); + var tlFontWeight = $('#qlo-img-tl-font-weight').val() || '400'; + $('#qlo-form-upload-progress').show(); + $('#qlo-img-form-upload-btn').prop('disabled', true); + + function next() { + if (index >= files.length) { + $('#qlo-form-upload-progress').hide(); + $('#qlo-img-form-upload-btn').prop('disabled', false); + closeImgForm(); + return; + } + var file = files[index++]; + if (typeof qloHmMaxUpload !== 'undefined' && qloHmMaxUpload > 0 && file.size > qloHmMaxUpload) { + showErrorMessage(qloHmI18n.fileTooLarge); + next(); + return; + } + var fd = new FormData(); + fd.append('header_image_file', file); + fd.append('ajax', '1'); + fd.append('action', 'uploadImage'); + fd.append('token', qloHmToken); + fd.append('active', active); + fd.append('tag_line_color', tlColor); + fd.append('tag_line_font_size', tlFontSize); + fd.append('tag_line_font_weight', tlFontWeight); + $.each(tagLines, function (langId, val) { + fd.append('tag_line_' + langId, val); + }); + + $.ajax({ + url: qloHmCurrentIndex, + type: 'POST', + data: fd, + processData: false, + contentType: false, + success: function (raw) { + var resp = safeParseJsonHm(raw); + if (resp && resp.success) { + var uploadedTagLines = safeParseJsonHm(resp.tag_lines_json) || tagLines; + appendImageRow( + resp.id, resp.imgUrl, resp.tag_line || '', uploadedTagLines, resp.tag_lines_json || '{}', + parseInt(resp.active, 10), + resp.tag_line_color || '#ffffff', + resp.tag_line_font_size || 16, + resp.tag_line_font_weight || '400' + ); + showSuccessMessage(qloHmI18n.imageUploadedSuccess); + } else { + showErrorMessage(file.name + ': ' + (resp ? resp.errors.join(', ') : qloHmI18n.uploadFailed)); + } + next(); + }, + error: function () { + showErrorMessage(file.name + ': ' + qloHmI18n.requestFailed); + next(); + } + }); + } + next(); + } + + function appendImageRow(id, imgUrl, tagLineDisplay, tagLines, tagLinesJson, active, tlColor, tlFontSize, tlFontWeight) { + id = parseInt(id, 10); + active = active !== undefined ? !!active : true; + tlColor = tlColor || '#ffffff'; + tlFontSize = tlFontSize || 16; + tlFontWeight = tlFontWeight || '400'; + var display = tagLineDisplay || ''; + var cellText = display.length > 50 ? display.substring(0, 50) + '...' : (display || '—'); + var pos = parseInt($('#qlo-image-count').text(), 10) + 1; + var nextActive = active ? 0 : 1; + var toggleUrl = qloHmCurrentIndex + '&ajax=1&action=toggle_image_active&id_header_image=' + id + '&active=' + nextActive + '&token=' + qloHmToken; + var toggleClass = active ? 'action-enabled' : 'action-disabled'; + var checkHidden = active ? '' : ' hidden'; + var removeHidden = active ? ' hidden' : ''; + + var $row = $( + '' + + '' + + '' + + '' + escapeHtmlHm(cellText) + '' + + '' + + '
    ' + pos + '
    ' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
    ' + + '
    ' + + '' + + '' + + '' + + '
    ' + + '
    ' + + '' + + '' + ); + $row.data('tag-lines', tagLines); + + $('#qlo-image-tbody').append($row); + $('#qlo-image-table').tableDnDUpdate(); + $('#qlo-no-images').hide(); + $('#qlo-image-count').text(parseInt($('#qlo-image-count').text(), 10) + 1); + updateBulkActionsVisibility(); + } + + function updateBulkActionsVisibility() { + var count = $('#qlo-image-tbody .qlo-img-row').length; + $('#qlo-bulk-actions-row').toggle(count > 1); + } + + function safeParseJsonHm(raw) { + try { return JSON.parse(raw); } catch (_e) { return null; } + } + + function escapeHtmlHm(str) { + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + } + + function escapeAttrHm(str) { + return String(str) + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(//g, '>'); + } + + +}); diff --git a/modules/hotelreservationsystem/views/js/HotelHeaderMediaFront.js b/modules/hotelreservationsystem/views/js/HotelHeaderMediaFront.js new file mode 100644 index 000000000..fac729d23 --- /dev/null +++ b/modules/hotelreservationsystem/views/js/HotelHeaderMediaFront.js @@ -0,0 +1,166 @@ +/** +* 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 +*/ + + +$(document).ready(function () { + + /* Background video autoplay fallback (handles strict browser autoplay policies) */ + var $bgVideo = $('video.header-bg-video'); + if ($bgVideo.length) { + $bgVideo.each(function () { + var vid = this; + vid.muted = true; + var p = vid.play(); + if (p !== undefined) { + p.catch(function () {}); + } + }); + } + + var $imgCarousel = $('.header-img-carousel'); + if (!$imgCarousel.length) { return; } + + var autoPlay = parseInt($imgCarousel.data('auto-play'), 10) === 1; + var interval = parseInt($imgCarousel.data('interval'), 10) || 5000; + var navType = parseInt($imgCarousel.data('nav-type'), 10) || 1; // 1=dots 2=arrows 3=both + var animType = parseInt($imgCarousel.data('anim-type'), 10) || 1; // 1=slide 2=fade 3=zoom 4=blur + var animNames = {1: 'Slide', 2: 'Fade', 3: 'Zoom', 4: 'Blur'}; + var animLabel = animNames[animType] || 'Slide'; + + var showDots = (navType === 1 || navType === 3); + $imgCarousel.owlCarousel({ + loop: true, + items: 1, + dots: showDots, + dotsContainer: showDots ? '#wk-header-owl-dots' : false, + nav: false, + autoplay: autoPlay, + autoplayTimeout: interval, + autoplaySpeed: 800, + autoplayHoverPause: false, + animateOut: 'wk' + animLabel + 'Out', + animateIn: 'wk' + animLabel + 'In', + responsiveClass: true, + rtl: (typeof language_is_rtl !== 'undefined' ? language_is_rtl : false) + }); + + var owl = $imgCarousel.data('owl.carousel'); + + var _origLeave = $.proxy(owl.leave, owl); + owl.leave = function (name) { + _origLeave(name); + var cur = owl._states.current; + for (var k in cur) { + if (cur[k] < 0) { cur[k] = 0; } + } + }; + + + var $taglineEl = $('.js-header-tagline'); + var _fadeTimer = null; + var _shownIndex = -1; + + var $slides = $imgCarousel.find('.owl-item:not(.cloned) .header-slide-img'); + var taglines = $slides.map(function () { return $(this).data('tagline') || ''; }).get(); + var tlStyles = $slides.map(function () { + return { + color: $(this).data('tl-color') || '#ffffff', + fontSize: $(this).data('tl-font-size') || 16, + fontWeight: $(this).data('tl-font-weight') || '400' + }; + }).get(); + + function showTagline(tag, style) { + if (!$taglineEl.length) { return; } + if (_fadeTimer) { clearTimeout(_fadeTimer); _fadeTimer = null; } + + style = style || { color: '#ffffff', fontSize: 16, fontWeight: '400' }; + $taglineEl.css({ + color: style.color, + fontSize: style.fontSize + 'px', + fontWeight: style.fontWeight + }); + + $taglineEl.removeClass('wk-tagline-out wk-tagline-in').text(tag); + if (!tag) { $taglineEl.hide(); return; } + + $taglineEl.show(); + $taglineEl[0].offsetHeight; // force reflow so the transition actually runs + $taglineEl.addClass('wk-tagline-in'); + _fadeTimer = setTimeout(function () { + _fadeTimer = null; + $taglineEl.removeClass('wk-tagline-in'); + }, 400); + } + + _shownIndex = owl.relative(owl.current()); + showTagline(taglines[_shownIndex] || '', tlStyles[_shownIndex]); + + $imgCarousel.on('changed.owl.carousel', function () { + var realIndex = owl.relative(owl.current()); + if (realIndex === _shownIndex) { return; } // loop-snap second fire — no-op + _shownIndex = realIndex; + showTagline(taglines[realIndex] || '', tlStyles[realIndex]); + }); + + + $('.js-header-media-prev').on('click', function () { + $imgCarousel.trigger('prev.owl.carousel'); + }); + $('.js-header-media-next').on('click', function () { + $imgCarousel.trigger('next.owl.carousel'); + }); + + + if (autoPlay) { + var ap = owl._plugins && owl._plugins.autoplay; + + if (ap) { ap.pause = function () {}; } + + $imgCarousel.on('translated.owl.carousel', function () { + if (ap && owl.is('rotating') && !ap._paused) { + ap._setAutoPlayInterval(); + } + }); + + document.addEventListener('visibilitychange', function () { + if (!document.hidden) { + $imgCarousel.trigger('stop.owl.autoplay'); + $imgCarousel.trigger('play.owl.autoplay'); + } + }); + + var _watchdogTimer = setInterval(function () { + if (document.hidden) { return; } + var apNow = owl._plugins && owl._plugins.autoplay; + if (!owl.is('rotating') || (apNow && apNow._paused)) { + $imgCarousel.trigger('stop.owl.autoplay'); + $imgCarousel.trigger('play.owl.autoplay'); + } + }, interval + 1000); + + $(window).one('beforeunload', function () { + clearInterval(_watchdogTimer); + }); + } + +}); diff --git a/modules/hotelreservationsystem/views/templates/admin/hotel_header_image/helpers/view/view.tpl b/modules/hotelreservationsystem/views/templates/admin/hotel_header_image/helpers/view/view.tpl new file mode 100644 index 000000000..efc16ea9d --- /dev/null +++ b/modules/hotelreservationsystem/views/templates/admin/hotel_header_image/helpers/view/view.tpl @@ -0,0 +1,586 @@ +{** +* 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 +*} + + + + + +
    +
    +  {l s='Header Media Settings' mod='hotelreservationsystem'} +
    +
    + +
    + +
    + +

    {l s='Choose whether the home page header shows a slideshow of images or a background video.' mod='hotelreservationsystem'}

    +
    +
    + + + + +
    + +
    + + + + +{* Bulk tag line modal — triggered by the "Set tag line for all images" button above *} + + +{* Add / Edit image modal — triggered by "Add Images" and the per-row "Edit" button *} + diff --git a/modules/hotelreservationsystem/views/templates/hook/headerHotelDescBlock.tpl b/modules/hotelreservationsystem/views/templates/hook/headerHotelDescBlock.tpl index 9ea527e80..448ed0cb8 100644 --- a/modules/hotelreservationsystem/views/templates/hook/headerHotelDescBlock.tpl +++ b/modules/hotelreservationsystem/views/templates/hook/headerHotelDescBlock.tpl @@ -24,17 +24,17 @@
    -
    +
    -
    -

    {l s='Welcome To' mod='hotelreservationsystem'}

    -
    +
    + {if $QLO_HOTEL_NAME_ENABLE && $QLO_HEADER_MEDIA_TYPE == $QLO_HEADER_MEDIA_TYPE_IMAGE} {block name='header_hotel_chain_name'}

    {$WK_HTL_CHAIN_NAME|escape:'htmlall':'UTF-8'}

    {/block} + {/if} {block name='header_hotel_description'} -

    {$WK_HTL_TAG_LINE|escape:'htmlall':'UTF-8'}

    + {/block}
    diff --git a/themes/hotel-reservation-theme/header.tpl b/themes/hotel-reservation-theme/header.tpl index 8dc419a6b..6fa210eae 100644 --- a/themes/hotel-reservation-theme/header.tpl +++ b/themes/hotel-reservation-theme/header.tpl @@ -87,7 +87,62 @@ {/if}
    -
    + {/if} {/block} + - {$extra_demands_tab} + {$additinal_services_tab} diff --git a/themes/hotel-reservation-theme/_partials/booking-form.tpl b/themes/hotel-reservation-theme/_partials/booking-form.tpl index 439363d23..a750602e6 100644 --- a/themes/hotel-reservation-theme/_partials/booking-form.tpl +++ b/themes/hotel-reservation-theme/_partials/booking-form.tpl @@ -75,7 +75,7 @@
    {/block} {block name='booking_form_price_information'} - {if (isset($has_room_type_demands) && $has_room_type_demands) || (isset($service_products_exists) && $service_products_exists)} + {if isset($service_products_exists) && $service_products_exists}
    @@ -87,15 +87,15 @@
    -

    - {if isset($demands_price)}{convertPrice price=$demands_price}{else}{convertPrice price=0}{/if} - {if (isset($selected_demands) && $selected_demands) || (isset($selected_service_product) && $selected_service_product)} +

    + {if isset($service_product_price)}{convertPrice price=$service_product_price}{else}{convertPrice price=0}{/if} + {if isset($selected_service_product) && $selected_service_product} {/if}

    - {if (isset($selected_demands) && $selected_demands) || (isset($selected_service_product) && $selected_service_product)} + {if isset($selected_service_product) && $selected_service_product} {/if} - {if isset($selected_demands) && $selected_demands} -
    -

    {l s='Selected facilities'} {l s='(Per room)'}

    -
    - {foreach $selected_demands as $product} -
    -
    - {$product['name']} - {if isset($product['advance_option']) && $product['advance_option']} -

    {l s='Option:'} {$product['advance_option']['name']}

    - {/if} -
    -
    -

    {displayPrice price=$product['price']}

    - -
    -
    - {/foreach} -
    -
    - {/if}
    {l s='Total price per room'}
    -

    {displayPrice price=$demands_price_per_room}

    +

    {displayPrice price=$service_product_price_per_room}

    {l s='Total price:'}
    -

    {displayPrice price=$demands_price}

    +

    {displayPrice price=$service_product_price}

    diff --git a/themes/hotel-reservation-theme/_partials/cart_booking_demands.tpl b/themes/hotel-reservation-theme/_partials/cart_booking_demands.tpl deleted file mode 100644 index dba731425..000000000 --- a/themes/hotel-reservation-theme/_partials/cart_booking_demands.tpl +++ /dev/null @@ -1,191 +0,0 @@ -{** -* 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 -*} - -{block name='cart_booking_demands'} -
    - {if (isset($selectedRoomDemands) && $selectedRoomDemands) || (isset($roomTypeServiceProducts) && $roomTypeServiceProducts)} - {block name='cart_booking_demands_tabs'} - - {/block} - {block name='cart_booking_demands_tabs_content'} -
    - {block name='cart_booking_demands_tab_content'} - {if isset($selectedRoomDemands) && $selectedRoomDemands} -
    -
    -

    {l s='Add below facilities to the rooms for better hotel experience'}

    -
    - {assign var=roomCount value=1} - {foreach $selectedRoomDemands as $key => $roomDemand} -
    -
    - -
    - {if isset($roomTypeDemands) && $roomTypeDemands} - {foreach $roomTypeDemands as $idGlobalDemand => $demand} -
    -
    -
    -
    -

    - -

    -
    -
    -

    {$demand['name']|escape:'html':'UTF-8'}

    - {if isset($demand['adv_option']) && $demand['adv_option']} - - {else} - - {/if} -
    -
    -
    -
    -

    - {if isset($selected_adv_option) && isset($demand['adv_option'][$selected_adv_option]['price'])}{convertPrice price = $demand['adv_option'][$selected_adv_option]['price']|escape:'html':'UTF-8'}{else}{convertPrice price = $demand['price']|escape:'html':'UTF-8'}{/if}{if $demand['price_calc_method'] == HotelRoomTypeGlobalDemand::WK_PRICE_CALC_METHOD_EACH_DAY}{l s='/Night'}{/if} -

    -
    -
    - {/foreach} - {/if} -
    -
    -
    - {assign var=roomCount value=$roomCount+1} - {/foreach} -
    - {/if} - {/block} - - {block name='cart_booking_services_tab_content'} - {if isset($roomTypeServiceProducts) && $roomTypeServiceProducts} -
    -
    -

    {l s='Add below services to the rooms for better hotel experience'}

    -
    - {assign var=roomCount value=1} - {foreach $cartRooms as $key => $cartRoom} -
    -
    - -
    - {if isset($roomTypeServiceProducts) && $roomTypeServiceProducts} - {foreach $roomTypeServiceProducts as $product} - {if isset($cartRoom['selected_service']) && $cartRoom['selected_service'] && ($product['id_product']|array_key_exists:$cartRoom['selected_service'])} - {assign var='serviceSelected' value=true} - {$product.price_tax_incl = $cartRoom['selected_service'][$product['id_product']]['unit_price_tax_incl']} - {$product.price_tax_exc = $cartRoom['selected_service'][$product['id_product']]['unit_price_tax_excl']} - {else} - {assign var='serviceSelected' value=false} - {/if} -
    -
    -
    -
    - {if $product.available_for_order} -

    - -

    - {/if} -
    -
    -
    {$product['name']|escape:'html':'UTF-8'}
    - {if $product.price_calculation_method == Product::PRICE_CALCULATION_METHOD_PER_DAY} -
    - {if ($product.show_price && !isset($restricted_country_mode)) || isset($groups)}{if !$priceDisplay}{convertPrice price=$product.price_tax_incl}{else}{convertPrice price=$product.price_tax_exc}{/if}{/if}{l s='/night'} -
    - {/if} - {if $product.allow_multiple_quantity} -
    - - -
    - - -
    -
    - {/if} -
    -
    -
    -
    - {if ($product.show_price && !isset($restricted_country_mode)) || isset($groups)} - - {if !$priceDisplay} - {if $serviceSelected} - {convertPrice price=$cartRoom['selected_service'][$product['id_product']]['total_price_tax_incl']} - {else} - {convertPrice price=$product.price_tax_incl} - {/if} - {else} - {if $serviceSelected} - {convertPrice price=$cartRoom['selected_service'][$product['id_product']]['total_price_tax_excl']} - {else} - {convertPrice price=$product.price_tax_exc} - {/if} - {/if} - - - {/if} -
    -
    - {/foreach} - {/if} -
    -
    -
    - {assign var=roomCount value=$roomCount+1} - {/foreach} -
    - {/if} - {/block} -
    - {/block} - {/if} -
    -{/block} diff --git a/themes/hotel-reservation-theme/_partials/cart_booking_services.tpl b/themes/hotel-reservation-theme/_partials/cart_booking_services.tpl new file mode 100644 index 000000000..5a0d69878 --- /dev/null +++ b/themes/hotel-reservation-theme/_partials/cart_booking_services.tpl @@ -0,0 +1,105 @@ +{** +* 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 +*} + +{block name='cart_booking_services'} +
    + {if isset($roomTypeServiceProducts) && $roomTypeServiceProducts} + {block name='cart_booking_services_tabs'} + + {/block} + {block name='cart_booking_services_tabs_content'} +
    + {block name='cart_booking_services_tab_content'} + {if isset($roomTypeServiceProducts) && $roomTypeServiceProducts} +
    +
    +

    {l s='Add below services to the rooms for better hotel experience'}

    +
    + {assign var=roomCount value=1} + {foreach $cartRooms as $key => $cartRoom} +
    +
    + +
    + {if isset($roomTypeServiceProducts) && $roomTypeServiceProducts} + {foreach $roomTypeServiceProducts as $product} + {if isset($cartRoom['selected_service']) && $cartRoom['selected_service'] && ($product['id_product']|array_key_exists:$cartRoom['selected_service'])} + {assign var='serviceSelected' value=true} + {$product.price_tax_incl = $cartRoom['selected_service'][$product['id_product']]['unit_price_tax_incl']} + {$product.price_tax_exc = $cartRoom['selected_service'][$product['id_product']]['unit_price_tax_excl']} + {else} + {assign var='serviceSelected' value=false} + {/if} +
    +
    +
    +
    + {if $product.available_for_order} +

    + +

    + {/if} +
    +
    +

    {$product['name']|escape:'html':'UTF-8'}

    + {if $product.allow_multiple_quantity} +
    + + +
    + + +
    +
    + {/if} +
    +
    +
    +
    + {if ($product.show_price && !isset($restricted_country_mode)) || isset($groups)} + {if !$priceDisplay}{convertPrice price=$product.price_tax_incl}{else}{convertPrice price=$product.price_tax_exc}{/if}{if $product.price_calculation_method == Product::PRICE_CALCULATION_METHOD_PER_DAY}{l s='/Night'}{/if} + + {/if} +
    +
    + {/foreach} + {/if} +
    +
    +
    + {assign var=roomCount value=$roomCount+1} + {/foreach} +
    + {/if} + {/block} +
    + {/block} + {/if} +
    +{/block} \ No newline at end of file diff --git a/themes/hotel-reservation-theme/_partials/order-extra-services.tpl b/themes/hotel-reservation-theme/_partials/order-extra-services.tpl index 095cfc644..7f91dca6a 100644 --- a/themes/hotel-reservation-theme/_partials/order-extra-services.tpl +++ b/themes/hotel-reservation-theme/_partials/order-extra-services.tpl @@ -26,15 +26,10 @@ {l s='Extra Services'}
    - {if (isset($extraDemands) && $extraDemands) || (isset($additionalServices) && $additionalServices)} + {if (isset($additionalServices) && $additionalServices)} {block name='order_extra_services_tabs'} {/block} {block name='order_extra_services_tabs_content'} @@ -44,14 +39,14 @@
    {assign var=roomCount value=1} {foreach $additionalServices as $key => $roomAdditionalService} -
    -
    +
    +
    {l s='Room'} {$roomCount|string_format:'%02d'}  ({if {$roomAdditionalService['adults']} <= 9}0{$roomAdditionalService['adults']}{else}{$roomAdditionalService['adults']}{/if} {if $roomAdditionalService['adults'] > 1}{l s='Adults'}{else}{l s='Adult'}{/if}{if $roomAdditionalService['children'] > 0}, {if {$roomAdditionalService['children']} <= 9}0{$roomAdditionalService['children']}{else}{$roomAdditionalService['children']}{/if} {if $roomAdditionalService['children'] > 1}{l s='Children'}{else}{l s='Child'}{/if}{/if})
    -
    +
    {foreach $roomAdditionalService['additional_services'] as $additionalService} -
    +
    {$additionalService['name']|escape:'html':'UTF-8'} @@ -78,40 +73,6 @@
    {/if} {/block} - - {block name='order_extra_demands_tab_content'} - {if isset($extraDemands) && $extraDemands} -
    - {assign var=roomCount value=1} - {foreach $extraDemands as $roomDemand} -
    -
    - {l s='Room'} {$roomCount|string_format:'%02d'}  - ({if {$roomDemand['adults']} <= 9}0{$roomDemand['adults']}{else}{$roomDemand['adults']}{/if} {if $roomDemand['adults'] > 1}{l s='Adults'}{else}{l s='Adult'}{/if}{if $roomDemand['children'] > 0}, {if {$roomDemand['children']} <= 9}0{$roomDemand['children']}{else}{$roomDemand['children']}{/if} {if $roomDemand['children'] > 1}{l s='Children'}{else}{l s='Child'}{/if}{/if}) -
    -
    - {foreach $roomDemand['extra_demands'] as $demand} -
    -
    {$demand['name']|escape:'html':'UTF-8'}
    -
    - - {if $useTax} - {displayPrice price="{$demand['total_price_tax_incl']|escape:'html':'UTF-8'}" currency=$objOrder->id_currency} - {else} - {displayPrice price="{$demand['total_price_tax_excl']|escape:'html':'UTF-8'}" currency=$objOrder->id_currency} - {/if} - -
    -
    - {/foreach} -
    -
    - {assign var=roomCount value=$roomCount+1} - {/foreach} -
    -
    - {/if} - {/block}
    {/block} {/if} diff --git a/themes/hotel-reservation-theme/_partials/order-room-detail.tpl b/themes/hotel-reservation-theme/_partials/order-room-detail.tpl index aaf47aa0c..caaa8c6b9 100644 --- a/themes/hotel-reservation-theme/_partials/order-room-detail.tpl +++ b/themes/hotel-reservation-theme/_partials/order-room-detail.tpl @@ -101,13 +101,13 @@
    {l s='Extra Services'}
    - {if (isset($rm_v.extra_demands) && $rm_v.extra_demands) || isset($rm_v.additional_services) && $rm_v.additional_services} + {if isset($rm_v.additional_services) && $rm_v.additional_services} {/if} {if $group_use_tax} - {displayWtPriceWithCurrency price=($rm_v.extra_demands_price_ti + $rm_v.additional_services_price_ti) currency=$currency} + {displayWtPriceWithCurrency price=$rm_v.additional_services_price_ti currency=$currency} {else} - {displayWtPriceWithCurrency price=($rm_v.extra_demands_price_te + $rm_v.additional_services_price_te) currency=$currency} + {displayWtPriceWithCurrency price=$rm_v.additional_services_price_te currency=$currency} {/if} {if (isset($rm_v.extra_demands) && $rm_v.extra_demands) || isset($rm_v.additional_services) && $rm_v.additional_services} @@ -120,11 +120,11 @@
    {l s='Total Price'}
    {if $group_use_tax} - {displayWtPriceWithCurrency price=($rm_v.amount_tax_incl + $rm_v.extra_demands_price_ti + $rm_v.additional_services_price_ti + $rm_v.additional_services_price_auto_add_ti) currency=$currency} + {displayWtPriceWithCurrency price=($rm_v.amount_tax_incl + $rm_v.additional_services_price_ti + $rm_v.additional_services_price_auto_add_ti) currency=$currency} {else} - {displayWtPriceWithCurrency price=($rm_v.amount_tax_excl + $rm_v.extra_demands_price_te + $rm_v.additional_services_price_te + $rm_v.additional_services_price_auto_add_te) currency=$currency} + {displayWtPriceWithCurrency price=($rm_v.amount_tax_excl + $rm_v.additional_services_price_te + $rm_v.additional_services_price_auto_add_te) currency=$currency} {/if} - {if (isset($rm_v.extra_demands) && $rm_v.extra_demands) || isset($rm_v.additional_services) && $rm_v.additional_services} + {if isset($rm_v.additional_services) && $rm_v.additional_services} @@ -151,9 +151,9 @@

    {if $group_use_tax} - {displayWtPriceWithCurrency price=($rm_v.extra_demands_price_ti + $rm_v.additional_services_price_ti) currency=$currency} + {displayWtPriceWithCurrency price=$rm_v.additional_services_price_ti currency=$currency} {else} - {displayWtPriceWithCurrency price=($rm_v.extra_demands_price_te + $rm_v.additional_services_price_te) currency=$currency} + {displayWtPriceWithCurrency price=$rm_v.additional_services_price_te currency=$currency} {/if}

    diff --git a/themes/hotel-reservation-theme/cart-total-block.tpl b/themes/hotel-reservation-theme/cart-total-block.tpl index 4b4e1b2fd..7034fd5f5 100644 --- a/themes/hotel-reservation-theme/cart-total-block.tpl +++ b/themes/hotel-reservation-theme/cart-total-block.tpl @@ -22,7 +22,7 @@
    - {if $total_rooms_wt + $total_extra_demands_wt + $total_additional_services_wt + $total_additional_services_auto_add_wt} + {if $total_rooms_wt + $total_additional_services_wt + $total_additional_services_auto_add_wt}

    {l s='Total rooms cost'} @@ -36,9 +36,9 @@ {if $use_taxes && $priceDisplay == 0} - {assign var='total_rooms_cost' value=($total_rooms_wt + $total_extra_demands_wt + $total_additional_services_wt + $total_additional_services_auto_add_wt)} + {assign var='total_rooms_cost' value=($total_rooms_wt + $total_additional_services_wt + $total_additional_services_auto_add_wt)} {else} - {assign var='total_rooms_cost' value=($total_rooms + $total_extra_demands + $total_additional_services + $total_additional_services_auto_add)} + {assign var='total_rooms_cost' value=($total_rooms + $total_additional_services + $total_additional_services_auto_add)} {/if} {displayPrice price=$total_rooms_cost} diff --git a/themes/hotel-reservation-theme/css/global.css b/themes/hotel-reservation-theme/css/global.css index 1f84e79ca..b78154fd0 100644 --- a/themes/hotel-reservation-theme/css/global.css +++ b/themes/hotel-reservation-theme/css/global.css @@ -9002,21 +9002,15 @@ a.open_rooms_extra_services_panel { #rooms_extra_services .tab-content { padding: 0 15px; } -#rooms_extra_services .demand_header a { +rooms_extra_services .additional_service_header a { text-decoration: none; color: #444444;} -#rooms_extra_services .room_demand_detail { +#rooms_extra_services .room_service_detail { margin-top: 15px; padding-top: 15px; border-top: 1px solid #eeeeee;} -/* #rooms_extra_services .room_demand_detail .demand_detail_price_block { - margin-bottom: 9px; - font-size: 14px; - margin-bottom: 15px;} */ -/* #rooms_extra_services .rooms_extra_services_head { - margin-bottom: 18px;} */ -#rooms_extra_services .room_demand_block { +#rooms_extra_services .room_service_block { margin-bottom: 10px; color: #333; font-size: 14px;} diff --git a/themes/hotel-reservation-theme/css/order-confirmation.css b/themes/hotel-reservation-theme/css/order-confirmation.css index 830daea1b..9ea9c3785 100644 --- a/themes/hotel-reservation-theme/css/order-confirmation.css +++ b/themes/hotel-reservation-theme/css/order-confirmation.css @@ -192,28 +192,28 @@ padding-bottom: 15px; } -#rooms_extra_services .tab-content .rooms_extra_demands_head { +#rooms_extra_services .tab-content .rooms_extra_services_head { padding: 10px 18px; border-bottom: 1px solid #F0F0F0; } -#rooms_extra_services .tab-content .rooms_extra_demands_text { +#rooms_extra_services .tab-content .rooms_extra_services_text { font-size: 12px; margin-bottom: 0; } -#rooms_extra_services .tab-content .room_demands { +#rooms_extra_services .tab-content .room_additional_services { margin-top: 20px; } -#rooms_extra_services .tab-content .demand_header { +#rooms_extra_services .tab-content .additional_service_header { font-size: 16px; font-weight: bold; line-height: 24px; margin-bottom: 10px; } -#rooms_extra_services .tab-content .room_demand_detail { +#rooms_extra_services .tab-content .room_service_detail { font-size: 14px; font-weight: 500; margin-top: 0; @@ -221,12 +221,12 @@ border-top: unset; } -#rooms_extra_services .tab-content .room_demand_block { +#rooms_extra_services .tab-content .room_service_block { display: flex; justify-content: space-between; } -#rooms_extra_services .tab-content .room_demand_block:not(:last-of-type) { +#rooms_extra_services .tab-content .room_service_block:not(:last-of-type) { margin-bottom: 12px; } diff --git a/themes/hotel-reservation-theme/css/order-detail.css b/themes/hotel-reservation-theme/css/order-detail.css index fd26eee66..1a39b7e47 100644 --- a/themes/hotel-reservation-theme/css/order-detail.css +++ b/themes/hotel-reservation-theme/css/order-detail.css @@ -1044,38 +1044,38 @@ padding-bottom: 15px; } -#popup-view-extra-services .tab-content .rooms_extra_demands_head { +#popup-view-extra-services .tab-content .rooms_extra_services_head { padding: 10px 18px; border-bottom: 1px solid #F0F0F0; } -#popup-view-extra-services .tab-content .rooms_extra_demands_text { +#popup-view-extra-services .tab-content .rooms_extra_services_text { font-size: 12px; margin-bottom: 0; } -#popup-view-extra-services .tab-content .room_demands { +#popup-view-extra-services .tab-content .room_additional_services { margin-top: 20px; } -#popup-view-extra-services .tab-content .demand_header { +#popup-view-extra-services .tab-content .additional_service_header { font-size: 16px; font-weight: bold; line-height: 24px; margin-bottom: 10px; } -#popup-view-extra-services .tab-content .room_demand_detail { +#popup-view-extra-services .tab-content .room_service_detail { font-size: 14px; font-weight: 500; } -#popup-view-extra-services .tab-content .room_demand_block { +#popup-view-extra-services .tab-content .room_service_block { display: flex; justify-content: space-between; } -#popup-view-extra-services .tab-content .room_demand_block:not(:last-of-type) { +#popup-view-extra-services .tab-content .room_service_block:not(:last-of-type) { margin-bottom: 12px; } .payment-summary tr, .guest-details tr { diff --git a/themes/hotel-reservation-theme/css/order-opc.css b/themes/hotel-reservation-theme/css/order-opc.css index cc24e5c7a..573f07c24 100644 --- a/themes/hotel-reservation-theme/css/order-opc.css +++ b/themes/hotel-reservation-theme/css/order-opc.css @@ -241,7 +241,7 @@ color: #777; content: '+'; } -.room_price_detail_block .demand_total_price, .room_price_detail_block .total_price { +.room_price_detail_block .extra_service_total_price, .room_price_detail_block .total_price { line-height: 28px; } @@ -255,7 +255,7 @@ transform: translate(15px, 0px); } } -.room_price_detail_block .price_block .total_price, .room_price_detail_block .demand_price_block .demand_total_price{ +.room_price_detail_block .price_block .total_price, .room_price_detail_block .extra_service_price_block .extra_service_total_price{ word-break: break-word; } .order-detail-content .room_price_detail_block .plus-sign{ @@ -652,8 +652,8 @@ text-align: center;} -/* Fancy-box for extra demands Css */ -.room_extra_demands .open_rooms_extra_services_panel { +/* Fancy-box for extra services Css */ +.room_extra_services .open_rooms_extra_services_panel { font-size:12px; font-weight: 600; color: #0000FF;} diff --git a/themes/hotel-reservation-theme/css/product.css b/themes/hotel-reservation-theme/css/product.css index cee78c595..3d1cfe030 100644 --- a/themes/hotel-reservation-theme/css/product.css +++ b/themes/hotel-reservation-theme/css/product.css @@ -1352,8 +1352,8 @@ Quick View Styles #booking_action_loader.active { display: block; } -/* room type extra demands container */ -.room_demands_container_overlay { +/* room type extra service container */ +.room_service_container_overlay { display: none; position: absolute; left: 0; @@ -1392,15 +1392,15 @@ Quick View Styles top: 0; } -.room_demands_container .room_demand_block { +.room_service_container .room_service_block { margin-bottom: 15px; color: #333; font-size: 14px; } -.room_demands_container .checkbox { +.room_service_container .checkbox { margin-top: 2px; } -.room_demands_container label { +.room_service_container label { opacity: 0.5; margin-bottom: 15px; text-transform: uppercase; @@ -1415,7 +1415,7 @@ Quick View Styles .price_desc_block .total_price_block { color: #333; } -.price_desc_block .extra_demands_price_block { +.price_desc_block .extra_services_price_block { color: #299DFF; } .price_desc_block .plus-sign { @@ -1469,49 +1469,46 @@ Quick View Styles /* end */ -/* extra demands Css */ -#rooms_extra_demands { +/* extra services Css */ +#rooms_extra_services { position: relative; font-size: 16px;} -.room_extra_demands .open_rooms_extra_demands { +.rooms_extra_services .open_rooms_extra_services { font-size:12px; font-weight: 600; color: #0000FF;} -#rooms_extra_demands .demand_header { +#rooms_extra_services .additional_service_header { padding-bottom: 15px;} -#rooms_extra_demands .room_demand_detail .demand_detail_price_block { - font-size: 14px; - margin-bottom: 15px;} -#rooms_extra_demands .rooms_extra_demands_head { +#rooms_extra_services .rooms_extra_services_head { margin-bottom: 18px;} -#rooms_extra_demands .card { +#rooms_extra_services .card { padding-bottom: 0px;} -#rooms_extra_demands .room_demand_block { +#rooms_extra_services .room_service_block { margin-bottom: 15px; color: #333; font-size: 14px;} -#rooms_extra_demands .checkbox { +#rooms_extra_services .checkbox { margin-top: 3px;} -#rooms_extra_demands label { +#rooms_extra_services label { opacity: 0.5; margin-bottom: 15px; text-transform: uppercase;} /* fancybox accordian css */ -/* #rooms_extra_demands .accordion, #rooms_extra_demands .accordion * { +/* #rooms_extra_services .accordion, #rooms_extra_services .accordion * { -webkit-box-sizing:border-box; -moz-box-sizing:border-box; box-sizing:border-box;} -#rooms_extra_demands .accordion { +#rooms_extra_services .accordion { overflow:hidden; border-bottom: 1px solid #CCCCCC;} -#rooms_extra_demands .accordion-section-title { +#rooms_extra_services .accordion-section-title { width:100%; display:inline-block; transition:all linear 0.15s; color:#252525 !important; text-decoration:none !important;} -#rooms_extra_demands .accordion-section-content { +#rooms_extra_services .accordion-section-content { display:none; border-top: 1px solid #eee; padding-top: 15px;} */ diff --git a/themes/hotel-reservation-theme/guest-tracking.tpl b/themes/hotel-reservation-theme/guest-tracking.tpl index f5764cd6b..72ae8c4c5 100644 --- a/themes/hotel-reservation-theme/guest-tracking.tpl +++ b/themes/hotel-reservation-theme/guest-tracking.tpl @@ -52,8 +52,6 @@ {assign var=HOOK_ORDERDETAILDISPLAYED value=$order->hook_orderdetaildisplayed} {assign var=total_convenience_fee_ti value=$order->total_convenience_fee_ti} {assign var=total_convenience_fee_te value=$order->total_convenience_fee_te} - {assign var=total_demands_price_ti value=$order->total_demands_price_ti} - {assign var=total_demands_price_te value=$order->total_demands_price_te} {assign var=any_back_order value=$order->any_back_order} {assign var=shw_bo_msg value=$order->shw_bo_msg} {assign var=back_ord_msg value=$order->back_ord_msg} diff --git a/themes/hotel-reservation-theme/js/modules/blockcart/ajax-cart.js b/themes/hotel-reservation-theme/js/modules/blockcart/ajax-cart.js index ca2ad2695..49ba8af63 100644 --- a/themes/hotel-reservation-theme/js/modules/blockcart/ajax-cart.js +++ b/themes/hotel-reservation-theme/js/modules/blockcart/ajax-cart.js @@ -392,7 +392,7 @@ var ajaxCart = { req.append('id_hotel', idHotel); if (parseInt(booking_product)) { - // get the selected extra demands by customer + // get the selected extra services by customer if (typeof dateFrom != 'undefined') req.append('dateFrom', dateFrom); if (typeof dateTo != 'undefined') @@ -403,9 +403,6 @@ var ajaxCart = { } else { req.append('qty', ((occupancy && occupancy != null) ? occupancy : '1')); } - - var roomDemands = getRoomsExtraDemands(); - req.append('roomDemands', JSON.stringify(roomDemands) ); req.append('serviceProducts', JSON.stringify(getRoomsServiceProducts()) ); } else { req.append('qty', ((occupancy && occupancy != null) ? occupancy : '1')); @@ -426,11 +423,6 @@ var ajaxCart = { processData: false, success: function(jsonData, textStatus, jqXHR) { if (pagename == 'product') { - if (jsonData.avail_rooms == 0) { - disableRoomTypeDemands(1); - } - - resetRoomtypeServices(); if (parseInt($('#product_page_booking_product').val()) == 0) { $('#service_product_qty').val(1); @@ -565,9 +557,6 @@ var ajaxCart = { var product_page_id_product = $('#product_page_product_id').val(); if (idProduct == product_page_id_product && dateFrom < date_checkOut && dateTo >= date_checkIn) { $(".max_avail_type_qty").val(jsonData.avail_rooms); - if (jsonData.avail_rooms == 0) { - disableRoomTypeDemands(1); - } } BookingForm.refresh(); } @@ -632,7 +621,6 @@ var ajaxCart = { data: 'controller=cart&delete=1&dateFrom=' + dateFrom + '&dateTo=' + dateTo + '&id_product=' + idProduct + '&id_hotel=' + idHotel + '&id_product_option=' + ((idCombination != null && parseInt(idCombination)) ? idCombination : '') + ((customizationId && customizationId != null) ? '&id_customization=' + customizationId : '') + '&id_address_delivery=' + idAddressDelivery + '&token=' + static_token + '&ajax=true', success: function(jsonData) { if (pagename == 'product') { - disableRoomTypeDemands(0); BookingForm.refresh(); } @@ -953,7 +941,7 @@ var ajaxCart = { content += ''; content += '' + moment(new Date(date_diff_v.data_form)).format(dateFormatToUse) + ' - ' + moment(new Date(date_diff_v.data_to)).format(dateFormatToUse) + ''; content += '' + date_diff_v.num_rm + ''; - content += '' + formatCurrency(parseFloat(date_diff_v.amount) + parseFloat(date_diff_v.demand_price), currency_format, currency_sign, currency_blank) + ''; + content += '' + formatCurrency(parseFloat(date_diff_v.amount) + parseFloat(date_diff_v.additional_price), currency_format, currency_sign, currency_blank) + ''; content += ''; content += ''; content += ''; @@ -1031,7 +1019,7 @@ var ajaxCart = { booking_dates_content += ''; booking_dates_content += '' + moment(new Date(date_diff_v1.data_form)).format(dateFormatToUse) + ' - ' + moment(new Date(date_diff_v1.data_to)).format(dateFormatToUse) + ''; booking_dates_content += '' + date_diff_v1.num_rm + ''; - booking_dates_content += '' + formatCurrency(parseFloat(date_diff_v1.amount) + parseFloat(date_diff_v1.demand_price), currency_format, currency_sign, currency_blank) + ''; + booking_dates_content += '' + formatCurrency(parseFloat(date_diff_v1.amount) + parseFloat(date_diff_v1.additional_price), currency_format, currency_sign, currency_blank) + ''; booking_dates_content += ''; booking_dates_content += ''; booking_dates_content += ''; @@ -1389,7 +1377,6 @@ function crossselling_serialScroll() { } function resetRoomtypeServices(refresh = true) { - $('.room_demands_container').find('input.id_room_type_demand:checked').prop('checked', false).uniform(); $('#additional_products').empty(); $('#additional_products div') $('.remove_roomtype_product').text(select_txt).removeClass('btn-danger remove_roomtype_product').addClass('btn-success add_roomtype_product'); @@ -1398,19 +1385,6 @@ function resetRoomtypeServices(refresh = true) { } } -function disableRoomTypeDemands(show) { - if (show) { - $('.room_demands_container_overlay').show(); - $('.room_demands_container').find('input:checkbox.id_room_type_demand').prop('checked', false); - $('.room_demand_block').find('.id_room_type_demand').prop('checked', false).parent().removeClass('checked'); - $('.room_demands_container').find('input:checkbox.id_room_type_demand').attr('disabled', 'disabled'); - } else { - $('.room_demands_container_overlay').hide(); - $('.room_demands_container').find('input:checkbox.id_room_type_demand').removeAttr('disabled'); - $('.room_demands_container').find('.checker').removeClass('disabled'); - } -} - function disableRoomTypeServices(disable) { if (disable) { resetRoomtypeServices(false); @@ -1422,20 +1396,6 @@ function disableRoomTypeServices(disable) { } } -function getRoomsExtraDemands() -{ - var roomDemands = []; - - $('input:checkbox.id_room_type_demand:checked').each(function () { - roomDemands.push({ - 'id_global_demand':$(this).val(), - 'id_option': $(this).closest('.room_demand_block').find('.id_option').val() - }); - }); - - return roomDemands; -} - function getRoomsServiceProducts() { var serviceProducts = []; diff --git a/themes/hotel-reservation-theme/js/order-confirmation.js b/themes/hotel-reservation-theme/js/order-confirmation.js index 615bee6ff..d70d79407 100644 --- a/themes/hotel-reservation-theme/js/order-confirmation.js +++ b/themes/hotel-reservation-theme/js/order-confirmation.js @@ -40,15 +40,15 @@ $(document).ready(function(){ date_to: dateTo, id_product: idProduct, id_order: idOrder, - action: 'getRoomTypeBookingDemands', - method: 'getRoomTypeBookingDemands', + action: 'getRoomTypeBookingServices', + method: 'getRoomTypeBookingServices', ajax: true, token: static_token }, success: function(result) { - if (result.extra_demands) { + if (result.extra_services) { $('#rooms_extra_services').html(''); - $('#rooms_extra_services').append(result.extra_demands); + $('#rooms_extra_services').append(result.extra_services); } $.fancybox({ href: "#rooms_extra_services", diff --git a/themes/hotel-reservation-theme/js/order-detail.js b/themes/hotel-reservation-theme/js/order-detail.js index 800440304..81d9792d8 100644 --- a/themes/hotel-reservation-theme/js/order-detail.js +++ b/themes/hotel-reservation-theme/js/order-detail.js @@ -354,14 +354,14 @@ $(document).on('click', '.btn-view-extra-services', function(e) { date_to: dateTo, id_product: idProduct, id_order: idOrder, - action: 'getRoomTypeBookingDemands', + action: 'getRoomTypeBookingServices', ajax: true, token: static_token, }, success: function(result) { - if (result.extra_demands) { + if (result.extra_services) { $('#popup-view-extra-services').html(''); - $('#popup-view-extra-services').append(result.extra_demands); + $('#popup-view-extra-services').append(result.extra_services); $.fancybox.open({ href: '#popup-view-extra-services', diff --git a/themes/hotel-reservation-theme/js/order-opc.js b/themes/hotel-reservation-theme/js/order-opc.js index 4c9ae91f1..bd206eaa2 100644 --- a/themes/hotel-reservation-theme/js/order-opc.js +++ b/themes/hotel-reservation-theme/js/order-opc.js @@ -558,79 +558,6 @@ $(document).ready(function() e.preventDefault(); }); - $(document).on('click', '.id_room_type_demand', function() { - var roomDemands = []; - // get the selected extra demands by customer - $(this).closest('.room_demand_detail').find('input:checkbox.id_room_type_demand:checked').each(function () { - roomDemands.push({ - 'id_global_demand':$(this).val(), - 'id_option': $(this).closest('.room_demand_block').find('.id_option').val() - }); - }); - var idBookingCart = $(this).attr('id_cart_booking'); - $.ajax({ - type: 'POST', - headers: { - "cache-control": "no-cache" - }, - url: orderOpcUrl, - dataType: 'JSON', - cache: false, - data: { - id_cart_booking: idBookingCart, - room_demands: JSON.stringify(roomDemands), - method: 'changeRoomDemands', - ajax: true - }, - success: function(result) { - if (result == 1) { - showSuccessMessage(txtExtraDemandSucc); - } else { - showErrorMessage(txtExtraDemandErr); - } - } - }); - }); - - $(document).on('change', '.demand_adv_option_block .id_option', function(e) { - var option_selected = $(this).find('option:selected'); - var extra_demand_price = option_selected.attr("optionPrice") - extra_demand_price = parseFloat(extra_demand_price); - extra_demand_price = formatCurrency(extra_demand_price, currency_format, currency_sign, currency_blank); - $(this).closest('.room_demand_block').find('.extra_demand_option_price').text(extra_demand_price); - var roomDemands = []; - // get the selected extra demands by customer - $(this).closest('.room_demand_detail').find('input:checkbox.id_room_type_demand:checked').each(function () { - roomDemands.push({ - 'id_global_demand':$(this).val(), - 'id_option': $(this).closest('.room_demand_block').find('.id_option').val() - }); - }); - var idBookingCart = $(this).closest('.room_demand_block').find('.id_room_type_demand').attr('id_cart_booking'); - $.ajax({ - type: 'POST', - headers: { - "cache-control": "no-cache" - }, - url: orderOpcUrl, - dataType: 'JSON', - cache: false, - data: { - id_cart_booking: idBookingCart, - room_demands: JSON.stringify(roomDemands), - method: 'changeRoomDemands', - ajax: true - }, - success: function(result) { - if (result == 1) { - showSuccessMessage(txtExtraDemandSucc); - } else { - showErrorMessage(txtExtraDemandErr); - } - } - }); - }); - $(document).on('click', '.change_room_type_service_product', function() { updateServiceProducts(this); }); @@ -692,8 +619,8 @@ $(document).ready(function() roomServiceProductTotalPriceQty = $(this).val()*roomServiceProductUnitPrice; roomServiceProductValue.text(roomServiceProductCurrencySign + roomServiceProductTotalPriceQty.toFixed(priceDisplayPrecision)) - if ($(this).closest('.room_demand_block').find('.change_room_type_service_product').is(':checked')) { - updateServiceProducts($(this).closest('.room_demand_block').find('.change_room_type_service_product')); + if ($(this).closest('.room_service_block').find('.change_room_type_service_product').is(':checked')) { + updateServiceProducts($(this).closest('.room_service_block').find('.change_room_type_service_product')); } }); @@ -716,14 +643,14 @@ $(document).ready(function() date_to: dateTo, id_product: idProduct, id_order: idOrder, - method: 'getRoomTypeBookingDemands', + method: 'getRoomTypeBookingServices', ajax: true, token: static_token }, success: function(result) { - if (result.extra_demands) { + if (result.extra_services) { $('#rooms_extra_services').html(''); - $('#rooms_extra_services').append(result.extra_demands); + $('#rooms_extra_services').append(result.extra_services); } $.fancybox({ href: "#rooms_extra_services", @@ -747,12 +674,12 @@ $(document).ready(function() { var id_product = $(element).val(); var id_cart_booking = $(element).data('id_cart_booking'); - var qty = $(element).closest('.room_demand_block').find('input.qty'); + var qty = $(element).closest('.room_service_block').find('input.qty'); if (qty.length == 0) { update_qty = 1; } else { - var qty_hidden = $(element).closest('.room_demand_block').find('input.qty_hidden'); + var qty_hidden = $(element).closest('.room_service_block').find('input.qty_hidden'); var update_qty = parseInt($(qty).val()) - parseInt($(qty_hidden).val()); } var checked = $(element).is(':checked'); @@ -789,7 +716,7 @@ $(document).ready(function() }, success: function(jsonData) { if (!jsonData.hasError) { - showSuccessMessage(txtExtraDemandSucc); + showSuccessMessage(txtExtraServiceSucc); } else { showErrorMessage(jsonData.errors); diff --git a/themes/hotel-reservation-theme/js/product.js b/themes/hotel-reservation-theme/js/product.js index b0771177c..4a5d20f89 100644 --- a/themes/hotel-reservation-theme/js/product.js +++ b/themes/hotel-reservation-theme/js/product.js @@ -1041,30 +1041,8 @@ $(document).ready(function() { BookingForm.refresh(); }); - $('.id_room_type_demand').on('click', function() { - BookingForm.refresh(); - }); - $(document).on('click', '.remove_roomtype_demand', function(e) { - e.preventDefault(); - let idGlobalDemand = $(this).data('id_global_demand'); - $('.id_room_type_demand[data-id_global_demand="' + idGlobalDemand + '"]:checked').prop('checked', false).uniform(); - BookingForm.refresh(); - }); - - $(document).on('change', '.room_demand_block .id_option', function(e) { - var optionSelected = $(this).find('option:selected'); - var extraDemandPrice = optionSelected.attr("optionPrice") - extraDemandPrice = parseFloat(extraDemandPrice); - extraDemandPrice = formatCurrency(extraDemandPrice, currency_format, currency_sign, currency_blank); - $(this).closest('.room_demand_block').find('.extra_demand_option_price').text(extraDemandPrice); - if ($(this).closest('.room_demand_block').find('.id_room_type_demand:checked').val()) { - BookingForm.refresh(); - } - }); - - - // Accordian for extra demand + // Accordian for extra services function close_accordion_section() { $('.accordion .accordion-section-title').removeClass('active'); $('.accordion .accordion-section-content').slideUp(300).removeClass('open'); @@ -1390,10 +1368,8 @@ var BookingForm = { }); } if (!$('.max_avail_type_qty').length || $('.max_avail_type_qty').val() < 1) { - disableRoomTypeDemands(1); disableRoomTypeServices(1); } else { - disableRoomTypeDemands(0); disableRoomTypeServices(0); } $(document).trigger("QloApps:afterBookingFormInit"); @@ -1453,7 +1429,6 @@ var BookingForm = { }, getFormData: function () { let formData = new FormData($('form#booking-form').get(0)); - formData.append('room_type_demands', JSON.stringify(getRoomsExtraDemands())); formData.append('room_service_products', JSON.stringify(getRoomsServiceProducts())); return formData; }, diff --git a/themes/hotel-reservation-theme/modules/blockcart/cartrow.tpl b/themes/hotel-reservation-theme/modules/blockcart/cartrow.tpl index e909aaacf..ce2b0d9e6 100644 --- a/themes/hotel-reservation-theme/modules/blockcart/cartrow.tpl +++ b/themes/hotel-reservation-theme/modules/blockcart/cartrow.tpl @@ -88,7 +88,7 @@ {dateFormat date=$data_v['data_form'] full=$is_full_date} - {dateFormat date=$data_v['data_to'] full=$is_full_date} {$data_v['num_rm']} - {convertPrice price=($data_v['amount'] + $data_v['demand_price'])} + {convertPrice price=($data_v['amount'] + $data_v['additional_price'])} {/foreach} diff --git a/themes/hotel-reservation-theme/order-confirmation.tpl b/themes/hotel-reservation-theme/order-confirmation.tpl index e8cc450cc..903b7a609 100644 --- a/themes/hotel-reservation-theme/order-confirmation.tpl +++ b/themes/hotel-reservation-theme/order-confirmation.tpl @@ -135,15 +135,15 @@

    {l s='Extra Services'}
    - {if (isset($rm_v['extra_demands']) && $rm_v['extra_demands']) || isset($rm_v['additional_services']) && $rm_v['additional_services']} + {if isset($rm_v['additional_services']) && $rm_v['additional_services']} {/if} {if $group_use_tax} - {displayWtPriceWithCurrency price=($rm_v['extra_demands_price_ti'] + $rm_v['additional_services_price_ti']) currency=$objOrderCurrency} + {displayWtPriceWithCurrency price=$rm_v['additional_services_price_ti'] currency=$objOrderCurrency} {else} - {displayWtPriceWithCurrency price=($rm_v['extra_demands_price_te'] + $rm_v['additional_services_price_te']) currency=$objOrderCurrency} + {displayWtPriceWithCurrency price=$rm_v['additional_services_price_te'] currency=$objOrderCurrency} {/if} - {if (isset($rm_v['extra_demands']) && $rm_v['extra_demands']) || isset($rm_v['additional_services']) && $rm_v['additional_services']} + {if isset($rm_v['additional_services']) && $rm_v['additional_services']} {/if}
    @@ -154,11 +154,11 @@
    {l s='Total Price'}
    {if $group_use_tax} - {displayWtPriceWithCurrency price=($rm_v['amount_tax_incl'] + $rm_v['extra_demands_price_ti'] + $rm_v['additional_services_price_ti'] + $rm_v['additional_services_price_auto_add_ti']) currency=$objOrderCurrency} + {displayWtPriceWithCurrency price=($rm_v['amount_tax_incl'] + $rm_v['additional_services_price_ti'] + $rm_v['additional_services_price_auto_add_ti']) currency=$objOrderCurrency} {else} - {displayWtPriceWithCurrency price=($rm_v['amount_tax_excl'] + $rm_v['extra_demands_price_te'] + $rm_v['additional_services_price_te'] + $rm_v['additional_services_price_auto_add_te']) currency=$objOrderCurrency} + {displayWtPriceWithCurrency price=($rm_v['amount_tax_excl'] + $rm_v['additional_services_price_te'] + $rm_v['additional_services_price_auto_add_te']) currency=$objOrderCurrency} {/if} - {if (isset($rm_v['extra_demands']) && $rm_v['extra_demands']) || isset($rm_v['additional_services']) && $rm_v['additional_services']} + {if isset($rm_v['additional_services']) && $rm_v['additional_services']} @@ -185,9 +185,9 @@

    {if $group_use_tax} - {displayWtPriceWithCurrency price=($rm_v['extra_demands_price_ti'] + $rm_v['additional_services_price_ti']) currency=$objOrderCurrency} + {displayWtPriceWithCurrency price=($rm_v['additional_services_price_ti']) currency=$objOrderCurrency} {else} - {displayWtPriceWithCurrency price=($rm_v['extra_demands_price_te'] + $rm_v['additional_services_price_te']) currency=$objOrderCurrency} + {displayWtPriceWithCurrency price=($rm_v['additional_services_price_te']) currency=$objOrderCurrency} {/if}

    @@ -428,9 +428,9 @@ {if $priceDisplay && $use_tax} - {displayWtPriceWithCurrency price=($orderTotalInfo['total_rooms_te'] + $orderTotalInfo['total_services_te'] + $orderTotalInfo['total_auto_add_services_te'] + $orderTotalInfo['total_demands_price_te']) currency=$objOrderCurrency} + {displayWtPriceWithCurrency price=($orderTotalInfo['total_rooms_te'] + $orderTotalInfo['total_services_te'] + $orderTotalInfo['total_auto_add_services_te']) currency=$objOrderCurrency} {else} - {displayWtPriceWithCurrency price=($orderTotalInfo['total_rooms_ti'] + $orderTotalInfo['total_services_ti'] + $orderTotalInfo['total_auto_add_services_ti'] + $orderTotalInfo['total_demands_price_ti']) currency=$objOrderCurrency} + {displayWtPriceWithCurrency price=($orderTotalInfo['total_rooms_ti'] + $orderTotalInfo['total_services_ti'] + $orderTotalInfo['total_auto_add_services_ti']) currency=$objOrderCurrency} {/if} @@ -536,18 +536,9 @@ {/if}
    - {* Fancybox for extra demands*} + {* Fancybox for extra services*} {block name='order_confirmation_room_extra_services'} {/block} {block name='order_confirmation_js_vars'} diff --git a/themes/hotel-reservation-theme/order-detail.tpl b/themes/hotel-reservation-theme/order-detail.tpl index efa462dc0..4dbcddc64 100644 --- a/themes/hotel-reservation-theme/order-detail.tpl +++ b/themes/hotel-reservation-theme/order-detail.tpl @@ -328,8 +328,8 @@ {assign var=room_price_tax_excl value=$order->getTotalProductsWithoutTaxes(false, true)} {assign var=room_price_tax_incl value=$order->getTotalProductsWithTaxes(false, true)} - {assign var=room_services_price_tax_excl value=($order->getTotalProductsWithoutTaxes(false, false, Product::SELLING_PREFERENCE_WITH_ROOM_TYPE) + $total_demands_price_te)} - {assign var=room_services_price_tax_incl value=($order->getTotalProductsWithTaxes(false, false, Product::SELLING_PREFERENCE_WITH_ROOM_TYPE) + $total_demands_price_ti)} + {assign var=room_services_price_tax_excl value=($order->getTotalProductsWithoutTaxes(false, false, Product::SELLING_PREFERENCE_WITH_ROOM_TYPE))} + {assign var=room_services_price_tax_incl value=($order->getTotalProductsWithTaxes(false, false, Product::SELLING_PREFERENCE_WITH_ROOM_TYPE))} {assign var=total_tax_without_discount value=(($room_price_tax_incl - $room_price_tax_excl) + ($room_services_price_tax_incl - $room_services_price_tax_excl ))} @@ -746,8 +746,8 @@ {assign var=room_price_tax_excl value=$order->getTotalProductsWithoutTaxes(false, true)} {assign var=room_price_tax_incl value=$order->getTotalProductsWithTaxes(false, true)} - {assign var=room_services_price_tax_excl value=($order->getTotalProductsWithoutTaxes(false, false, Product::SELLING_PREFERENCE_WITH_ROOM_TYPE) + $total_demands_price_te)} - {assign var=room_services_price_tax_incl value=($order->getTotalProductsWithTaxes(false, false, Product::SELLING_PREFERENCE_WITH_ROOM_TYPE) + $total_demands_price_ti)} + {assign var=room_services_price_tax_excl value=($order->getTotalProductsWithoutTaxes(false, false, Product::SELLING_PREFERENCE_WITH_ROOM_TYPE))} + {assign var=room_services_price_tax_incl value=($order->getTotalProductsWithTaxes(false, false, Product::SELLING_PREFERENCE_WITH_ROOM_TYPE))} {assign var=total_standard_products_tax_incl value=($order->getTotalProductsWithTaxes(false, false, Product::SELLING_PREFERENCE_STANDALONE) + $order->getTotalProductsWithTaxes(false, false, Product::SELLING_PREFERENCE_HOTEL_STANDALONE))} {assign var=total_standard_products_tax_excl value=($order->getTotalProductsWithoutTaxes(false, false, Product::SELLING_PREFERENCE_STANDALONE) + $order->getTotalProductsWithoutTaxes(false, false, Product::SELLING_PREFERENCE_HOTEL_STANDALONE))} @@ -1005,9 +1005,7 @@ {* Services are indexed with id_htl_booking *} {assign var='has_services' value=(isset($rm_v.additional_services) && isset($rm_v.additional_services[$hotel_booking_detail.id_htl_booking]) && isset($rm_v.additional_services[$hotel_booking_detail.id_htl_booking]['additional_services']))} - {* Additional Facilities are indexed with id_room *} - {assign var='has_facilities' value=(isset($rm_v.extra_demands) && isset($rm_v.extra_demands[$hotel_booking_detail.id_room]) && isset($rm_v.extra_demands[$hotel_booking_detail.id_room]['extra_demands']))} - {if $has_services || $has_facilities} + {if $has_services}
    {if $has_services}
    @@ -1021,18 +1019,6 @@
    {/if} - {if $has_facilities} -
    -
    - {l s='Facilities'} -
    -
    - {foreach from=$rm_v.extra_demands[$hotel_booking_detail.id_room]['extra_demands'] item=facility} - {$facility.name} - {/foreach} -
    -
    - {/if}
    {else}
    diff --git a/themes/hotel-reservation-theme/order-opc.tpl b/themes/hotel-reservation-theme/order-opc.tpl index 81115a208..a9dac4fc6 100644 --- a/themes/hotel-reservation-theme/order-opc.tpl +++ b/themes/hotel-reservation-theme/order-opc.tpl @@ -392,9 +392,8 @@ {addJsDefL name=titleDelivery}{$smarty.capture.default|@addcslashes:'\''}{/addJsDefL} {capture}{l s='Update' js=1}{/capture} {addJsDefL name=liUpdate}{$smarty.capture.default|@addcslashes:'\''}{/addJsDefL} - {addJsDefL name=txtExtraDemandSucc}{l s='Updated Successfully' js=1}{/addJsDefL} + {addJsDefL name=txtExtraServiceSucc}{l s='Updated Successfully' js=1}{/addJsDefL} {addJsDefL name=txtMaxQuantityAdded}{l s='Maximum quantity of service added' js=1}{/addJsDefL} - {addJsDefL name=txtExtraDemandErr}{l s='Some error occurred while updating demands' js=1}{/addJsDefL} {/strip} {/block} {/if} diff --git a/themes/hotel-reservation-theme/product.tpl b/themes/hotel-reservation-theme/product.tpl index 67f4a03fe..8ff2aef01 100644 --- a/themes/hotel-reservation-theme/product.tpl +++ b/themes/hotel-reservation-theme/product.tpl @@ -439,40 +439,6 @@ {block name='booking_form'} {include file='./_partials/booking-form.tpl'} {/block} - - {block name='product_demands'} - {* extra room type demands *} - {if isset($room_type_demands) && $room_type_demands} -
    - - {foreach $room_type_demands as $idGlobalDemand => $demand} -
    - {if $product->show_price && !isset($restricted_country_mode) && !$PS_CATALOG_MODE} -
    -

    - -

    -
    - {/if} -
    -

    {$demand['name']|escape:'html':'UTF-8'} {if $product->show_price && !isset($restricted_country_mode) && !$PS_CATALOG_MODE}{convertPrice price = $demand['price']}{if $demand['price_calc_method'] == $WK_PRICE_CALC_METHOD_EACH_DAY}{l s='/Night'}{/if}{/if}

    - {if isset($demand['adv_option']) && $demand['adv_option']} - - {else} - - {/if} -
    -
    - {/foreach} -
    -
    -
    - {/if} - {/block} {/if} {block name='displayRightColumnProduct'} {if isset($HOOK_EXTRA_RIGHT) && $HOOK_EXTRA_RIGHT}{$HOOK_EXTRA_RIGHT}{/if} diff --git a/themes/hotel-reservation-theme/shopping-cart-detail.tpl b/themes/hotel-reservation-theme/shopping-cart-detail.tpl index ccbb199e7..973fa9620 100644 --- a/themes/hotel-reservation-theme/shopping-cart-detail.tpl +++ b/themes/hotel-reservation-theme/shopping-cart-detail.tpl @@ -118,7 +118,7 @@ {/if}
    -
    +

    @@ -141,12 +141,12 @@

    - {if (isset($data_v['extra_demands']) && $data_v['extra_demands']) || (isset($data_v['service_products']) && $data_v['service_products'])} + {if (isset($data_v['service_products']) && $data_v['service_products'])}
    -
    -

    +

    +

    - {displayPrice price=$rm_v['demand_price']} + {displayPrice price=$rm_v['additional_price']}

    @@ -165,7 +165,7 @@

    - {displayPrice price=($rm_v['amount']+$rm_v['demand_price'])} + {displayPrice price=($rm_v['amount']+$rm_v['additional_price'])}

    diff --git a/themes/hotel-reservation-theme/shopping-cart.tpl b/themes/hotel-reservation-theme/shopping-cart.tpl index 06b09b5bb..dd2fbfd94 100644 --- a/themes/hotel-reservation-theme/shopping-cart.tpl +++ b/themes/hotel-reservation-theme/shopping-cart.tpl @@ -87,7 +87,7 @@ {/block} {/if} -{* Fancybox for extra demands*} +{* Fancybox for extra services *} {block name='shopping_cart_extra_services'}

    From 6f2d1e599e4397c0cb98b974d1d11f6541a801c3 Mon Sep 17 00:00:00 2001 From: amit sharma Date: Thu, 16 Jul 2026 17:04:53 +0530 Subject: [PATCH 13/78] Fixed: initial commit Co-authored-by: Name --- .../sources/helpers/list/list_header.tpl | 18 + .../sources/modals/_merge_source_form.tpl | 36 + classes/BoOrder.php | 1 + classes/BusinessSource.php | 237 ++++++ classes/PaymentModule.php | 7 + classes/Source.php | 222 ++++++ controllers/admin/AdminOrdersController.php | 38 + controllers/admin/AdminSourcesController.php | 709 ++++++++++++++++++ install/data/db_structure.sql | 44 ++ install/data/xml/business_source.xml | 21 + install/data/xml/source.xml | 34 + install/data/xml/tab.xml | 3 + install/langs/en/data/business_source.xml | 18 + install/langs/en/data/source.xml | 36 + install/langs/en/data/tab.xml | 1 + install/models/install.php | 8 + js/admin/sources.js | 44 ++ 17 files changed, 1477 insertions(+) create mode 100644 admin/themes/default/template/controllers/sources/helpers/list/list_header.tpl create mode 100644 admin/themes/default/template/controllers/sources/modals/_merge_source_form.tpl create mode 100644 classes/BusinessSource.php create mode 100644 classes/Source.php create mode 100644 controllers/admin/AdminSourcesController.php create mode 100644 install/data/xml/business_source.xml create mode 100644 install/data/xml/source.xml create mode 100644 install/langs/en/data/business_source.xml create mode 100644 install/langs/en/data/source.xml create mode 100644 js/admin/sources.js diff --git a/admin/themes/default/template/controllers/sources/helpers/list/list_header.tpl b/admin/themes/default/template/controllers/sources/helpers/list/list_header.tpl new file mode 100644 index 000000000..13245ca07 --- /dev/null +++ b/admin/themes/default/template/controllers/sources/helpers/list/list_header.tpl @@ -0,0 +1,18 @@ +{* +* 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 +* +* @author Webkul IN +* @copyright Since 2010 Webkul +* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0 +*} +{extends file="helpers/list/list_header.tpl"} +{block name="override_form_extra"} + {if isset($smarty.get.id_source_type)} + + {/if} +{/block} diff --git a/admin/themes/default/template/controllers/sources/modals/_merge_source_form.tpl b/admin/themes/default/template/controllers/sources/modals/_merge_source_form.tpl new file mode 100644 index 000000000..8002b45f4 --- /dev/null +++ b/admin/themes/default/template/controllers/sources/modals/_merge_source_form.tpl @@ -0,0 +1,36 @@ +{* +* 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 +* +* @author Webkul IN +* @copyright Since 2010 Webkul +* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0 +*} +
    + + +
    diff --git a/classes/BoOrder.php b/classes/BoOrder.php index f08c766f6..adc4d423a 100644 --- a/classes/BoOrder.php +++ b/classes/BoOrder.php @@ -32,5 +32,6 @@ class BoOrder extends PaymentModule $this->validateOrderAmount = false; // Default payment type for backoffice order should be PAY_AT_HOTEL $this->payment_type = OrderPayment::PAYMENT_TYPE_PAY_AT_HOTEL; + $this->idBookingSource = Source::getIdByCode('WALKIN'); } } \ No newline at end of file diff --git a/classes/BusinessSource.php b/classes/BusinessSource.php new file mode 100644 index 000000000..8aa4da30c --- /dev/null +++ b/classes/BusinessSource.php @@ -0,0 +1,237 @@ + 'business_source', + 'primary' => 'id_source_type', + 'multilang' => true, + 'fields' => array( + 'source_type_code' => array('type' => self::TYPE_STRING, 'validate' => 'isModuleName', 'required' => true, 'size' => 64), + 'position' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'), + 'unremovable' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), + 'active' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), + 'deleted' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), + 'date_add' => array('type' => self::TYPE_DATE, 'validate' => 'isDate'), + 'date_upd' => array('type' => self::TYPE_DATE, 'validate' => 'isDate'), + 'name' => array('type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'required' => true, 'size' => 64), + ), + ); + + public function add($autodate = true, $nullValues = false) + { + if ($this->position <= 0) { + $this->position = BusinessSource::getHigherPosition() + 1; + } + + return parent::add($autodate, $nullValues); + } + + /** + * @return int Highest current position among non-deleted Business Sources, or -1 if none exist + */ + public static function getHigherPosition() + { + $position = Db::getInstance()->getValue( + 'SELECT MAX(`position`) FROM `'._DB_PREFIX_.'business_source` WHERE `deleted` = 0' + ); + + return is_numeric($position) ? (int)$position : -1; + } + + /** + * Move this Business Source up/down the list. + * @param bool $way Up (1) or Down (0) + * @param int $position + * @return bool + */ + public function updatePosition($way, $position) + { + if (!$res = Db::getInstance()->executeS(' + SELECT `position`, `id_source_type` + FROM `'._DB_PREFIX_.'business_source` + WHERE `deleted` = 0 + ORDER BY `position` ASC' + )) { + return false; + } + + $moved_item = null; + foreach ($res as $row) { + if ((int)$row['id_source_type'] == (int)$this->id) { + $moved_item = $row; + } + } + + if (!$moved_item) { + return false; + } + + return (Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'business_source` + SET `position` = `position` '.($way ? '- 1' : '+ 1').' + WHERE `deleted` = 0 AND `position` + '.($way + ? '> '.(int)$moved_item['position'].' AND `position` <= '.(int)$position + : '< '.(int)$moved_item['position'].' AND `position` >= '.(int)$position) + ) && Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'business_source` + SET `position` = '.(int)$position.' + WHERE `id_source_type` = '.(int)$moved_item['id_source_type'])); + } + + /** + * Reorder Business Source positions into a contiguous 0..N-1 sequence. Call after a delete. + * @return bool + */ + public static function cleanPositions() + { + $return = true; + + $result = Db::getInstance()->executeS(' + SELECT `id_source_type` + FROM `'._DB_PREFIX_.'business_source` + WHERE `deleted` = 0 + ORDER BY `position` ASC' + ); + + $i = 0; + foreach ($result as $row) { + $return = Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'business_source` + SET `position` = '.(int)$i++.' + WHERE `id_source_type` = '.(int)$row['id_source_type'] + ) && $return; + } + + return $return; + } + + public static function codeExists($code, $idExclude = 0) + { + return (bool)Db::getInstance()->getValue( + 'SELECT `id_source_type` FROM `'._DB_PREFIX_.'business_source` + WHERE `source_type_code` = \''.pSQL($code).'\' AND `id_source_type` != '.(int)$idExclude + ); + } + + public static function getUnremovableIds() + { + $rows = Db::getInstance()->executeS( + 'SELECT `id_source_type` FROM `'._DB_PREFIX_.'business_source` WHERE `unremovable` = 1 AND `deleted` = 0' + ); + + return array_map('intval', array_column($rows, 'id_source_type')); + } + + public static function getActiveList($idLang) + { + return Db::getInstance()->executeS(' + SELECT bs.`id_source_type`, bsl.`name` + FROM `'._DB_PREFIX_.'business_source` bs + LEFT JOIN `'._DB_PREFIX_.'business_source_lang` bsl + ON (bsl.`id_source_type` = bs.`id_source_type` AND bsl.`id_lang` = '.(int)$idLang.') + WHERE bs.`active` = 1 AND bs.`deleted` = 0 + ORDER BY bs.`position` ASC + '); + } + + public function isRemovable() + { + return !$this->unremovable; + } + + /** + * @return bool True if any Booking Source under this Business Source — deleted or not — + * is referenced by an order. A soft-deleted Booking Source still referenced by a historical + * order must keep resolving to a Business Source name too, so this still blocks deletion. + */ + public function hasSourceUsedByOrder() + { + return (bool)Db::getInstance()->getValue( + 'SELECT COUNT(*) FROM `'._DB_PREFIX_.'source` s + INNER JOIN `'._DB_PREFIX_.'orders` o ON o.`id_source` = s.`id_source` + WHERE s.`id_source_type` = '.(int)$this->id + ); + } + + /** + * @return int Total orders placed across every Booking Source under this Business Source — + * the denominator for each Booking Source's own percentage within this category's + * drill-down list (as opposed to a share of every order in the system). + */ + public function getOrderCount() + { + return (int)Db::getInstance()->getValue( + 'SELECT COUNT(*) FROM `'._DB_PREFIX_.'source` s + INNER JOIN `'._DB_PREFIX_.'orders` o ON o.`id_source` = s.`id_source` + WHERE s.`id_source_type` = '.(int)$this->id + ); + } + + /** + * Deleting a Business Source deletes every Booking Source under it (each through its own + * delete() rule — soft-deleted if order-linked, hard-deleted otherwise). If any of them + * turns out to be order-linked, this Business Source is soft-deleted too, the same way, so + * historical orders keep resolving a category name; otherwise it hard-deletes for real. + */ + public function delete() + { + if (!$this->isRemovable()) { + return false; + } + + $keepSoft = $this->hasSourceUsedByOrder(); + + $sources = Db::getInstance()->executeS( + 'SELECT `id_source` FROM `'._DB_PREFIX_.'source` WHERE `id_source_type` = '.(int)$this->id.' AND `deleted` = 0' + ); + foreach ($sources as $row) { + $objSource = new Source((int)$row['id_source']); + if (!$objSource->delete()) { + return false; + } + } + + if ($keepSoft) { + $this->deleted = 1; + if (!$this->update()) { + return false; + } + } elseif (!parent::delete()) { + return false; + } + + return BusinessSource::cleanPositions(); + } +} diff --git a/classes/PaymentModule.php b/classes/PaymentModule.php index fe1ce4392..489edb743 100644 --- a/classes/PaymentModule.php +++ b/classes/PaymentModule.php @@ -352,6 +352,13 @@ abstract class PaymentModuleCore extends Module $order->source = Configuration::get('PS_SHOP_DOMAIN'); } + // Booking Source (business categorization, e.g. Direct Website/Walk-in/OTA) — + if (isset($this->idBookingSource) && $this->idBookingSource) { + $order->id_source = (int)$this->idBookingSource; + } else { + $order->id_source = Source::getDefaultSourceId(); + } + if (self::DEBUG_MODE) { PrestaShopLogger::addLog('PaymentModule::validateOrder - Order is about to be added', 1, null, 'Cart', (int)$id_cart, true); } diff --git a/classes/Source.php b/classes/Source.php new file mode 100644 index 000000000..6e687e454 --- /dev/null +++ b/classes/Source.php @@ -0,0 +1,222 @@ + 'source', + 'primary' => 'id_source', + 'multilang' => true, + 'fields' => array( + 'id_source_type' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true), + 'source_code' => array('type' => self::TYPE_STRING, 'validate' => 'isModuleName', 'required' => true, 'size' => 64), + 'position' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'), + 'unremovable' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), + 'active' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), + 'deleted' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), + 'date_add' => array('type' => self::TYPE_DATE, 'validate' => 'isDate'), + 'date_upd' => array('type' => self::TYPE_DATE, 'validate' => 'isDate'), + 'name' => array('type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'required' => true, 'size' => 64), + ), + ); + + public function add($autodate = true, $nullValues = false) + { + if ($this->position <= 0) { + $this->position = Source::getHigherPosition($this->id_source_type) + 1; + } + + return parent::add($autodate, $nullValues); + } + + public static function getHigherPosition($idSourceType) + { + $position = Db::getInstance()->getValue( + 'SELECT MAX(`position`) FROM `'._DB_PREFIX_.'source` + WHERE `id_source_type` = '.(int)$idSourceType.' AND `deleted` = 0' + ); + + return is_numeric($position) ? (int)$position : -1; + } + + public function updatePosition($way, $position) + { + if (!$res = Db::getInstance()->executeS(' + SELECT `position`, `id_source` + FROM `'._DB_PREFIX_.'source` + WHERE `id_source_type` = '.(int)$this->id_source_type.' AND `deleted` = 0 + ORDER BY `position` ASC' + )) { + return false; + } + + $moved_item = null; + foreach ($res as $row) { + if ((int)$row['id_source'] == (int)$this->id) { + $moved_item = $row; + } + } + + if (!$moved_item) { + return false; + } + + return (Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'source` + SET `position` = `position` '.($way ? '- 1' : '+ 1').' + WHERE `id_source_type` = '.(int)$this->id_source_type.' AND `deleted` = 0 AND `position` + '.($way + ? '> '.(int)$moved_item['position'].' AND `position` <= '.(int)$position + : '< '.(int)$moved_item['position'].' AND `position` >= '.(int)$position) + ) && Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'source` + SET `position` = '.(int)$position.' + WHERE `id_source` = '.(int)$moved_item['id_source'])); + } + + public static function codeExists($code, $idExclude = 0) + { + return (bool)Db::getInstance()->getValue( + 'SELECT `id_source` FROM `'._DB_PREFIX_.'source` + WHERE `source_code` = \''.pSQL($code).'\' AND `id_source` != '.(int)$idExclude + ); + } + + public static function getIdByCode($code) + { + return (int)Db::getInstance()->getValue( + 'SELECT `id_source` FROM `'._DB_PREFIX_.'source` + WHERE `source_code` = \''.pSQL($code).'\' AND `deleted` = 0' + ); + } + + public static function getUnremovableIds() + { + $rows = Db::getInstance()->executeS( + 'SELECT `id_source` FROM `'._DB_PREFIX_.'source` WHERE `unremovable` = 1 AND `deleted` = 0' + ); + + return array_map('intval', array_column($rows, 'id_source')); + } + + public static function getActiveListForType($idSourceType, $idLang) + { + return Db::getInstance()->executeS(' + SELECT a.`id_source`, al.`name` + FROM `'._DB_PREFIX_.'source` a + LEFT JOIN `'._DB_PREFIX_.'source_lang` al ON (al.`id_source` = a.`id_source` AND al.`id_lang` = '.(int)$idLang.') + WHERE a.`id_source_type` = '.(int)$idSourceType.' AND a.`deleted` = 0 AND a.`active` = 1 + ORDER BY a.`position` ASC + '); + } + + public static function getSourcesUsedInOrders($idLang) + { + return Db::getInstance()->executeS(' + SELECT DISTINCT o.`id_source`, sl.`name` + FROM `'._DB_PREFIX_.'orders` o + INNER JOIN `'._DB_PREFIX_.'source` s ON (s.`id_source` = o.`id_source`) + LEFT JOIN `'._DB_PREFIX_.'source_lang` sl ON (sl.`id_source` = s.`id_source` AND sl.`id_lang` = '.(int)$idLang.') + '); + } + + public static function cleanPositions($idSourceType) + { + $return = true; + + $result = Db::getInstance()->executeS(' + SELECT `id_source` + FROM `'._DB_PREFIX_.'source` + WHERE `id_source_type` = '.(int)$idSourceType.' AND `deleted` = 0 + ORDER BY `position` ASC' + ); + + $i = 0; + foreach ($result as $row) { + $return = Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'source` + SET `position` = '.(int)$i++.' + WHERE `id_source` = '.(int)$row['id_source'] + ) && $return; + } + + return $return; + } + + public function isRemovable() + { + return !$this->unremovable; + } + + public function isUsedByOrder() + { + return (bool)Db::getInstance()->getValue( + 'SELECT COUNT(*) FROM `'._DB_PREFIX_.'orders` WHERE `id_source` = '.(int)$this->id + ); + } + + public function mergeOrdersInto($idTargetSource) + { + return (bool)Db::getInstance()->execute( + 'UPDATE `'._DB_PREFIX_.'orders` SET `id_source` = '.(int)$idTargetSource.' WHERE `id_source` = '.(int)$this->id + ); + } + + public static function getTotalOrderCount() + { + return (int)Db::getInstance()->getValue('SELECT COUNT(*) FROM `'._DB_PREFIX_.'orders`'); + } + + public function delete() + { + if (!$this->isRemovable()) { + return false; + } + + if ($this->isUsedByOrder()) { + $this->deleted = 1; + if (!$this->update()) { + return false; + } + } elseif (!parent::delete()) { + return false; + } + + return Source::cleanPositions($this->id_source_type); + } + + public static function getDefaultSourceId() + { + return (int)Configuration::get('PS_DEFAULT_BOOKING_SOURCE'); + } +} diff --git a/controllers/admin/AdminOrdersController.php b/controllers/admin/AdminOrdersController.php index bcbe5868c..a690ffb10 100644 --- a/controllers/admin/AdminOrdersController.php +++ b/controllers/admin/AdminOrdersController.php @@ -33,6 +33,7 @@ class AdminOrdersControllerCore extends AdminController protected $statuses_array = array(); protected $all_order_sources = array(); + protected $bookingSourcesArray = array(); protected $hotelsArray = array(); protected $roomTypesArray = array(); protected $roomsArray = array(); @@ -74,6 +75,10 @@ class AdminOrdersControllerCore extends AdminController hbd.`id_room` AS id_room_information, (SELECT COUNT(spod.`id_service_product_order_detail`) FROM `'._DB_PREFIX_.'service_product_order_detail` spod WHERE spod.`id_order` = a.`id_order` AND spod.`id_htl_booking_detail`=0) as num_products'; + $this->_select .= ', + (SELECT sl.`name` FROM `'._DB_PREFIX_.'source` s LEFT JOIN `'._DB_PREFIX_.'source_lang` sl ON (sl.`id_source` = s.`id_source` AND sl.`id_lang` = '.(int)$this->context->language->id.') WHERE s.`id_source` = a.`id_source`) AS `booking_source_name`, + (SELECT s.`source_code` FROM `'._DB_PREFIX_.'source` s WHERE s.`id_source` = a.`id_source`) AS `booking_source_code`'; + $this->_join = ' LEFT JOIN `'._DB_PREFIX_.'customer` c ON (c.`id_customer` = a.`id_customer`) LEFT JOIN `'._DB_PREFIX_.'currency` cu ON (cu.`id_currency` = a.`id_currency`) @@ -113,6 +118,9 @@ class AdminOrdersControllerCore extends AdminController foreach ($all_order_sources as $source) { $this->all_order_sources[$source['source']] = $source['source']; } + foreach (Source::getSourcesUsedInOrders((int)$this->context->language->id) as $bookingSource) { + $this->bookingSourcesArray[$bookingSource['id_source']] = $bookingSource['name']; + } $hotelsArray = HotelBranchInformation::getProfileAccessedHotels($this->context->employee->id_profile, 1); foreach ($hotelsArray as $hotel) { @@ -272,6 +280,16 @@ class AdminOrdersControllerCore extends AdminController 'optional' => true, 'visible_default' => true ), + 'booking_source_name' => array( + 'title' => $this->l('Booking Source'), + 'type' => 'select', + 'filter_key' => 'a!id_source', + 'list' => $this->bookingSourcesArray, + 'orderby' => false, + 'callback' => 'formatBookingSource', + 'optional' => true, + 'visible_default' => true + ), 'osname' => array( 'title' => $this->l('Status'), 'type' => 'select', @@ -359,6 +377,26 @@ class AdminOrdersControllerCore extends AdminController return Tools::displayPrice($echo, (int)$idCurrency); } + /** + * fields_list callback for the Booking Source column — shows the Booking Source name and, + * only for the built-in "Direct Website" source, a link to the storefront alongside it. + */ + public static function formatBookingSource($echo, $row) + { + if (!$echo) { + return '--'; + } + + $html = Tools::safeOutput($echo); + + if (isset($row['booking_source_code']) && $row['booking_source_code'] === 'DIRECT_WEBSITE') { + $title = Translate::getAdminTranslation('Visit website', 'AdminOrders', true); + $html .= ' '; + } + + return $html; + } + public function initPageHeaderToolbar() { if (empty($this->display)) { diff --git a/controllers/admin/AdminSourcesController.php b/controllers/admin/AdminSourcesController.php new file mode 100644 index 000000000..de80d4a01 --- /dev/null +++ b/controllers/admin/AdminSourcesController.php @@ -0,0 +1,709 @@ +bootstrap = true; + $this->table = 'business_source'; + $this->className = 'BusinessSource'; + $this->identifier = 'id_source_type'; + $this->list_id = 'business_source'; + $this->lang = true; + $this->deleted = false; + $this->_defaultOrderBy = 'position'; + $this->_defaultOrderWay = 'ASC'; + $this->context = Context::getContext(); + + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->l('Delete selected'), + 'icon' => 'icon-trash', + 'confirm' => $this->l('Delete selected items?') + ) + ); + + parent::__construct(); + } + + public function setMedia() + { + parent::setMedia(); + + $this->addJS(_PS_JS_DIR_.'admin/sources.js'); + } + + public function init() + { + if (Tools::isSubmit('viewbusiness_source') + || Tools::isSubmit('submitFiltersource') + || Tools::isSubmit('submitResetsource') + || Tools::getValue('sourceOrderby') + || Tools::getValue('sourceOrderway') + ) { + $this->list_id = 'source'; + } + + $idSourceType = (int) Tools::getValue('id_source_type'); + + if ($idSourceType && Tools::getValue('viewbusiness_source')) { + self::$currentIndex .= '&id_source_type=' . $idSourceType . '&viewbusiness_source'; + $this->className = 'Source'; + } + + if (Tools::isSubmit('addsource') || Tools::isSubmit('addbusiness_source')) { + $this->display = 'add'; + } + if (Tools::isSubmit('updatesource') || Tools::isSubmit('updatebusiness_source')) { + $this->display = 'edit'; + } + if (Tools::isSubmit('submitAddsource') || Tools::isSubmit('submitAddsourceAndStay')) { + $this->display = Tools::getValue('id_source') ? 'edit' : 'add'; + } + if (Tools::isSubmit('submitAddbusiness_source') || Tools::isSubmit('submitAddbusiness_sourceAndStay')) { + $this->display = Tools::getValue('id_source_type') ? 'edit' : 'add'; + } + + + return parent::init(); + } + + /** + * @see AdminController::initPageHeaderToolbar() + */ + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_business_source'] = array( + 'href' => self::$currentIndex.'&addbusiness_source&token='.$this->token, + 'desc' => $this->l('Add new Business Source', null, null, false), + 'icon' => 'process-icon-new' + ); + } elseif ($this->display == 'view') { + $idSourceType = (int)Tools::getValue('id_source_type'); + if ($idSourceType) { + $this->page_header_toolbar_btn['new_source'] = array( + 'href' => self::$currentIndex.'&addsource&id_source_type='.$idSourceType.'&token='.$this->token, + 'desc' => $this->l('Add new Booking Source', null, null, false), + 'icon' => 'process-icon-new' + ); + $this->page_header_toolbar_btn['back_to_business'] = array( + 'href' => self::$currentIndex.'&token='.$this->token, + 'desc' => $this->l('Back To Business Source', null, null, false), + 'icon' => 'process-icon-back' + ); + } + } + + unset($this->toolbar_btn['back']); + parent::initPageHeaderToolbar(); + } + + + + protected function initBusinessSourcesList() + { + $this->fields_list = array( + 'id_source_type' => array( + 'title' => $this->l('ID'), + 'align' => 'text-center', + 'class' => 'fixed-width-xs' + ), + 'name' => array( + 'title' => $this->l('Name'), + 'width' => 'auto', + 'filter_key' => 'b!name' + ), + 'source_type_code' => array( + 'title' => $this->l('Code'), + 'align' => 'text-center' + ), + 'position' => array( + 'title' => $this->l('Position'), + 'filter_key' => 'a!position', + 'align' => 'text-center', + 'class' => 'fixed-width-xs', + 'position' => 'position' + ), + 'orders_count' => array( + 'title' => $this->l('Orders'), + 'align' => 'text-center', + 'orderby' => false, + 'search' => false, + 'callback' => 'formatOrdersPercentage', + ), + 'active' => array( + 'title' => $this->l('Enabled'), + 'active' => 'status', + 'type' => 'bool', + 'align' => 'text-center', + 'class' => 'fixed-width-xs', + 'orderby' => false + ), + ); + } + + protected function initSourcesList() + { + $this->fields_list = array( + 'id_source' => array( + 'title' => $this->l('ID'), + 'align' => 'text-center', + 'class' => 'fixed-width-xs' + ), + 'name' => array( + 'title' => $this->l('Name'), + 'width' => 'auto', + 'filter_key' => 'b!name' + ), + 'source_type_name' => array( + 'title' => $this->l('Business Source'), + 'orderby' => false, + 'search' => false, + ), + 'source_code' => array( + 'title' => $this->l('Code'), + 'align' => 'text-center' + ), + 'position' => array( + 'title' => $this->l('Position'), + 'filter_key' => 'a!position', + 'align' => 'text-center', + 'class' => 'fixed-width-xs', + 'position' => 'position' + ), + 'orders_count' => array( + 'title' => $this->l('Orders'), + 'align' => 'text-center', + 'orderby' => false, + 'search' => false, + 'callback' => 'formatOrdersPercentage', + ), + 'active' => array( + 'title' => $this->l('Enabled'), + 'active' => 'status', + 'type' => 'bool', + 'align' => 'text-center', + 'class' => 'fixed-width-xs', + 'orderby' => false + ), + ); + } + + public function renderList() + { + $this->addRowAction('view'); + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + $this->addRowActionSkipList('delete', BusinessSource::getUnremovableIds()); + $this->_where = 'AND a.`deleted` = 0'; + $this->_select = '(SELECT COUNT(*) FROM `'._DB_PREFIX_.'orders` o + INNER JOIN `'._DB_PREFIX_.'source` s ON s.`id_source` = o.`id_source` + WHERE s.`id_source_type` = a.`id_source_type`) AS `orders_count`'; + $this->ordersTotal = Source::getTotalOrderCount(); + $this->initBusinessSourcesList(); + $this->toolbar_title = $this->l('Business Source'); + + return parent::renderList(); + } + + public function renderView() + { + $idSourceType = (int)Tools::getValue('id_source_type'); + $objBusinessSource = new BusinessSource($idSourceType, (int)$this->context->language->id); + if (!Validate::isLoadedObject($objBusinessSource)) { + return; + } + + $this->addRowAction('edit'); + $this->addRowAction('merge'); + $this->addRowAction('delete'); + $this->addRowActionSkipList('delete', Source::getUnremovableIds()); + + $this->table = 'source'; + $this->className = 'Source'; + $this->identifier = 'id_source'; + $this->list_id = 'source'; + $this->_select = 'bsl.`name` AS `source_type_name`, + (SELECT COUNT(*) FROM `'._DB_PREFIX_.'orders` o WHERE o.`id_source` = a.`id_source`) AS `orders_count`'; + $this->ordersTotal = $objBusinessSource->getOrderCount(); + $this->_join = 'LEFT JOIN `'._DB_PREFIX_.'business_source` bs ON (bs.`id_source_type` = a.`id_source_type`)'; + $this->_join .= 'LEFT JOIN `'._DB_PREFIX_.'business_source_lang` bsl ON (bsl.`id_source_type` = bs.`id_source_type` AND bsl.`id_lang` = '.(int)$this->context->language->id.')'; + $this->_where = 'AND a.`deleted` = 0 AND a.`id_source_type` = '.(int)$objBusinessSource->id; + $this->initSourcesList(); + + $this->toolbar_title = sprintf($this->l('Booking Source - %s'),$objBusinessSource->name); + $this->processFilter(); + return parent::renderList(); + } + + public function renderForm() + { + if (Tools::isSubmit('updatesource') + || Tools::isSubmit('addsource') + || Tools::isSubmit('submitAddsource') + || Tools::isSubmit('submitAddsourceAndStay') + ) { + return $this->renderSourceForm(); + } + + return $this->renderBusinessSourceForm(); + } + + protected function renderBusinessSourceForm() + { + if (!($objBusinessSource = $this->loadObject(true))) { + return; + } + + $this->fields_form = array( + 'legend' => array( + 'title' => $this->l('Business Source'), + 'icon' => 'icon-tags' + ), + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->l('Name'), + 'name' => 'name', + 'lang' => true, + 'required' => true, + 'hint' => $this->l('Invalid characters:').' <>;=#{}' + ), + array( + 'type' => 'text', + 'label' => $this->l('Code'), + 'name' => 'source_type_code', + 'required' => true, + 'hint' => $this->l('A short unique machine code, e.g. "OTA". Letters, numbers, underscores and hyphens only.') + ), + array( + 'type' => 'switch', + 'label' => $this->l('Enabled'), + 'name' => 'active', + 'required' => false, + 'class' => 't', + 'is_bool' => true, + 'values' => array( + array('id' => 'active_on', 'value' => 1, 'label' => $this->l('Enabled')), + array('id' => 'active_off', 'value' => 0, 'label' => $this->l('Disabled')), + ) + ), + ), + 'submit' => array( + 'title' => $this->l('Save'), + 'name' => 'submitAddbusiness_source' + ), + 'buttons' => array( + 'save-and-stay' => array( + 'title' => $this->l('Save and stay'), + 'name' => 'submitAddbusiness_sourceAndStay', + 'type' => 'submit', + 'class' => 'btn btn-default pull-right', + 'icon' => 'process-icon-save', + ), + ), + ); + + return parent::renderForm(); + } + + protected function renderSourceForm() + { + $this->table = 'source'; + $this->className = 'Source'; + $this->identifier = 'id_source'; + $this->object = null; + + if (!($objSource = $this->loadObject(true))) { + return; + } + + $businessSources = BusinessSource::getActiveList((int)$this->context->language->id); + + $this->fields_form = array( + 'legend' => array( + 'title' => $this->l('Booking Source'), + 'icon' => 'icon-tags' + ), + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->l('Name'), + 'name' => 'name', + 'lang' => true, + 'required' => true, + 'hint' => $this->l('Invalid characters:').' <>;=#{}' + ), + array( + 'type' => 'select', + 'label' => $this->l('Business Source'), + 'name' => 'id_source_type', + 'required' => true, + 'options' => array( + 'query' => $businessSources, + 'id' => 'id_source_type', + 'name' => 'name', + ), + ), + array( + 'type' => 'text', + 'label' => $this->l('Source Code'), + 'name' => 'source_code', + 'required' => true, + 'hint' => $this->l('A short unique machine code, e.g. "BOOKING_COM". Letters, numbers, underscores and hyphens only.') + ), + array( + 'type' => 'switch', + 'label' => $this->l('Enabled'), + 'name' => 'active', + 'required' => false, + 'class' => 't', + 'is_bool' => true, + 'values' => array( + array('id' => 'active_on', 'value' => 1, 'label' => $this->l('Enabled')), + array('id' => 'active_off', 'value' => 0, 'label' => $this->l('Disabled')), + ) + ), + ), + 'submit' => array( + 'title' => $this->l('Save'), + 'name' => 'submitAddsource' + ), + 'buttons' => array( + 'save-and-stay' => array( + 'title' => $this->l('Save and stay'), + 'name' => 'submitAddsourceAndStay', + 'type' => 'submit', + 'class' => 'btn btn-default pull-right', + 'icon' => 'process-icon-save', + ), + ), + ); + + return parent::renderForm(); + } + + + public function postProcess() + { + if (Tools::isSubmit('submitAddsource') || Tools::isSubmit('submitAddsourceAndStay')) { + return $this->postProcessSource(); + } elseif (Tools::isSubmit('submitMergeSource')) { + return $this->postProcessMergeSource(); + } elseif (Tools::isSubmit('deletesource')) { + $objSource = new Source((int)Tools::getValue('id_source')); + if (!$objSource->isRemovable()) { + $this->errors[] = $this->l('For security reasons, you cannot delete a built-in Booking Source.'); + } elseif (!$objSource->delete()) { + $this->errors[] = Tools::displayError('An error occurred while deleting the Booking Source.'); + } else { + Tools::redirectAdmin(self::$currentIndex.'&viewbusiness_source&id_source_type='.(int)$objSource->id_source_type.'&conf=1&token='.$this->token); + } + } else if(Tools::isSubmit('submitBulkenableSelectionsource')){ + $idSourceType = (int)Tools::getValue('id_source_type'); + $this->className = 'Source'; + $this->table = 'source'; + $this->boxes = Tools::getValue('sourceBox'); + parent::processBulkEnableSelection(); + $this->redirect_after = self::$currentIndex.'&viewbusiness_source&id_source_type='.(int)$idSourceType.'&token='.$this->token; + } else if(Tools::isSubmit('submitBulkdisableSelectionsource')){ + $idSourceType = (int)Tools::getValue('id_source_type'); + $this->className = 'Source'; + $this->table = 'source'; + $this->boxes = Tools::getValue('sourceBox'); + parent::processBulkDisableSelection(); + $this->redirect_after = self::$currentIndex.'&viewbusiness_source&id_source_type='.(int)$idSourceType.'&token='.$this->token; + } elseif (Tools::isSubmit('submitBulkdeletesource')) { + $idSourceType = 0; + foreach (Tools::getValue('sourceBox') as $selection) { + $objSource = new Source((int)$selection); + $idSourceType = $objSource->id_source_type; + if (!$objSource->isRemovable()) { + $this->errors[] = $this->l('For security reasons, you cannot delete a built-in Booking Source.'); + break; + } + } + + if (!count($this->errors)) { + $this->className = 'Source'; + $this->table = 'source'; + $this->boxes = Tools::getValue('sourceBox'); + parent::processBulkDelete(); + $this->redirect_after = self::$currentIndex.'&viewbusiness_source&id_source_type='.(int)$idSourceType.'&conf=1&token='.$this->token; + } + } elseif (Tools::isSubmit('submitAddbusiness_source') || Tools::isSubmit('submitAddbusiness_sourceAndStay')) { + return $this->postProcessBusinessSource(); + } elseif (Tools::isSubmit('delete'.$this->table)) { + $objBusinessSource = new BusinessSource((int)Tools::getValue('id_source_type')); + if (!$objBusinessSource->isRemovable()) { + $this->errors[] = $this->l('For security reasons, you cannot delete a built-in Business Source.'); + } else { + return parent::postProcess(); + } + } elseif (Tools::isSubmit('submitBulkdelete'.$this->table)) { + foreach (Tools::getValue($this->table.'Box') as $selection) { + $objBusinessSource = new BusinessSource((int)$selection); + if (!$objBusinessSource->isRemovable()) { + $this->errors[] = $this->l('For security reasons, you cannot delete a built-in Business Source.'); + break; + } + } + + if (!count($this->errors)) { + return parent::postProcess(); + } + } else { + return parent::postProcess(); + } + } + + protected function postProcessSource() + { + $idSource = (int)Tools::getValue('id_source'); + $idSourceType = (int)Tools::getValue('id_source_type'); + $sourceCode = trim(Tools::getValue('source_code')); + + $defaultLangId = Configuration::get('PS_LANG_DEFAULT'); + $objDefaultLanguage = Language::getLanguage((int) $defaultLangId); + $languages = Language::getLanguages(false); + if (!trim(Tools::getValue('name_'.$defaultLangId))) { + $this->errors[] = $this->l('Name is required at least in '). + $objDefaultLanguage['name']; + } else { + foreach ($languages as $lang) { + if (trim(Tools::getValue('name_'.$lang['id_lang']))) { + if (!Validate::isGenericName(Tools::getValue('name_'.$lang['id_lang']))) { + $this->errors[] = $this->l('Invalid name in ').$lang['name']; + } + } + } + } + + $objBusinessSource = new BusinessSource($idSourceType); + if (!Validate::isLoadedObject($objBusinessSource) || $objBusinessSource->deleted) { + $this->errors[] = $this->l('Please select a valid Business Source.'); + } + + if (!$sourceCode || !Validate::isModuleName($sourceCode)) { + $this->errors[] = $this->l('Source Code is invalid. Only letters, numbers, underscores and hyphens are allowed.'); + } elseif (Source::codeExists($sourceCode, $idSource)) { + $this->errors[] = $this->l('This Source Code is already used by another Booking Source.'); + } + + if (count($this->errors)) { + $this->display = $idSource ? 'edit' : 'add'; + return false; + } + + $objSource = new Source($idSource); + $objSource->id_source_type = $idSourceType; + $objSource->source_code = $sourceCode; + $objSource->active = (int)Tools::getValue('active'); + $objSource->name = array(); + foreach (Language::getIDs(false) as $idLang) { + $objSource->name[$idLang] = Tools::getValue('name_'.$idLang); + } + + if (!$objSource->save()) { + $this->errors[] = Tools::displayError('An error occurred while saving the Booking Source.'); + $this->display = $idSource ? 'edit' : 'add'; + return false; + } + + if (Tools::isSubmit('submitAddsourceAndStay')) { + Tools::redirectAdmin(self::$currentIndex.'&updatesource&id_source='.$objSource->id.'&conf='.($idSource ? 4 : 3).'&token='.$this->token); + } + + Tools::redirectAdmin(self::$currentIndex.'&viewbusiness_source&id_source_type='.$idSourceType.'&conf='.($idSource ? 4 : 3).'&token='.$this->token); + } + + protected function postProcessMergeSource() + { + $idSourceType = (int)Tools::getValue('id_source_type'); + $idCurrentSource = (int)Tools::getValue('merge_current_source'); + $idTargetSource = (int)Tools::getValue('merge_target_source'); + + $objCurrent = new Source($idCurrentSource); + $objTarget = new Source($idTargetSource); + + if (!Validate::isLoadedObject($objCurrent) || !Validate::isLoadedObject($objTarget) + || (int)$objCurrent->id_source_type !== $idSourceType || (int)$objTarget->id_source_type !== $idSourceType + ) { + $this->errors[] = $this->l('Please select valid Current and Target Booking Sources.'); + } elseif ($idCurrentSource === $idTargetSource) { + $this->errors[] = $this->l('Current and Target Booking Source cannot be the same.'); + } else { + $objCurrent->mergeOrdersInto($idTargetSource); + } + + if (!count($this->errors)) { + Tools::redirectAdmin(self::$currentIndex.'&viewbusiness_source&id_source_type='.$idSourceType.'&conf=4&token='.$this->token); + } + } + + protected function postProcessBusinessSource() + { + $idSourceType = (int)Tools::getValue('id_source_type'); + $sourceTypeCode = trim(Tools::getValue('source_type_code')); + + $defaultLangId = Configuration::get('PS_LANG_DEFAULT'); + $objDefaultLanguage = Language::getLanguage((int) $defaultLangId); + $languages = Language::getLanguages(false); + if (!trim(Tools::getValue('name_'.$defaultLangId))) { + $this->errors[] = $this->l('Name is required at least in '). + $objDefaultLanguage['name']; + } else { + foreach ($languages as $lang) { + if (trim(Tools::getValue('name_'.$lang['id_lang']))) { + if (!Validate::isGenericName(Tools::getValue('name_'.$lang['id_lang']))) { + $this->errors[] = $this->l('Invalid name in ').$lang['name']; + } + } + } + } + if (!$sourceTypeCode || !Validate::isModuleName($sourceTypeCode)) { + $this->errors[] = $this->l('Code is invalid. Only letters, numbers, underscores and hyphens are allowed.'); + } elseif (BusinessSource::codeExists($sourceTypeCode, $idSourceType)) { + $this->errors[] = $this->l('This Code is already used by another Business Source.'); + } + + if (count($this->errors)) { + $this->display = $idSourceType ? 'edit' : 'add'; + return false; + } + + return parent::postProcess(); + } + + public function formatOrdersPercentage($count, $row) + { + $percentage = $this->ordersTotal ? round(((int)$count / $this->ordersTotal) * 100, 1) : 0; + return $percentage.'%'; + } + + public function displayMergeLink($token, $id, $name = null) + { + $idSourceType = (int) Tools::getValue('id_source_type'); + + return ' + ' . $this->l('Merge') . ' + '; + } + + public function ajaxProcessInitMergeSourceModal() + { + $response = array('hasError' => 1); + + if ($this->tabAccess['edit'] === 1) { + $idSourceType = (int)Tools::getValue('id_source_type'); + $idSource = (int)Tools::getValue('id_source'); + $objSource = new Source($idSource); + + if (Validate::isLoadedObject($objSource) && (int)$objSource->id_source_type === $idSourceType) { + $this->context->smarty->assign(array( + 'sources' => Source::getActiveListForType($idSourceType, (int)$this->context->language->id), + 'id_current_source' => $idSource, + 'id_source_type' => $idSourceType, + 'current_index' => self::$currentIndex, + 'token' => $this->token, + )); + + $this->context->smarty->assign(array( + 'modal_id' => 'merge_source_modal', + 'modal_class' => 'modal-md', + 'modal_title' => $this->l('Merge Booking Source'), + 'modal_content' => $this->context->smarty->fetch('controllers/sources/modals/_merge_source_form.tpl'), + )); + + $response['hasError'] = 0; + $response['modalHtml'] = $this->context->smarty->fetch('modal.tpl'); + } + } + + die(Tools::jsonEncode($response)); + } + + protected function filterToField($key, $filter) + { + if ($this->table == 'source') { + $this->initSourcesList(); + } else { + $this->initBusinessSourcesList(); + } + + return parent::filterToField($key, $filter); + } + + public function ajaxProcessUpdatePositions() + { + if ($this->tabAccess['edit'] !== 1) { + die(json_encode(array('hasError' => true, 'errors' => $this->l('You do not have permission to edit this.')))); + } + + $way = (int)Tools::getValue('way'); + $id = (int)Tools::getValue('id'); + + if ($positions = Tools::getValue('source')) { + $className = 'Source'; + } elseif ($positions = Tools::getValue('source_type')) { + $className = 'BusinessSource'; + } else { + die(json_encode(array('hasError' => true, 'errors' => 'Unknown table for position update.'))); + } + + foreach ($positions as $position => $value) { + $pos = explode('_', $value); + + if (isset($pos[2]) && (int)$pos[2] === $id) { + if ($object = new $className((int)$pos[2])) { + if (isset($position) && $object->updatePosition($way, $position)) { + echo 'ok position '.(int)$position.' for '.$className.' '.$id; + } else { + echo '{"hasError" : true, "errors" : "Can not update tab '.(int)$id.' to position '.(int)$position.' "}'; + } + } else { + echo json_encode(array( + 'hasError' => true, + 'errors' => 'Can not update '.$className.' '.$id.' to position '.(int)$position, + )); + } + + break; + } + } + } +} diff --git a/install/data/db_structure.sql b/install/data/db_structure.sql index 9520ec459..28077bf94 100644 --- a/install/data/db_structure.sql +++ b/install/data/db_structure.sql @@ -1174,6 +1174,7 @@ CREATE TABLE `PREFIX_orders` ( `invoice_date` datetime NOT NULL, `delivery_date` datetime NOT NULL, `source` varchar(255) DEFAULT NULL, + `id_source` int(10) unsigned DEFAULT NULL, `valid` int(1) unsigned NOT NULL DEFAULT '0', `is_advance_payment` tinyint(1) NOT NULL DEFAULT '0', `advance_paid_amount` decimal(20,6) NOT NULL DEFAULT '0.00', @@ -1193,6 +1194,7 @@ CREATE TABLE `PREFIX_orders` ( KEY `id_shop_group` (`id_shop_group`), KEY (`current_state`), KEY `id_shop` (`id_shop`), + KEY `id_source` (`id_source`), INDEX `date_add`(`date_add`) ) ENGINE=ENGINE_TYPE DEFAULT CHARSET=utf8 COLLATION; @@ -1879,6 +1881,48 @@ CREATE TABLE `PREFIX_search_word` ( UNIQUE KEY `id_lang` (`id_lang`,`id_shop`, `word`) ) ENGINE=ENGINE_TYPE DEFAULT CHARSET=utf8 COLLATION; +CREATE TABLE `PREFIX_business_source` ( + `id_source_type` int(10) unsigned NOT NULL auto_increment, + `source_type_code` varchar(64) NOT NULL, + `position` int(10) unsigned NOT NULL DEFAULT '0', + `unremovable` tinyint(1) UNSIGNED NOT NULL DEFAULT '0', + `active` tinyint(1) UNSIGNED NOT NULL DEFAULT '1', + `deleted` tinyint(1) UNSIGNED NOT NULL DEFAULT '0', + `date_add` datetime NOT NULL, + `date_upd` datetime NOT NULL, + PRIMARY KEY (`id_source_type`), + UNIQUE KEY `source_type_code` (`source_type_code`) +) ENGINE=ENGINE_TYPE DEFAULT CHARSET=utf8 COLLATION; + +CREATE TABLE `PREFIX_business_source_lang` ( + `id_source_type` int(10) unsigned NOT NULL, + `id_lang` int(10) unsigned NOT NULL, + `name` varchar(64) NOT NULL, + PRIMARY KEY (`id_source_type`,`id_lang`) +) ENGINE=ENGINE_TYPE DEFAULT CHARSET=utf8 COLLATION; + +CREATE TABLE `PREFIX_source` ( + `id_source` int(10) unsigned NOT NULL auto_increment, + `id_source_type` int(10) unsigned NOT NULL, + `source_code` varchar(64) NOT NULL, + `position` int(10) unsigned NOT NULL DEFAULT '0', + `unremovable` tinyint(1) UNSIGNED NOT NULL DEFAULT '0', + `active` tinyint(1) UNSIGNED NOT NULL DEFAULT '1', + `deleted` tinyint(1) UNSIGNED NOT NULL DEFAULT '0', + `date_add` datetime NOT NULL, + `date_upd` datetime NOT NULL, + PRIMARY KEY (`id_source`), + UNIQUE KEY `source_code` (`source_code`), + KEY `id_source_type` (`id_source_type`) +) ENGINE=ENGINE_TYPE DEFAULT CHARSET=utf8 COLLATION; + +CREATE TABLE `PREFIX_source_lang` ( + `id_source` int(10) unsigned NOT NULL, + `id_lang` int(10) unsigned NOT NULL, + `name` varchar(64) NOT NULL, + PRIMARY KEY (`id_source`,`id_lang`) +) ENGINE=ENGINE_TYPE DEFAULT CHARSET=utf8 COLLATION; + CREATE TABLE `PREFIX_specific_price` ( `id_specific_price` INT UNSIGNED NOT NULL AUTO_INCREMENT, `id_specific_price_rule` INT(11) UNSIGNED NOT NULL, diff --git a/install/data/xml/business_source.xml b/install/data/xml/business_source.xml new file mode 100644 index 000000000..a2bd5e41b --- /dev/null +++ b/install/data/xml/business_source.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/install/data/xml/source.xml b/install/data/xml/source.xml new file mode 100644 index 000000000..90a44e9a8 --- /dev/null +++ b/install/data/xml/source.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/install/data/xml/tab.xml b/install/data/xml/tab.xml index 8909ed603..64aae0b8c 100644 --- a/install/data/xml/tab.xml +++ b/install/data/xml/tab.xml @@ -117,6 +117,9 @@ AdminStatuses + + AdminSources + AdminOrderMessage diff --git a/install/langs/en/data/business_source.xml b/install/langs/en/data/business_source.xml new file mode 100644 index 000000000..9071a22a1 --- /dev/null +++ b/install/langs/en/data/business_source.xml @@ -0,0 +1,18 @@ + + + + Direct + + + OTA + + + Corporate + + + Travel Agent + + + Group + + diff --git a/install/langs/en/data/source.xml b/install/langs/en/data/source.xml new file mode 100644 index 000000000..14c5a2b50 --- /dev/null +++ b/install/langs/en/data/source.xml @@ -0,0 +1,36 @@ + + + + Direct Website + + + Walk-in + + + Phone + + + Email + + + Social Media + + + WhatsApp + + + Booking.com + + + Expedia + + + GoMMT (GoIbibo/MakeMyTrip) + + + Agoda + + + Airbnb + + diff --git a/install/langs/en/data/tab.xml b/install/langs/en/data/tab.xml index 2ccd45418..7695dc904 100644 --- a/install/langs/en/data/tab.xml +++ b/install/langs/en/data/tab.xml @@ -37,6 +37,7 @@ + diff --git a/install/models/install.php b/install/models/install.php index 461237b9c..0fd9ddf84 100644 --- a/install/models/install.php +++ b/install/models/install.php @@ -537,6 +537,14 @@ class InstallModelInstall extends InstallAbstractModel Configuration::updateGlobalValue('PS_OS_OUTOFSTOCK_PAID', (int)Configuration::get('PS_OS_OUTOFSTOCK')); } + // Set the default Booking Source (Direct Website) + $id_default_source = (int)Db::getInstance()->getValue( + 'SELECT `id_source` FROM '._DB_PREFIX_.'source WHERE `source_code` = \'DIRECT_WEBSITE\'' + ); + if ($id_default_source) { + Configuration::updateGlobalValue('PS_DEFAULT_BOOKING_SOURCE', $id_default_source); + } + // Set logo configuration if (file_exists(_PS_IMG_DIR_.'logo.png')) { list($width, $height) = getimagesize(_PS_IMG_DIR_.'logo.png'); diff --git a/js/admin/sources.js b/js/admin/sources.js new file mode 100644 index 000000000..b2721b3f5 --- /dev/null +++ b/js/admin/sources.js @@ -0,0 +1,44 @@ +/** +* 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 +* +* @author Webkul IN +* @copyright Since 2010 Webkul +* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0 +*/ + +$(document).on('click', '.merge-source-btn', function (e) { + e.preventDefault(); + + var $btn = $(this); + + $('.loading_overlay').show(); + $.ajax({ + type: 'POST', + url: $btn.data('ajax-url'), + dataType: 'JSON', + cache: false, + data: { + ajax: true, + action: 'InitMergeSourceModal', + id_source: $btn.data('id-source'), + id_source_type: $btn.data('id-source-type') + }, + success: function (result) { + if (!result.hasError && result.modalHtml) { + $('#merge_source_modal').remove(); + $('#footer').next('.bootstrap').append(result.modalHtml); + $('#merge_source_modal').modal('show'); + } else { + showErrorMessage(result.error || 'Could not open the merge form.'); + } + }, + complete: function () { + $('.loading_overlay').hide(); + } + }); +}); From 5fe50311f1e95fba311934b2e07b60c91ab4ec7c Mon Sep 17 00:00:00 2001 From: amit sharma Date: Fri, 17 Jul 2026 11:29:06 +0530 Subject: [PATCH 14/78] Fixed: fix filter data issue and status update issue Co-authored-by: Name --- .../sources/modals/_merge_source_form.tpl | 9 +- classes/PaymentModule.php | 2 +- classes/order/Order.php | 1 + controllers/admin/AdminSourcesController.php | 85 +++++++++++-------- install/data/xml/source.xml | 2 +- js/admin/sources.js | 2 +- 6 files changed, 58 insertions(+), 43 deletions(-) diff --git a/admin/themes/default/template/controllers/sources/modals/_merge_source_form.tpl b/admin/themes/default/template/controllers/sources/modals/_merge_source_form.tpl index 8002b45f4..7eecabd2f 100644 --- a/admin/themes/default/template/controllers/sources/modals/_merge_source_form.tpl +++ b/admin/themes/default/template/controllers/sources/modals/_merge_source_form.tpl @@ -11,8 +11,8 @@
    diff --git a/classes/PaymentModule.php b/classes/PaymentModule.php index 489edb743..7e786f24f 100644 --- a/classes/PaymentModule.php +++ b/classes/PaymentModule.php @@ -356,7 +356,7 @@ abstract class PaymentModuleCore extends Module if (isset($this->idBookingSource) && $this->idBookingSource) { $order->id_source = (int)$this->idBookingSource; } else { - $order->id_source = Source::getDefaultSourceId(); + $order->id_source = (int)Source::getDefaultSourceId(); } if (self::DEBUG_MODE) { diff --git a/classes/order/Order.php b/classes/order/Order.php index e5ed30602..25f23e88f 100644 --- a/classes/order/Order.php +++ b/classes/order/Order.php @@ -271,6 +271,7 @@ class OrderCore extends ObjectModel 'invoice_date' => array('type' => self::TYPE_DATE), 'delivery_date' => array('type' => self::TYPE_DATE), 'source' => array('type' => self::TYPE_STRING), + 'id_source' => array('type' => self::TYPE_INT), 'valid' => array('type' => self::TYPE_BOOL), 'reference' => array('type' => self::TYPE_STRING), 'is_advance_payment' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'default' => 0), diff --git a/controllers/admin/AdminSourcesController.php b/controllers/admin/AdminSourcesController.php index de80d4a01..299069578 100644 --- a/controllers/admin/AdminSourcesController.php +++ b/controllers/admin/AdminSourcesController.php @@ -62,22 +62,6 @@ class AdminSourcesControllerCore extends AdminController public function init() { - if (Tools::isSubmit('viewbusiness_source') - || Tools::isSubmit('submitFiltersource') - || Tools::isSubmit('submitResetsource') - || Tools::getValue('sourceOrderby') - || Tools::getValue('sourceOrderway') - ) { - $this->list_id = 'source'; - } - - $idSourceType = (int) Tools::getValue('id_source_type'); - - if ($idSourceType && Tools::getValue('viewbusiness_source')) { - self::$currentIndex .= '&id_source_type=' . $idSourceType . '&viewbusiness_source'; - $this->className = 'Source'; - } - if (Tools::isSubmit('addsource') || Tools::isSubmit('addbusiness_source')) { $this->display = 'add'; } @@ -91,10 +75,23 @@ class AdminSourcesControllerCore extends AdminController $this->display = Tools::getValue('id_source_type') ? 'edit' : 'add'; } - return parent::init(); } + public function initContent() + { + if (Tools::isSubmit('submitFiltersource') || Tools::isSubmit('submitResetsource')) { + $this->list_id = 'source'; + $this->display = 'view'; + + $idSourceType = (int) Tools::getValue('id_source_type'); + self::$currentIndex .= '&id_source_type=' . $idSourceType . '&viewbusiness_source'; + if (Tools::isSubmit('submitResetsource')) { + $this->processResetFilters('source'); + } + } + parent::initContent(); + } /** * @see AdminController::initPageHeaderToolbar() */ @@ -153,7 +150,7 @@ class AdminSourcesControllerCore extends AdminController 'position' => 'position' ), 'orders_count' => array( - 'title' => $this->l('Orders'), + 'title' => $this->l('Orders (%)'), 'align' => 'text-center', 'orderby' => false, 'search' => false, @@ -200,7 +197,7 @@ class AdminSourcesControllerCore extends AdminController 'position' => 'position' ), 'orders_count' => array( - 'title' => $this->l('Orders'), + 'title' => $this->l('Orders (%)'), 'align' => 'text-center', 'orderby' => false, 'search' => false, @@ -240,7 +237,7 @@ class AdminSourcesControllerCore extends AdminController $idSourceType = (int)Tools::getValue('id_source_type'); $objBusinessSource = new BusinessSource($idSourceType, (int)$this->context->language->id); if (!Validate::isLoadedObject($objBusinessSource)) { - return; + Tools::redirectAdmin(self::$currentIndex.'&token='.$this->token); } $this->addRowAction('edit'); @@ -428,20 +425,20 @@ class AdminSourcesControllerCore extends AdminController } else { Tools::redirectAdmin(self::$currentIndex.'&viewbusiness_source&id_source_type='.(int)$objSource->id_source_type.'&conf=1&token='.$this->token); } + } else if(Tools::isSubmit('statussource')){ + if(Validate::isLoadedObject($objSource = new Source((int)Tools::getValue('id_source')))){ + $idSourceType = $objSource->id_source_type; + $objSource->active = !$objSource->active; + if($objSource->save()){ + $this->redirect_after = self::$currentIndex.'&viewbusiness_source&id_source_type='.(int)$idSourceType.'&conf=5&token='.$this->token; + } + + } + } else if(Tools::isSubmit('submitBulkenableSelectionsource')){ - $idSourceType = (int)Tools::getValue('id_source_type'); - $this->className = 'Source'; - $this->table = 'source'; - $this->boxes = Tools::getValue('sourceBox'); - parent::processBulkEnableSelection(); - $this->redirect_after = self::$currentIndex.'&viewbusiness_source&id_source_type='.(int)$idSourceType.'&token='.$this->token; + $this->processUpdateStatusBulk(1); } else if(Tools::isSubmit('submitBulkdisableSelectionsource')){ - $idSourceType = (int)Tools::getValue('id_source_type'); - $this->className = 'Source'; - $this->table = 'source'; - $this->boxes = Tools::getValue('sourceBox'); - parent::processBulkDisableSelection(); - $this->redirect_after = self::$currentIndex.'&viewbusiness_source&id_source_type='.(int)$idSourceType.'&token='.$this->token; + $this->processUpdateStatusBulk(0); } elseif (Tools::isSubmit('submitBulkdeletesource')) { $idSourceType = 0; foreach (Tools::getValue('sourceBox') as $selection) { @@ -486,6 +483,22 @@ class AdminSourcesControllerCore extends AdminController } } + protected function processUpdateStatusBulk($status = 1) + { + $idSourceType = (int)Tools::getValue('id_source_type'); + $this->className = 'Source'; + $this->table = 'source'; + $this->boxes = Tools::getValue('sourceBox'); + if(empty($this->boxes)){ + $this->errors[] = $this->l('You must select at least one item to perform a bulk action.'); + } + $this->display = 'view'; + if(!$this->errors){ + parent::processBulkStatusSelection($status); + $this->redirect_after = self::$currentIndex.'&viewbusiness_source&id_source_type='.(int)$idSourceType.'&conf=5&token='.$this->token; + } + } + protected function postProcessSource() { $idSource = (int)Tools::getValue('id_source'); @@ -549,8 +562,8 @@ class AdminSourcesControllerCore extends AdminController protected function postProcessMergeSource() { $idSourceType = (int)Tools::getValue('id_source_type'); - $idCurrentSource = (int)Tools::getValue('merge_current_source'); - $idTargetSource = (int)Tools::getValue('merge_target_source'); + $idCurrentSource = (int)Tools::getValue('current_source'); + $idTargetSource = (int)Tools::getValue('target_source'); $objCurrent = new Source($idCurrentSource); $objTarget = new Source($idTargetSource); @@ -564,7 +577,7 @@ class AdminSourcesControllerCore extends AdminController } else { $objCurrent->mergeOrdersInto($idTargetSource); } - + $this->display = 'view'; if (!count($this->errors)) { Tools::redirectAdmin(self::$currentIndex.'&viewbusiness_source&id_source_type='.$idSourceType.'&conf=4&token='.$this->token); } @@ -645,7 +658,7 @@ class AdminSourcesControllerCore extends AdminController $this->context->smarty->assign(array( 'modal_id' => 'merge_source_modal', 'modal_class' => 'modal-md', - 'modal_title' => $this->l('Merge Booking Source'), + 'modal_title' => '   ' .$this->l('Merge Booking Source'), 'modal_content' => $this->context->smarty->fetch('controllers/sources/modals/_merge_source_form.tpl'), )); diff --git a/install/data/xml/source.xml b/install/data/xml/source.xml index 90a44e9a8..e08ef2489 100644 --- a/install/data/xml/source.xml +++ b/install/data/xml/source.xml @@ -16,7 +16,7 @@ - + diff --git a/js/admin/sources.js b/js/admin/sources.js index b2721b3f5..263dda926 100644 --- a/js/admin/sources.js +++ b/js/admin/sources.js @@ -34,7 +34,7 @@ $(document).on('click', '.merge-source-btn', function (e) { $('#footer').next('.bootstrap').append(result.modalHtml); $('#merge_source_modal').modal('show'); } else { - showErrorMessage(result.error || 'Could not open the merge form.'); + showErrorMessage(result.error); } }, complete: function () { From feff43ed8974e308141ee645e94351f588bb4876 Mon Sep 17 00:00:00 2001 From: azhar bhat Date: Mon, 20 Jul 2026 18:16:37 +0530 Subject: [PATCH 15/78] Fixed: hide payment method when payment amount is zero. Co-authored-by: Name --- .../template/controllers/orders/form.tpl | 38 +++++++++++-------- controllers/admin/AdminOrdersController.php | 12 +++--- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/admin/themes/default/template/controllers/orders/form.tpl b/admin/themes/default/template/controllers/orders/form.tpl index aef84a89c..fbc844516 100644 --- a/admin/themes/default/template/controllers/orders/form.tpl +++ b/admin/themes/default/template/controllers/orders/form.tpl @@ -760,6 +760,11 @@ $('#total_taxes').html(formatCurrency(parseFloat(jsonSummary.summary.total_tax_without_discount), currency_format, currency_sign, currency_blank)); $('#total_with_taxes').html(formatCurrency(parseFloat(jsonSummary.summary.total_price), currency_format, currency_sign, currency_blank)); + if (parseInt($('input[name="is_full_payment"]:checked').val())) { + $('#payment_amount').val(jsonSummary.summary.total_price); + } + $('#full_payment_amount_value').html(formatCurrency(parseFloat(jsonSummary.summary.total_price), currency_format, currency_sign, currency_blank)); + if (jsonSummary.summary.is_advance_payment_active) { $('#advance_payment_amount').html(formatCurrency(parseFloat(jsonSummary.summary.advance_payment_amount_with_tax), currency_format, currency_sign, currency_blank)); $('#advance_payment_amount_block').show(); @@ -769,10 +774,10 @@ // toggle payment fields if (jsonSummary.summary.total_price == 0) { // if free order - $('#send_email_to_customer, [name="is_full_payment"], #payment_amount_input_field_group, #payment_amount_static_group, #payment_type, #payment_module_name, #payment_transaction_id').closest('.form-group').hide(200); + $('#send_email_to_customer, [name="is_full_payment"], #payment_amount_input_field_group, #payment_type, #payment_module_name, #payment_transaction_id').closest('.form-group').hide(200); } else { $('#send_email_to_customer, [name="is_full_payment"], #payment_type, #payment_module_name, #payment_transaction_id').closest('.form-group').show(200); - $(parseInt($('input[name="is_full_payment"]:checked').val()) ? '#payment_amount_static_group' : '#payment_amount_input_field_group').show(200); + $('#payment_amount_input_field_group').toggle(!parseInt($('input[name="is_full_payment"]:checked').val())); } } @@ -1289,6 +1294,11 @@ } $('#order_message').val(jsonSummary.order_message); $('#payment_amount').siblings('.input-group-addon').html(currency_sign); + if (parseInt($('input[name="is_full_payment"]:checked').val())) { + $('#payment_amount').val(jsonSummary.summary.total_price); + } + $('#full_payment_amount_value').html(formatCurrency(parseFloat(jsonSummary.summary.total_price), currency_format, currency_sign, currency_blank)); + if (jsonSummary.summary.is_advance_payment_active) { $('#advance_payment_amount').html(formatCurrency(parseFloat(jsonSummary.summary.advance_payment_amount_with_tax), currency_format, currency_sign, currency_blank)); $('#advance_payment_amount_block').show(); @@ -1298,10 +1308,10 @@ // toggle payment fields if (jsonSummary.summary.total_price == 0) { // if free order - $('#send_email_to_customer, [name="is_full_payment"], #payment_amount_input_field_group, #payment_amount_static_group, #payment_type, #payment_module_name, #payment_transaction_id').closest('.form-group').hide(200); + $('#send_email_to_customer, [name="is_full_payment"], #payment_amount_input_field_group, #payment_type, #payment_module_name, #payment_transaction_id').closest('.form-group').hide(200); } else { $('#send_email_to_customer, [name="is_full_payment"], #payment_type, #payment_module_name, #payment_transaction_id').closest('.form-group').show(200); - $(parseInt($('input[name="is_full_payment"]:checked').val()) ? '#payment_amount_static_group' : '#payment_amount_input_field_group').show(200); + $('#payment_amount_input_field_group').toggle(!parseInt($('input[name="is_full_payment"]:checked').val())); } resetBind(); @@ -1713,14 +1723,12 @@ $(document).on('change', 'input[name="is_full_payment"]', function() { if (parseInt($('input[name="is_full_payment"]:checked').val())) { - $('#payment_amount').attr('disabled', true); + $('#payment_amount').val({$order_total|floatval}).attr('disabled', true); $('#payment_amount_input_field_group').hide(200); - $('#payment_amount_static_group').show(200); - $('#payment_type, #payment_transaction_id').closest('.form-group').show(200); + $('#payment_type, #payment_module_name, #payment_transaction_id').closest('.form-group').show(200); } else { $('#payment_amount').val({$order_total|floatval}).attr('disabled', false); - $('#payment_amount_static_group').hide(200); $('#payment_amount_input_field_group').show(200); managePaymentOptions(); @@ -1735,9 +1743,9 @@ let paymentAmount = parseFloat($('#payment_amount').val().trim()); if (paymentAmount != 0) { - $('#payment_type, #payment_transaction_id').closest('.form-group').show(200); + $('#payment_type, #payment_module_name, #payment_transaction_id').closest('.form-group').show(200); } else { - $('#payment_type, #payment_transaction_id').closest('.form-group').hide(200); + $('#payment_type, #payment_module_name, #payment_transaction_id').closest('.form-group').hide(200); } } @@ -2401,18 +2409,16 @@ {$currency->sign}
    +

    + {l s='Full amount: '} + {displayPrice price=$order_total currency=$currency->id} +

    {l s='Advance payment amount: '} {displayPrice price=$advance_payment_amount_with_tax currency=$currency->id}

    -
    - -
    -

    {displayPrice price=$order_total currency=$currency->id}

    -
    -
    diff --git a/controllers/admin/AdminOrdersController.php b/controllers/admin/AdminOrdersController.php index bcbe5868c..f59e098e0 100644 --- a/controllers/admin/AdminOrdersController.php +++ b/controllers/admin/AdminOrdersController.php @@ -2174,15 +2174,13 @@ class AdminOrdersControllerCore extends AdminController $paymentAmount = $orderTotal; } - if ($paymentAmount >= 0) { + if ($paymentAmount > 0) { if (!$moduleName) { $this->errors[] = Tools::displayError('Please enter Payment method.'); } elseif ($moduleName && !Validate::isGenericName($moduleName)) { $this->errors[] = Tools::displayError('Payment method is invalid. Please enter a valid payment method.'); } - } - if ($paymentAmount > 0) { if (!$paymentType) { $this->errors[] = Tools::displayError('Please select a Payment source.'); } elseif ($paymentType && !Validate::isUnsignedInt($paymentType)) { @@ -2199,10 +2197,14 @@ class AdminOrdersControllerCore extends AdminController // Set payment module details $objPaymentModule = new BoOrder(); - $objPaymentModule->displayName = $moduleName; + if ($moduleName) { + $objPaymentModule->displayName = $moduleName; + } if ($orderTotal > 0) { - $objPaymentModule->payment_type = $paymentType; + if ($paymentType) { + $objPaymentModule->payment_type = $paymentType; + } // Set order state if ($isFullPayment) { From b7c74e01b609252a1d60a93594a7dd28ee483cb1 Mon Sep 17 00:00:00 2001 From: azhar bhat Date: Tue, 21 Jul 2026 17:59:59 +0530 Subject: [PATCH 16/78] fixed id name --- .../default/template/controllers/orders/form.tpl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/admin/themes/default/template/controllers/orders/form.tpl b/admin/themes/default/template/controllers/orders/form.tpl index fbc844516..18673b82a 100644 --- a/admin/themes/default/template/controllers/orders/form.tpl +++ b/admin/themes/default/template/controllers/orders/form.tpl @@ -774,10 +774,10 @@ // toggle payment fields if (jsonSummary.summary.total_price == 0) { // if free order - $('#send_email_to_customer, [name="is_full_payment"], #payment_amount_input_field_group, #payment_type, #payment_module_name, #payment_transaction_id').closest('.form-group').hide(200); + $('#send_email_to_customer, [name="is_full_payment"], #payment_amount, #payment_type, #payment_module_name, #payment_transaction_id').closest('.form-group').hide(200); } else { $('#send_email_to_customer, [name="is_full_payment"], #payment_type, #payment_module_name, #payment_transaction_id').closest('.form-group').show(200); - $('#payment_amount_input_field_group').toggle(!parseInt($('input[name="is_full_payment"]:checked').val())); + $('#payment_amount').closest('.form-group').toggle(!parseInt($('input[name="is_full_payment"]:checked').val())); } } @@ -1308,10 +1308,10 @@ // toggle payment fields if (jsonSummary.summary.total_price == 0) { // if free order - $('#send_email_to_customer, [name="is_full_payment"], #payment_amount_input_field_group, #payment_type, #payment_module_name, #payment_transaction_id').closest('.form-group').hide(200); + $('#send_email_to_customer, [name="is_full_payment"], #payment_amount, #payment_type, #payment_module_name, #payment_transaction_id').closest('.form-group').hide(200); } else { $('#send_email_to_customer, [name="is_full_payment"], #payment_type, #payment_module_name, #payment_transaction_id').closest('.form-group').show(200); - $('#payment_amount_input_field_group').toggle(!parseInt($('input[name="is_full_payment"]:checked').val())); + $('#payment_amount').closest('.form-group').toggle(!parseInt($('input[name="is_full_payment"]:checked').val())); } resetBind(); @@ -1724,12 +1724,12 @@ $(document).on('change', 'input[name="is_full_payment"]', function() { if (parseInt($('input[name="is_full_payment"]:checked').val())) { $('#payment_amount').val({$order_total|floatval}).attr('disabled', true); - $('#payment_amount_input_field_group').hide(200); + $('#payment_amount').closest('.form-group').hide(200); $('#payment_type, #payment_module_name, #payment_transaction_id').closest('.form-group').show(200); } else { $('#payment_amount').val({$order_total|floatval}).attr('disabled', false); - $('#payment_amount_input_field_group').show(200); + $('#payment_amount').closest('.form-group').show(200); managePaymentOptions(); } @@ -2402,7 +2402,7 @@

    {l s='Keep this option enabled for full payment and disable it to take partial payment of the booking.'}

    -
    +
    From b4e8baca9297f40598d84e5c925a7d8501846044 Mon Sep 17 00:00:00 2001 From: amit sharma Date: Thu, 23 Jul 2026 11:46:21 +0530 Subject: [PATCH 17/78] Fixed: fix testing issues Co-authored-by: Name --- .../sources/modals/_merge_source_form.tpl | 10 +- classes/BusinessSource.php | 34 ++-- classes/Source.php | 4 +- controllers/admin/AdminOrdersController.php | 2 +- controllers/admin/AdminSourcesController.php | 161 +++++++++++------- install/data/xml/business_source.xml | 10 +- install/data/xml/source.xml | 22 +-- 7 files changed, 143 insertions(+), 100 deletions(-) diff --git a/admin/themes/default/template/controllers/sources/modals/_merge_source_form.tpl b/admin/themes/default/template/controllers/sources/modals/_merge_source_form.tpl index 7eecabd2f..410185759 100644 --- a/admin/themes/default/template/controllers/sources/modals/_merge_source_form.tpl +++ b/admin/themes/default/template/controllers/sources/modals/_merge_source_form.tpl @@ -11,12 +11,8 @@
    '; } + $mail_available_variables = $this->displayMailAvailableVariables($mail_files); + if (array_key_exists('txt', $mail_files)) { $str_return .= '
    '; $str_return .= $this->displayMailBlockTxt($mail_files['txt'], $obj_lang->iso_code, $mail_name, $group_name, $name_for_module); + $str_return .= $mail_available_variables; $str_return .= '
    '; } @@ -2372,6 +2426,7 @@ class AdminTranslationsControllerCore extends AdminController if (isset($mail_files['html'])) { $str_return .= $this->displayMailEditor($mail_files['html'], $obj_lang->iso_code, $url_mail, $mail_name, $group_name, $name_for_module); } + $str_return .= $mail_available_variables; $str_return .= '
    '; $str_return .= '
    '; @@ -2389,6 +2444,70 @@ class AdminTranslationsControllerCore extends AdminController $str_return .= '
    '; return $str_return; } + + /** + * Build the "Available variables" tag list for a mail template, so the + * admin can see which {variable} placeholders the template supports and + * click one to insert it into the editor. + * + * @param array $mail_files Same structure as $mails['files'][$mail_name] + * + * @return string + */ + protected function displayMailAvailableVariables($mail_files) + { + $variables = $this->getMailTemplateVariables($mail_files); + if (empty($variables)) { + return ''; + } + + // Bootstrap's .label is display:inline with vertical padding, so it can't take a + // vertical margin; without extra line-height here, wrapped rows visually overlap. + $str_return = '
    '; + foreach ($variables as $variable) { + $tag = '{'.$variable.'}'; + $str_return .= ''.Tools::htmlentitiesUTF8($tag).' '; + } + $str_return .= '
    '; + + return $str_return; + } + + /** + * Extract the unique {variable} placeholders used in a mail template's + * core content. Reads the 'en' key populated by getMailFiles(), which + * actually holds whichever language resolved as the shop's default + * (English, or PS_LANG_DEFAULT when English isn't an installed language) + * - falls back to any other available language content in the rare case + * that key isn't set. + * + * @param array $mail_files Same structure as $mails['files'][$mail_name] + * + * @return array Variable names, without braces, sorted alphabetically + */ + protected function getMailTemplateVariables($mail_files) + { + $content = ''; + foreach (array('html', 'txt') as $type) { + if (empty($mail_files[$type]) || !is_array($mail_files[$type])) { + continue; + } + if (isset($mail_files[$type]['en'])) { + $content .= ' '.$mail_files[$type]['en']; + } else { + $content .= ' '.reset($mail_files[$type]); + } + } + + $variables = array(); + if ($content !== '' && preg_match_all('/\{([a-zA-Z0-9_]+)\}/', $content, $matches)) { + $variables = array_unique($matches[1]); + sort($variables); + } + + return $variables; + } + /** * Just build the html structure for display txt mails * @since 1.4.0.14 @@ -3070,9 +3189,33 @@ class AdminTranslationsControllerCore extends AdminController } $sanitizedFilePath = realpath($email_file); - $permittedMailDir = realpath(_PS_MAIL_DIR_) . DIRECTORY_SEPARATOR; + if ($sanitizedFilePath === false) { + return false; + } - if ($sanitizedFilePath === false || $permittedMailDir === false || strpos($sanitizedFilePath, $permittedMailDir) !== 0) { + // Mail templates can also live under a theme's or a module's own + // "mails" override directory (themes//mails//, + // modules//mails//, or their theme-override copies), + // not just the core mails/ directory - so every permitted root below + // is additionally required to have a "mails" path segment. + $permitted_roots = array( + realpath(_PS_MAIL_DIR_), + realpath(_PS_ROOT_DIR_.'/themes/'), + realpath(_PS_ROOT_DIR_.'/modules/'), + ); + + $is_permitted = false; + foreach ($permitted_roots as $permitted_root) { + if ($permitted_root !== false + && strpos($sanitizedFilePath, $permitted_root.DIRECTORY_SEPARATOR) === 0 + && strpos($sanitizedFilePath, DIRECTORY_SEPARATOR.'mails'.DIRECTORY_SEPARATOR) !== false + ) { + $is_permitted = true; + break; + } + } + + if (!$is_permitted) { return false; } From 5a69b1f768dca4c73b5dd98e06b50ac1bc79c469 Mon Sep 17 00:00:00 2001 From: azhar bhat Date: Fri, 24 Jul 2026 17:48:11 +0530 Subject: [PATCH 19/78] removed the unnecessary changes --- .../admin/AdminTranslationsController.php | 53 ++----------------- 1 file changed, 3 insertions(+), 50 deletions(-) diff --git a/controllers/admin/AdminTranslationsController.php b/controllers/admin/AdminTranslationsController.php index 25248bfe6..3d361ed25 100644 --- a/controllers/admin/AdminTranslationsController.php +++ b/controllers/admin/AdminTranslationsController.php @@ -2445,15 +2445,6 @@ class AdminTranslationsControllerCore extends AdminController return $str_return; } - /** - * Build the "Available variables" tag list for a mail template, so the - * admin can see which {variable} placeholders the template supports and - * click one to insert it into the editor. - * - * @param array $mail_files Same structure as $mails['files'][$mail_name] - * - * @return string - */ protected function displayMailAvailableVariables($mail_files) { $variables = $this->getMailTemplateVariables($mail_files); @@ -2461,8 +2452,6 @@ class AdminTranslationsControllerCore extends AdminController return ''; } - // Bootstrap's .label is display:inline with vertical padding, so it can't take a - // vertical margin; without extra line-height here, wrapped rows visually overlap. $str_return = '
    '; foreach ($variables as $variable) { $tag = '{'.$variable.'}'; @@ -2473,18 +2462,6 @@ class AdminTranslationsControllerCore extends AdminController return $str_return; } - /** - * Extract the unique {variable} placeholders used in a mail template's - * core content. Reads the 'en' key populated by getMailFiles(), which - * actually holds whichever language resolved as the shop's default - * (English, or PS_LANG_DEFAULT when English isn't an installed language) - * - falls back to any other available language content in the rare case - * that key isn't set. - * - * @param array $mail_files Same structure as $mails['files'][$mail_name] - * - * @return array Variable names, without braces, sorted alphabetically - */ protected function getMailTemplateVariables($mail_files) { $content = ''; @@ -3189,36 +3166,12 @@ class AdminTranslationsControllerCore extends AdminController } $sanitizedFilePath = realpath($email_file); - if ($sanitizedFilePath === false) { - return false; - } + $permittedMailDir = realpath(_PS_MAIL_DIR_) . DIRECTORY_SEPARATOR; - // Mail templates can also live under a theme's or a module's own - // "mails" override directory (themes//mails//, - // modules//mails//, or their theme-override copies), - // not just the core mails/ directory - so every permitted root below - // is additionally required to have a "mails" path segment. - $permitted_roots = array( - realpath(_PS_MAIL_DIR_), - realpath(_PS_ROOT_DIR_.'/themes/'), - realpath(_PS_ROOT_DIR_.'/modules/'), - ); - - $is_permitted = false; - foreach ($permitted_roots as $permitted_root) { - if ($permitted_root !== false - && strpos($sanitizedFilePath, $permitted_root.DIRECTORY_SEPARATOR) === 0 - && strpos($sanitizedFilePath, DIRECTORY_SEPARATOR.'mails'.DIRECTORY_SEPARATOR) !== false - ) { - $is_permitted = true; - break; - } - } - - if (!$is_permitted) { + if ($sanitizedFilePath === false || $permittedMailDir === false || strpos($sanitizedFilePath, $permittedMailDir) !== 0) { return false; } return file_get_contents($sanitizedFilePath); } -} +} \ No newline at end of file From 01e557d48152b7c1b5f23e902329e9fa708439f8 Mon Sep 17 00:00:00 2001 From: azhar bhat Date: Fri, 31 Jul 2026 15:30:49 +0530 Subject: [PATCH 20/78] Reset button not reset the mails using gzip file --- .../helpers/view/translation_mails.tpl | 24 ++---- classes/Language.php | 51 ++++++++++++- .../admin/AdminTranslationsController.php | 74 +++++++++++++------ 3 files changed, 107 insertions(+), 42 deletions(-) diff --git a/admin/themes/default/template/controllers/translations/helpers/view/translation_mails.tpl b/admin/themes/default/template/controllers/translations/helpers/view/translation_mails.tpl index 5475edf88..9f9c13c18 100644 --- a/admin/themes/default/template/controllers/translations/helpers/view/translation_mails.tpl +++ b/admin/themes/default/template/controllers/translations/helpers/view/translation_mails.tpl @@ -81,12 +81,10 @@ {l s='Cancel'} - {if $theme} - - {/if} {*$toggle_button*} - + + diff --git a/modules/hotelreservationsystem/views/templates/admin/add_hotel/_partials/htl-images-list.tpl b/modules/hotelreservationsystem/views/templates/admin/add_hotel/_partials/htl-images-list.tpl index 57b0a1262..a5eadc833 100644 --- a/modules/hotelreservationsystem/views/templates/admin/add_hotel/_partials/htl-images-list.tpl +++ b/modules/hotelreservationsystem/views/templates/admin/add_hotel/_partials/htl-images-list.tpl @@ -66,7 +66,7 @@
  • - +  {l s='Update selection' mod='hotelreservationsystem'}
  • @@ -77,3 +77,143 @@
    + + + + + + + + diff --git a/modules/hotelreservationsystem/views/templates/admin/add_hotel/helpers/form/form.tpl b/modules/hotelreservationsystem/views/templates/admin/add_hotel/helpers/form/form.tpl index ce8255cb3..71b7b2128 100644 --- a/modules/hotelreservationsystem/views/templates/admin/add_hotel/helpers/form/form.tpl +++ b/modules/hotelreservationsystem/views/templates/admin/add_hotel/helpers/form/form.tpl @@ -439,9 +439,9 @@ {if isset($hotel_info.id) && $hotel_info.id}
    -
    -
    @@ -449,146 +449,6 @@ {include file="../../_partials/htl-images-list.tpl"}
    - - - - - - - - {else}
    {l s='Please save hotel information before saving hotel images.' mod='hotelreservationsystem'} diff --git a/themes/hotel-reservation-theme/_partials/hotel_images.tpl b/themes/hotel-reservation-theme/_partials/hotel_images.tpl index f20e7268a..85f7c1443 100644 --- a/themes/hotel-reservation-theme/_partials/hotel_images.tpl +++ b/themes/hotel-reservation-theme/_partials/hotel_images.tpl @@ -24,12 +24,10 @@ {if is_array($hotel_images) && count($hotel_images)} {foreach from=$hotel_images item=hotel_image} diff --git a/themes/hotel-reservation-theme/css/product.css b/themes/hotel-reservation-theme/css/product.css index 7662875e9..4a197037e 100644 --- a/themes/hotel-reservation-theme/css/product.css +++ b/themes/hotel-reservation-theme/css/product.css @@ -46,7 +46,6 @@ a.fancybox-nav { border-radius: 4px; width: 100%; min-height: 420px; - -o-object-fit: cover; object-fit: cover; } .pb-left-column #image-block #view_full_size .span_link { position: absolute; @@ -1774,270 +1773,3 @@ ul.service-product-nav > li:hover { display: block; } } - -.htl-full-gallery-modal { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 10000; -} - -.htl-full-gallery__overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.92); -} - -.htl-full-gallery__container { - position: relative; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - width: 100%; - height: 100%; - padding: 1.5rem; - box-sizing: border-box; -} - -.htl-full-gallery__close { - position: absolute; - top: 0.75rem; - right: 0.75rem; - z-index: 10001; - width: 3.25rem; - height: 3.25rem; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - color: #fff; - font-size: 1.1rem; - border: 2px solid rgba(255,255,255,0.6); - border-radius: 50%; - background: rgba(0,0,0,0.35); - cursor: pointer; - line-height: 1; - transition: background 0.2s, border-color 0.2s; -} - -.htl-full-gallery__close:hover { - background: rgba(0,0,0,0.7); - border-color: #fff; -} - -.htl-full-gallery__header { - text-align: center; - padding-top: 0.25rem; -} - -.htl-full-gallery__hotel-name { - color: #fff; - font-size: 1.6rem; - font-weight: 400; - margin: 0 3rem 0.5rem; - letter-spacing: 0.02em; -} - -.htl-full-gallery__tabs { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - margin-top: 2rem; -} - -.htl-full-gallery__tabs-wrapper { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -ms-flex-wrap: wrap; - flex-wrap: wrap; - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - gap: 0.4rem; -} - -.htl-gallery-tab { - font-size: 13px; - padding: 5px 16px; - background: transparent; - border: 1px solid rgba(255,255,255,0.7); - border-radius: 20px; - color: rgba(255,255,255,0.85); - cursor: pointer; - transition: background 0.2s, border-color 0.2s, color 0.2s; - line-height: 1.4; -} - -.htl-gallery-tab:hover { - background: rgba(255,255,255,0.12); - border-color: #fff; - color: #fff; -} - -.htl-gallery-tab.active { - background: #379BF0; - border-color: #379BF0; - color: #fff; -} - -.htl-full-gallery__main { - -webkit-box-flex: 1; - -ms-flex: 1; - flex: 1; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - position: relative; - overflow: hidden; - min-height: 0; -} - -.htl-full-gallery__nav { - position: absolute; - top: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); - width: 2.75rem; - height: 2.75rem; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - background: rgba(0,0,0,0.4); - border: 1px solid rgba(255,255,255,0.5); - border-radius: 50%; - color: #fff; - font-size: 1rem; - cursor: pointer; - z-index: 10; - transition: background 0.2s, border-color 0.2s; -} - -.htl-full-gallery__nav:hover { - background: rgba(55,155,240,0.75); - border-color: #379BF0; -} - -.htl-full-gallery__nav--prev { left: 0.75rem; } -.htl-full-gallery__nav--next { right: 0.75rem; } - -.htl-full-gallery__image-container { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - max-width: calc(100% - 8rem); - max-height: 100%; -} - -.htl-full-gallery__main-image { - max-width: 100%; - max-height: 60vh; - -o-object-fit: contain; - object-fit: contain; - border-radius: 3px; - display: block; - box-shadow: 0 4px 24px rgba(0,0,0,0.5); -} - -.htl-full-gallery__thumbnails { - width: 100%; - padding: 0.5rem 0 0; -} - -.htl-gallery-thumbnails__scroll { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - gap: 0.35rem; - overflow-x: auto; - padding: 0.4rem 0.5rem; - scroll-behavior: smooth; - -webkit-overflow-scrolling: touch; -} - -.htl-gallery-thumbnails__scroll::-webkit-scrollbar { height: 3px; } -.htl-gallery-thumbnails__scroll::-webkit-scrollbar-track { background: rgba(255,255,255,0.08); } -.htl-gallery-thumbnails__scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.35); border-radius: 2px; } - -.htl-gallery-thumbnail { - -ms-flex-negative: 0; - flex-shrink: 0; - width: 5.5rem; - height: 4rem; - border-radius: 3px; - overflow: hidden; - cursor: pointer; - opacity: 0.5; - -webkit-transition: opacity 0.2s ease, -webkit-transform 0.15s ease; - transition: opacity 0.2s ease, transform 0.15s ease; - border: 2px solid transparent; -} - -.htl-gallery-thumbnail:hover { opacity: 0.8; } - -.htl-gallery-thumbnail.active { - opacity: 1; - border-color: #379BF0; - -webkit-transform: scale(1.08); - transform: scale(1.08); -} - -.htl-gallery-thumbnail img { - width: 100%; - height: 100%; - -o-object-fit: cover; - object-fit: cover; - display: block; -} - -body.gallery-open { overflow: hidden; } - -@media (max-width: 575px) { - .htl-full-gallery__container { padding: 0.6rem; } - .htl-full-gallery__hotel-name { font-size: 1rem; margin: 0 2.5rem 0.4rem; } - .htl-gallery-tab { font-size: 12px; padding: 4px 12px; } - .htl-full-gallery__nav { width: 2rem; height: 2rem; font-size: 0.85rem; } - .htl-full-gallery__nav--prev { left: 0.25rem; } - .htl-full-gallery__nav--next { right: 0.25rem; } - .htl-full-gallery__image-container { max-width: calc(100% - 5rem); } - .htl-full-gallery__main-image { max-height: 45vh; } - .htl-gallery-thumbnail { width: 3rem; height: 2rem; } -} diff --git a/themes/hotel-reservation-theme/js/product.js b/themes/hotel-reservation-theme/js/product.js index 12ca94bd4..ce085c10d 100644 --- a/themes/hotel-reservation-theme/js/product.js +++ b/themes/hotel-reservation-theme/js/product.js @@ -205,7 +205,13 @@ $(document).ready(function() { } if ($('.room_info_hotel_images_wrap').length) { - hotelImagesGallery.init(); + if (!!$.prototype.fancybox) { + $('.room_info_hotel_images_wrap .fancybox').fancybox({ + 'hideOnContentClick': true, + 'openEffect': 'elastic', + 'closeEffect': 'elastic', + }); + } loadHotelImagesByPage(1); } }); @@ -1506,193 +1512,6 @@ var BookingForm = { } } -var hotelImagesGallery = { - modal: null, - images: [], - filteredImages: [], - currentIndex: 0, - currentCategory: 'all', - categories: {}, - loadedSrcs: {}, - - init: function () { - this.modal = $('#htl-img-gallery-modal'); - this.bindEvents(); - }, - - addImages: function (newImages) { - for (var i = 0; i < newImages.length; i++) { - var img = newImages[i]; - if (this.loadedSrcs[img.src]) { continue; } - this.loadedSrcs[img.src] = true; - this.images.push(img); - if (img.categoryId && img.categoryName && !this.categories[img.categoryId]) { - this.categories[img.categoryId] = img.categoryName; - } - } - this.renderCategoryTabs(); - }, - - bindEvents: function () { - var self = this; - - $(document).on('click', '.htl-img-gallery-trigger', function (e) { - e.preventDefault(); - e.stopPropagation(); - var clickedUrl = $(this).data('large-url'); - var startIndex = 0; - for (var i = 0; i < self.images.length; i++) { - if (self.images[i].src === clickedUrl) { - startIndex = i; - break; - } - } - self.open(startIndex); - }); - - this.modal.on('click', '.htl-full-gallery__close, .htl-full-gallery__overlay', function () { - self.close(); - }); - - this.modal.on('click', '.htl-full-gallery__nav--prev', function () { - self.prevImage(); - }); - - this.modal.on('click', '.htl-full-gallery__nav--next', function () { - self.nextImage(); - }); - - $(document).on('keydown', function (e) { - if (!self.modal || !self.modal.is(':visible')) return; - if (e.key === 'Escape') { self.close(); } - else if (e.key === 'ArrowLeft') { self.prevImage(); } - else if (e.key === 'ArrowRight') { self.nextImage(); } - }); - - this.modal.on('click', '.htl-gallery-tab', function () { - var category = $(this).data('category'); - self.filterByCategory(category); - self.modal.find('.htl-gallery-tab').removeClass('active'); - $(this).addClass('active'); - }); - - this.modal.on('click', '.htl-gallery-thumbnail', function () { - var index = parseInt($(this).data('index')); - self.showImage(index); - }); - }, - - open: function (startIndex) { - if (!this.modal || !this.modal.length) return; - this.currentCategory = 'all'; - this.filteredImages = this.images.slice(); - this.modal.find('.htl-gallery-tab').removeClass('active'); - this.modal.find('.htl-gallery-tab[data-category="all"]').addClass('active'); - this.renderThumbnails(); - this.showImage(startIndex || 0); - this.modal.fadeIn(300); - $('body').addClass('gallery-open'); - }, - - close: function () { - if (!this.modal) return; - this.modal.fadeOut(300); - $('body').removeClass('gallery-open'); - }, - - filterByCategory: function (categoryId) { - this.currentCategory = categoryId; - if (categoryId === 'all') { - this.filteredImages = this.images.slice(); - } else { - var targetId = String(categoryId); - this.filteredImages = this.images.filter(function (img) { - return String(img.categoryId) === targetId; - }); - } - this.renderThumbnails(); - if (this.filteredImages.length > 0) { - this.showImage(0); - } - }, - - showImage: function (index) { - if (index < 0 || index >= this.filteredImages.length) return; - this.currentIndex = index; - var image = this.filteredImages[index]; - this.modal.find('#htl-main-image').attr('src', image.src); - this.modal.find('.htl-gallery-thumbnail').removeClass('active'); - this.modal.find('.htl-gallery-thumbnail[data-index="' + index + '"]').addClass('active'); - var thumbContainer = this.modal.find('#htl-gallery-thumbnail'); - var activeThumb = thumbContainer.find('.htl-gallery-thumbnail.active'); - if (activeThumb.length) { - var scrollTarget = activeThumb.position().left + thumbContainer.scrollLeft() - (thumbContainer.width() / 2) + (activeThumb.outerWidth() / 2); - thumbContainer.animate({ scrollLeft: scrollTarget }, 200); - } - }, - - prevImage: function () { - var newIndex = this.currentIndex - 1; - if (newIndex < 0) { newIndex = this.filteredImages.length - 1; } - this.showImage(newIndex); - }, - - nextImage: function () { - var newIndex = this.currentIndex + 1; - if (newIndex >= this.filteredImages.length) { newIndex = 0; } - this.showImage(newIndex); - }, - - renderCategoryTabs: function () { - var self = this; - var tabWrapper = this.modal.find('.htl-full-gallery__tabs-wrapper'); - if (!tabWrapper.length) return; - tabWrapper.empty(); - - var catKeys = Object.keys(this.categories); - var allPhotosLabel = (typeof htl_gallery_all_photos !== 'undefined') ? htl_gallery_all_photos : 'All Photos'; - - var activeCategory = this.currentCategory; - - /* "All Photos" tab is always visible */ - var allTab = $('
    - {/if} {/block} + {$additinal_services_tab} From 05397f20372a3e940b8119b9ee1ec0e176ef4e66 Mon Sep 17 00:00:00 2001 From: shazar rahman Date: Tue, 1 Sep 2026 11:17:28 +0530 Subject: [PATCH 57/78] Fixed: Undefined array key warning on SEO tab when adding a new hotel Co-authored-by: Name --- .../templates/admin/add_hotel/helpers/form/form.tpl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/hotelreservationsystem/views/templates/admin/add_hotel/helpers/form/form.tpl b/modules/hotelreservationsystem/views/templates/admin/add_hotel/helpers/form/form.tpl index 244a174f2..349e02167 100644 --- a/modules/hotelreservationsystem/views/templates/admin/add_hotel/helpers/form/form.tpl +++ b/modules/hotelreservationsystem/views/templates/admin/add_hotel/helpers/form/form.tpl @@ -423,12 +423,12 @@ show_label_tooltip = false show_flag = true inputs = [ - 'meta_title' => $meta_title_info, - 'meta_description' => $meta_description_info, - 'link_rewrite' => $link_rewrite_info, - 'meta_keywords' => $meta_keywords_info, - 'name' => $hotel_info.hotel_name, - 'description_short' => $hotel_info.short_description + 'meta_title' => $meta_title_info|default:'', + 'meta_description' => $meta_description_info|default:'', + 'link_rewrite' => $link_rewrite_info|default:'', + 'meta_keywords' => $meta_keywords_info|default:'', + 'name' => $hotel_info.hotel_name|default:'', + 'description_short' => $hotel_info.short_description|default:'' ] } {hook h='displayAdminAddHotelFormSeoTabAfter' id_hotel=$hook_arg_id_hotel} From c44d1560dc951fca06ca198480cf1ed78ecf72e5 Mon Sep 17 00:00:00 2001 From: azhar bhat Date: Tue, 1 Sep 2026 12:00:29 +0530 Subject: [PATCH 58/78] Fixed warning issues which were displying during order status change to complete --- classes/Translate.php | 6 ++++-- classes/pdf/HTMLTemplateInvoice.php | 3 --- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/classes/Translate.php b/classes/Translate.php index 7fac82462..8da0d39c5 100644 --- a/classes/Translate.php +++ b/classes/Translate.php @@ -255,8 +255,10 @@ class TranslateCore $i18n_file = $override_i18n_file; } - if (!include($i18n_file)) { - Tools::displayError(sprintf('Cannot include PDF translation language file : %s', $i18n_file)); + if (file_exists($i18n_file)) { + if (!include($i18n_file)) { + Tools::displayError(sprintf('Cannot include PDF translation language file : %s', $i18n_file)); + } } if (!isset($_LANGPDF) || !is_array($_LANGPDF)) { diff --git a/classes/pdf/HTMLTemplateInvoice.php b/classes/pdf/HTMLTemplateInvoice.php index e610dee7f..1aa0a425c 100644 --- a/classes/pdf/HTMLTemplateInvoice.php +++ b/classes/pdf/HTMLTemplateInvoice.php @@ -486,9 +486,6 @@ class HTMLTemplateInvoiceCore extends HTMLTemplate $cart_htl_data[$type_key]['date_diff'][$date_join]['id_room'] = $data_v['id_room']; } - $totalDemandsPriceTE += $cart_htl_data[$type_key]['date_diff'][$date_join]['extra_demands_price_te']; - $totalDemandsPriceTI += $cart_htl_data[$type_key]['date_diff'][$date_join]['extra_demands_price_ti']; - $cart_htl_data[$type_key]['date_diff'][$date_join]['total_price_tax_excl'] += $data_v['total_price_tax_excl']; $cart_htl_data[$type_key]['date_diff'][$date_join]['total_price_tax_incl'] += $data_v['total_price_tax_incl']; $cart_htl_data[$type_key]['date_diff'][$date_join]['additional_services_price_auto_add_ti'] += $objServiceProductOrderDetail->getroomTypeServiceProducts( From cd7370a33552a6db3bdc779c05487a2283d75633 Mon Sep 17 00:00:00 2001 From: shazar rahman Date: Tue, 1 Sep 2026 12:58:06 +0530 Subject: [PATCH 59/78] Fixed: Warning triggered when subscription controller is accessed without a token Co-authored-by: Name --- classes/controller/FrontController.php | 5 ++ .../hotel-reservation-theme/access-denied.tpl | 80 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 themes/hotel-reservation-theme/access-denied.tpl diff --git a/classes/controller/FrontController.php b/classes/controller/FrontController.php index 5252400ca..420e39ef2 100644 --- a/classes/controller/FrontController.php +++ b/classes/controller/FrontController.php @@ -665,6 +665,11 @@ class FrontControllerCore extends Controller public function initCursedPage() { $this->displayMaintenancePage(); + + header('HTTP/1.1 403 Forbidden'); + $this->context->smarty->assign($this->initLogoAndFavicon()); + $front_controller = preg_match('/ModuleFrontController$/', get_class($this)) ? new FrontController() : $this; + $this->layout = $front_controller->getTemplatePath($this->getThemeDir().'access-denied.tpl'); } /** diff --git a/themes/hotel-reservation-theme/access-denied.tpl b/themes/hotel-reservation-theme/access-denied.tpl new file mode 100644 index 000000000..14968e4cd --- /dev/null +++ b/themes/hotel-reservation-theme/access-denied.tpl @@ -0,0 +1,80 @@ +{* +* 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 +*} + + + + + {$meta_title|escape:'html':'UTF-8'} + + + + + +
    + +
    +

    {l s='Access denied'}

    +

    {l s='You do not have permission to access this page.'}

    + + From cba04028508c2657451bce82d81046375104bf92 Mon Sep 17 00:00:00 2001 From: azhar bhat Date: Tue, 1 Sep 2026 13:00:45 +0530 Subject: [PATCH 60/78] Fixed: Use the Large image instead of the medium image --- controllers/front/ContactController.php | 2 +- controllers/front/OurPropertiesController.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/controllers/front/ContactController.php b/controllers/front/ContactController.php index acb21798d..8e935e833 100644 --- a/controllers/front/ContactController.php +++ b/controllers/front/ContactController.php @@ -311,7 +311,7 @@ class ContactControllerCore extends FrontController ) ) { // by webkul to get media link. - $htlImgLink = $this->context->link->getMediaLink($objHotelImage->getImageLink($hotel['id_cover_img'], ImageType::getFormatedName('medium'))); + $htlImgLink = $this->context->link->getMediaLink($objHotelImage->getImageLink($hotel['id_cover_img'], ImageType::getFormatedName('large'))); if ((bool)Tools::file_get_contents($htlImgLink)) { $hotel['image_url'] = $htlImgLink; diff --git a/controllers/front/OurPropertiesController.php b/controllers/front/OurPropertiesController.php index 06520f044..d36546d1e 100644 --- a/controllers/front/OurPropertiesController.php +++ b/controllers/front/OurPropertiesController.php @@ -49,7 +49,7 @@ class OurPropertiesControllerCore extends FrontController && $hotel['id_cover_img'] && Validate::isLoadedObject($objHotelImage = new HotelImage($hotel['id_cover_img'])) ) { - $htlImgLink = $this->context->link->getMediaLink($objHotelImage->getImageLink($hotel['id_cover_img'], ImageType::getFormatedName('medium'))); + $htlImgLink = $this->context->link->getMediaLink($objHotelImage->getImageLink($hotel['id_cover_img'], ImageType::getFormatedName('large'))); if ((bool)Tools::file_get_contents($htlImgLink)) { $hotelsInfo[$hotelKey]['image_url'] = $htlImgLink; } else { From ef667de7911dacb04e1947905f7b840f1f4f5484 Mon Sep 17 00:00:00 2001 From: shazar rahman Date: Tue, 1 Sep 2026 15:27:01 +0530 Subject: [PATCH 61/78] Fixed: Extra services details are not visible on order confirmation page Co-authored-by: Name --- controllers/front/GuestTrackingController.php | 6 ++++++ themes/hotel-reservation-theme/js/order-confirmation.js | 2 ++ themes/hotel-reservation-theme/order-confirmation.tpl | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/controllers/front/GuestTrackingController.php b/controllers/front/GuestTrackingController.php index c685eb430..26b5ede23 100644 --- a/controllers/front/GuestTrackingController.php +++ b/controllers/front/GuestTrackingController.php @@ -110,6 +110,12 @@ class GuestTrackingControllerCore extends FrontController { $response = array('extra_demands' => false); + $order = new Order((int) Tools::getValue('id_order')); + + if (!Validate::isLoadedObject($order) || $order->secure_key != Tools::getValue('secure_key')) { + $this->ajaxDie(json_encode($response)); + } + if (($idProduct = Tools::getValue('id_product')) && ($idOrder = Tools::getValue('id_order')) && ($dateFrom = Tools::getValue('date_from')) diff --git a/themes/hotel-reservation-theme/js/order-confirmation.js b/themes/hotel-reservation-theme/js/order-confirmation.js index 615bee6ff..6aa9ae3e1 100644 --- a/themes/hotel-reservation-theme/js/order-confirmation.js +++ b/themes/hotel-reservation-theme/js/order-confirmation.js @@ -26,6 +26,7 @@ $(document).ready(function(){ var idOrder = $(this).data('id_order'); var dateFrom = $(this).data('date_from'); var dateTo = $(this).data('date_to'); + var secureKey = $(this).data('secure_key'); var action = $(this).data('action'); $.ajax({ type: 'POST', @@ -40,6 +41,7 @@ $(document).ready(function(){ date_to: dateTo, id_product: idProduct, id_order: idOrder, + secure_key: secureKey, action: 'getRoomTypeBookingDemands', method: 'getRoomTypeBookingDemands', ajax: true, diff --git a/themes/hotel-reservation-theme/order-confirmation.tpl b/themes/hotel-reservation-theme/order-confirmation.tpl index e8cc450cc..d4c6b3ce5 100644 --- a/themes/hotel-reservation-theme/order-confirmation.tpl +++ b/themes/hotel-reservation-theme/order-confirmation.tpl @@ -136,7 +136,7 @@
    {l s='Extra Services'}
    {if (isset($rm_v['extra_demands']) && $rm_v['extra_demands']) || isset($rm_v['additional_services']) && $rm_v['additional_services']} - + {/if} {if $group_use_tax} {displayWtPriceWithCurrency price=($rm_v['extra_demands_price_ti'] + $rm_v['additional_services_price_ti']) currency=$objOrderCurrency} From d67fe81ef855d2016b5b781c03ad2f78eef092dc Mon Sep 17 00:00:00 2001 From: shazar rahman Date: Tue, 1 Sep 2026 19:32:27 +0530 Subject: [PATCH 62/78] Fixed: Calendar not working correctly throughout backoffice Co-authored-by: Name --- js/admin/orders.js | 4 +--- js/jquery/ui/jquery.effects.core.min.js | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/js/admin/orders.js b/js/admin/orders.js index 0f3a5a38a..4fa65de93 100644 --- a/js/admin/orders.js +++ b/js/admin/orders.js @@ -25,9 +25,7 @@ var current_product = null; var ajaxQueries = new Array(); -if (typeof jQuery !== 'undefined' && jQuery.effects && !jQuery.effects.effect) { - jQuery.effects.effect = {}; -} + $(document).ready(function() { // Init all events init(); diff --git a/js/jquery/ui/jquery.effects.core.min.js b/js/jquery/ui/jquery.effects.core.min.js index a2bec4544..bef2f09d0 100644 --- a/js/jquery/ui/jquery.effects.core.min.js +++ b/js/jquery/ui/jquery.effects.core.min.js @@ -16,7 +16,7 @@ a;b=null;a=c;c=a.effect}if(f.isFunction(a)){d=a;b=null;a={}}if(typeof a=="number 211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0],transparent:[255,255,255]},q=["add","remove","toggle"],t={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};f.effects.animateClass=function(c,a,b, d){if(f.isFunction(b)){d=b;b=null}return this.queue(function(){var e=f(this),g=e.attr("style")||" ",h=p(o.call(this)),r,v=e.attr("class");f.each(q,function(w,i){c[i]&&e[i+"Class"](c[i])});r=p(o.call(this));e.attr("class",v);e.animate(u(h,r),{queue:false,duration:a,easing:b,complete:function(){f.each(q,function(w,i){c[i]&&e[i+"Class"](c[i])});if(typeof e.attr("style")=="object"){e.attr("style").cssText="";e.attr("style").cssText=g}else e.attr("style",g);d&&d.apply(this,arguments);f.dequeue(this)}})})}; f.fn.extend({_addClass:f.fn.addClass,addClass:function(c,a,b,d){return a?f.effects.animateClass.apply(this,[{add:c},a,b,d]):this._addClass(c)},_removeClass:f.fn.removeClass,removeClass:function(c,a,b,d){return a?f.effects.animateClass.apply(this,[{remove:c},a,b,d]):this._removeClass(c)},_toggleClass:f.fn.toggleClass,toggleClass:function(c,a,b,d,e){return typeof a=="boolean"||a===j?b?f.effects.animateClass.apply(this,[a?{add:c}:{remove:c},b,d,e]):this._toggleClass(c,a):f.effects.animateClass.apply(this, -[{toggle:c},a,b,d])},switchClass:function(c,a,b,d,e){return f.effects.animateClass.apply(this,[{add:a,remove:c},b,d,e])}});f.extend(f.effects,{version:"1.8.16",save:function(c,a){for(var b=0;b
    ").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}), d=document.activeElement;c.wrap(b);if(c[0]===d||f.contains(c[0],d))f(d).focus();b=c.parent();if(c.css("position")=="static"){b.css({position:"relative"});c.css({position:"relative"})}else{f.extend(a,{position:c.css("position"),zIndex:c.css("z-index")});f.each(["top","left","bottom","right"],function(e,g){a[g]=c.css(g);if(isNaN(parseInt(a[g],10)))a[g]="auto"});c.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})}return b.css(a).show()},removeWrapper:function(c){var a,b=document.activeElement; if(c.parent().is(".ui-effects-wrapper")){a=c.parent().replaceWith(c);if(c[0]===b||f.contains(c[0],b))f(b).focus();return a}return c},setTransition:function(c,a,b,d){d=d||{};f.each(a,function(e,g){unit=c.cssUnit(g);if(unit[0]>0)d[g]=unit[0]*b+unit[1]});return d}});f.fn.extend({effect:function(c){var a=k.apply(this,arguments),b={options:a[1],duration:a[2],callback:a[3]};a=b.options.mode;var d=f.effects[c];if(f.fx.off||!d)return a?this[a](b.duration,b.callback):this.each(function(){b.callback&&b.callback.call(this)}); From e974eb51b9a7c404b204bb439e5b76288b6523a4 Mon Sep 17 00:00:00 2001 From: amit sharma Date: Wed, 2 Sep 2026 15:12:53 +0530 Subject: [PATCH 63/78] Fixed: downgrade cron expression library Co-authored-by: Name --- modules/qlocrontaskmanager/composer.json | 2 +- .../vendor/composer/installed.json | 16 ++++++++-------- .../vendor/composer/installed.php | 10 +++++----- .../vendor/composer/platform_check.php | 4 ++-- .../dragonmantank/cron-expression/composer.json | 2 +- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/modules/qlocrontaskmanager/composer.json b/modules/qlocrontaskmanager/composer.json index 28ab8bb00..566eb0609 100644 --- a/modules/qlocrontaskmanager/composer.json +++ b/modules/qlocrontaskmanager/composer.json @@ -2,6 +2,6 @@ "name": "qloapps/qlocrontaskmanager", "description": "Centralized cron task manager for QloApps modules", "require": { - "dragonmantank/cron-expression": "^3.0" + "dragonmantank/cron-expression": "3.5.0" } } diff --git a/modules/qlocrontaskmanager/vendor/composer/installed.json b/modules/qlocrontaskmanager/vendor/composer/installed.json index 0228ed9d2..d4a2783e3 100644 --- a/modules/qlocrontaskmanager/vendor/composer/installed.json +++ b/modules/qlocrontaskmanager/vendor/composer/installed.json @@ -2,21 +2,21 @@ "packages": [ { "name": "dragonmantank/cron-expression", - "version": "v3.6.0", - "version_normalized": "3.6.0.0", + "version": "v3.5.0", + "version_normalized": "3.5.0.0", "source": { "type": "git", "url": "https://github.com/dragonmantank/cron-expression.git", - "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013" + "reference": "1b2de7f4a468165dca07b142240733a1973e766d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013", - "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/1b2de7f4a468165dca07b142240733a1973e766d", + "reference": "1b2de7f4a468165dca07b142240733a1973e766d", "shasum": "" }, "require": { - "php": "^8.2|^8.3|^8.4|^8.5" + "php": "^7.2|^8.0" }, "replace": { "mtdowling/cron-expression": "^1.0" @@ -26,7 +26,7 @@ "phpstan/phpstan": "^1.12.32|^2.1.31", "phpunit/phpunit": "^8.5.48|^9.0" }, - "time": "2025-10-31T18:51:33+00:00", + "time": "2025-10-31T18:36:32+00:00", "type": "library", "extra": { "branch-alias": { @@ -57,7 +57,7 @@ ], "support": { "issues": "https://github.com/dragonmantank/cron-expression/issues", - "source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.0" + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.5.0" }, "funding": [ { diff --git a/modules/qlocrontaskmanager/vendor/composer/installed.php b/modules/qlocrontaskmanager/vendor/composer/installed.php index 65c12da9b..7225406da 100644 --- a/modules/qlocrontaskmanager/vendor/composer/installed.php +++ b/modules/qlocrontaskmanager/vendor/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'qloapps/qlocrontaskmanager', 'pretty_version' => 'dev-develop', 'version' => 'dev-develop', - 'reference' => 'f7d1d629ca72fc5ffd3349dea2877013367cea81', + 'reference' => '8d04d350b83cd28b5d141bf0cb287d4ba95de7d0', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -11,9 +11,9 @@ ), 'versions' => array( 'dragonmantank/cron-expression' => array( - 'pretty_version' => 'v3.6.0', - 'version' => '3.6.0.0', - 'reference' => 'd61a8a9604ec1f8c3d150d09db6ce98b32675013', + 'pretty_version' => 'v3.5.0', + 'version' => '3.5.0.0', + 'reference' => '1b2de7f4a468165dca07b142240733a1973e766d', 'type' => 'library', 'install_path' => __DIR__ . '/../dragonmantank/cron-expression', 'aliases' => array(), @@ -28,7 +28,7 @@ 'qloapps/qlocrontaskmanager' => array( 'pretty_version' => 'dev-develop', 'version' => 'dev-develop', - 'reference' => 'f7d1d629ca72fc5ffd3349dea2877013367cea81', + 'reference' => '8d04d350b83cd28b5d141bf0cb287d4ba95de7d0', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), diff --git a/modules/qlocrontaskmanager/vendor/composer/platform_check.php b/modules/qlocrontaskmanager/vendor/composer/platform_check.php index d32d90c6a..589e9e770 100644 --- a/modules/qlocrontaskmanager/vendor/composer/platform_check.php +++ b/modules/qlocrontaskmanager/vendor/composer/platform_check.php @@ -4,8 +4,8 @@ $issues = array(); -if (!(PHP_VERSION_ID >= 80200)) { - $issues[] = 'Your Composer dependencies require a PHP version ">= 8.2.0". You are running ' . PHP_VERSION . '.'; +if (!(PHP_VERSION_ID >= 70200)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.0". You are running ' . PHP_VERSION . '.'; } if ($issues) { diff --git a/modules/qlocrontaskmanager/vendor/dragonmantank/cron-expression/composer.json b/modules/qlocrontaskmanager/vendor/dragonmantank/cron-expression/composer.json index d2e23b860..ccfbc812b 100644 --- a/modules/qlocrontaskmanager/vendor/dragonmantank/cron-expression/composer.json +++ b/modules/qlocrontaskmanager/vendor/dragonmantank/cron-expression/composer.json @@ -12,7 +12,7 @@ } ], "require": { - "php": "^8.2|^8.3|^8.4|^8.5" + "php": "^7.2|^8.0" }, "require-dev": { "phpstan/phpstan": "^1.12.32|^2.1.31", From ff21552d43fcef4db014232f934ca11bf15de7cd Mon Sep 17 00:00:00 2001 From: shazar rahman Date: Wed, 2 Sep 2026 19:34:10 +0530 Subject: [PATCH 64/78] Fixed: Uncaught TypeError on Preference's Orders page on Back-office Co-authored-by: Name --- .../controllers/order_preferences/helpers/options/options.tpl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/admin/themes/default/template/controllers/order_preferences/helpers/options/options.tpl b/admin/themes/default/template/controllers/order_preferences/helpers/options/options.tpl index 45eb6a349..b9fb5a7b2 100644 --- a/admin/themes/default/template/controllers/order_preferences/helpers/options/options.tpl +++ b/admin/themes/default/template/controllers/order_preferences/helpers/options/options.tpl @@ -100,6 +100,8 @@ }); } {/if} + + changeStandardProductAddressType(); }); {/if} @@ -110,6 +112,5 @@ {/block} From ee59e238f03f743933a5a5e77ffb07c92abe4481 Mon Sep 17 00:00:00 2001 From: shazar rahman Date: Wed, 2 Sep 2026 19:45:54 +0530 Subject: [PATCH 65/78] Fixed: Undefined array key warning in room type configuration section on back office Co-authored-by: Name --- .../default/template/controllers/products/configuration.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/themes/default/template/controllers/products/configuration.tpl b/admin/themes/default/template/controllers/products/configuration.tpl index 37bd27107..7b0e8764f 100644 --- a/admin/themes/default/template/controllers/products/configuration.tpl +++ b/admin/themes/default/template/controllers/products/configuration.tpl @@ -121,7 +121,7 @@ + data-room-type="{if isset($room_info['id_product'])}{$room_info['id_product']|intval}{/if}"> From 7fc4ecc91fa1f63279836e0a67690c89c79bd6be Mon Sep 17 00:00:00 2001 From: amit sharma Date: Thu, 3 Sep 2026 14:43:40 +0530 Subject: [PATCH 66/78] Fixed: fix review changes --- .../template/controllers/orders/form.tpl | 4 ++-- classes/Source.php | 2 +- classes/helper/HelperKpiRow.php | 2 +- controllers/admin/AdminOrdersController.php | 23 +++++++++++-------- controllers/admin/AdminSourcesController.php | 2 +- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/admin/themes/default/template/controllers/orders/form.tpl b/admin/themes/default/template/controllers/orders/form.tpl index 61adcc07a..1648045f8 100644 --- a/admin/themes/default/template/controllers/orders/form.tpl +++ b/admin/themes/default/template/controllers/orders/form.tpl @@ -2348,13 +2348,13 @@ {assign var="prev_business_source" value=$booking_source.business_source_name} {/if} - {/foreach} {if $booking_sources}{/if} -

    {l s='Leave unselected to use the default Walk-in source.'}

    +

    {l s='Walk-in is selected by default. Change it if this booking came from a different source.'}

    diff --git a/classes/Source.php b/classes/Source.php index e124e8cf5..4f453e96b 100644 --- a/classes/Source.php +++ b/classes/Source.php @@ -150,7 +150,7 @@ class SourceCore extends ObjectModel } return Db::getInstance()->executeS(' - SELECT a.`id_source`, a.`id_business_source`, al.`name`, bsl.`name` AS `business_source_name` + SELECT a.`id_source`, a.`id_business_source`, a.`code`, al.`name`, bsl.`name` AS `business_source_name` FROM `'._DB_PREFIX_.'source` a LEFT JOIN `'._DB_PREFIX_.'source_lang` al ON (al.`id_source` = a.`id_source` AND al.`id_lang` = '.(int)$idLang.') LEFT JOIN `'._DB_PREFIX_.'business_source_lang` bsl ON (bsl.`id_business_source` = a.`id_business_source` AND bsl.`id_lang` = '.(int)$idLang.') diff --git a/classes/helper/HelperKpiRow.php b/classes/helper/HelperKpiRow.php index f47022bf7..b10bce643 100644 --- a/classes/helper/HelperKpiRow.php +++ b/classes/helper/HelperKpiRow.php @@ -44,7 +44,7 @@ class HelperKpiRowCore extends Helper if (isset($this->context->cookie->$cookieKey)) { $kpi->visible = (bool) $this->context->cookie->$cookieKey; } else { - $kpi->visible = true; + $kpi->visible = isset($kpi->visible) ? (bool) $kpi->visible : true; } $countVisible = $kpi->visible ? ++$countVisible : $countVisible; diff --git a/controllers/admin/AdminOrdersController.php b/controllers/admin/AdminOrdersController.php index 4bd81bb4d..7c71ec915 100644 --- a/controllers/admin/AdminOrdersController.php +++ b/controllers/admin/AdminOrdersController.php @@ -288,7 +288,6 @@ class AdminOrdersControllerCore extends AdminController 'filter_key' => 'a!source', 'list' => $this->all_order_sources, 'optional' => true, - 'visible_default' => true ), 'booking_source_name' => array( 'title' => $this->l('Booking Source'), @@ -393,14 +392,7 @@ class AdminOrdersControllerCore extends AdminController return '--'; } - $html = Tools::safeOutput($echo); - - if (isset($row['booking_source_code']) && $row['booking_source_code'] === 'DIRECT_WEBSITE') { - $title = $this->l('Visit website', 'AdminOrders', true); - $html .= ' '; - } - - return $html; + return Tools::safeOutput($echo); } public function formatStayPeriods($value, $row) @@ -3026,12 +3018,25 @@ class AdminOrdersControllerCore extends AdminController $helper->id = 'box-order-source'; $helper->icon = 'icon-globe'; $helper->color = 'color1'; + $helper->visible = false; $helper->title = $this->l('Order Source'); $helper->tooltip = $this->l('Order source shows from which source the order was placed.'); $helper->subtitle = $orderHistory[0]['id_employee'] ? $this->l('Back office') : $this->l('Front office'); $helper->value = $objOrder->source; $this->kpis[] = $helper; + $objBookingSource = new Source((int)$objOrder->id_source, $this->context->language->id); + if (Validate::isLoadedObject($objBookingSource)) { + $helper = new HelperKpi(); + $helper->id = 'box-booking-source'; + $helper->icon = 'icon-tag'; + $helper->color = 'color2'; + $helper->title = $this->l('Booking Source'); + $helper->tooltip = $this->l('Booking source shows the source from which this booking originated, e.g. Walk-in, Website, OTA.'); + $helper->value = $objBookingSource->name; + $this->kpis[] = $helper; + } + $objCustomerThread = new CustomerThread(); $idCustomerThread = $objCustomerThread->getIdCustomerThreadByIdOrder($objOrder->id); $helper = new HelperKpi(); diff --git a/controllers/admin/AdminSourcesController.php b/controllers/admin/AdminSourcesController.php index 81a6557c8..c51d5dd36 100644 --- a/controllers/admin/AdminSourcesController.php +++ b/controllers/admin/AdminSourcesController.php @@ -176,7 +176,7 @@ class AdminSourcesControllerCore extends AdminController 'class' => 'fixed-width-xs' ), 'name' => array( - 'title' => $this->l('Name'), + 'title' => $this->l('Booking Source'), 'width' => 'auto', 'filter_key' => 'b!name' ), From 437ca81d8616c92a0816c7669a7bb6946872ef70 Mon Sep 17 00:00:00 2001 From: azhar bhat Date: Thu, 3 Sep 2026 15:33:40 +0530 Subject: [PATCH 67/78] added extra info in tooltip --- .../default/template/controllers/orders/form.tpl | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/admin/themes/default/template/controllers/orders/form.tpl b/admin/themes/default/template/controllers/orders/form.tpl index c5ea9c11a..1b95df2cf 100644 --- a/admin/themes/default/template/controllers/orders/form.tpl +++ b/admin/themes/default/template/controllers/orders/form.tpl @@ -2322,8 +2322,13 @@

    {l s='If disabled, no mail related to this order will be sent during order creation.'}

    -
    - +
    +
    @@ -2332,8 +2337,7 @@ -

    {l s='Keep this option enabled for full payment and disable it to take partial payment of the booking.'}

    -

    +

    {l s='Total amount: '} {displayPrice price=$order_total currency=$currency->id}

    From cfe2f5663662894eb1a8778beb1e9a7d902aeac7 Mon Sep 17 00:00:00 2001 From: azhar bhat Date: Thu, 3 Sep 2026 17:18:41 +0530 Subject: [PATCH 68/78] Fix remove background color and fix the database id missmatch issue --- .../hotelreservationsystem/classes/HotelReservationSystemDb.php | 2 +- modules/hotelreservationsystem/views/css/qhrs_header_media.css | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/hotelreservationsystem/classes/HotelReservationSystemDb.php b/modules/hotelreservationsystem/classes/HotelReservationSystemDb.php index 54daef146..aeee8e2d6 100644 --- a/modules/hotelreservationsystem/classes/HotelReservationSystemDb.php +++ b/modules/hotelreservationsystem/classes/HotelReservationSystemDb.php @@ -507,7 +507,7 @@ class HotelReservationSystemDb ORDER BY `id_lang`;", "INSERT IGNORE INTO `"._DB_PREFIX_."htl_settings_link_lang` (`id_settings_link`, `id_lang`, `name`, `hint`) - SELECT 6, `id_lang`, 'Landing Page Header Media', 'Configure and manage header images or videos displayed on the home page.' + SELECT 5, `id_lang`, 'Landing Page Header Media', 'Configure and manage header images or videos displayed on the home page.' FROM `"._DB_PREFIX_."lang` ORDER BY `id_lang`;", "CREATE TABLE IF NOT EXISTS `"._DB_PREFIX_."htl_connected_room` ( diff --git a/modules/hotelreservationsystem/views/css/qhrs_header_media.css b/modules/hotelreservationsystem/views/css/qhrs_header_media.css index 96a676c3e..e32734df0 100644 --- a/modules/hotelreservationsystem/views/css/qhrs_header_media.css +++ b/modules/hotelreservationsystem/views/css/qhrs_header_media.css @@ -156,7 +156,6 @@ left: 0; right: 0; bottom: 0; - background: rgba(0, 0, 0, 0.28); z-index: 1; pointer-events: none; } From 2bcb9bb0c602fb8721a59ccfa8cf053c15b6468c Mon Sep 17 00:00:00 2001 From: shazar rahman Date: Thu, 3 Sep 2026 17:59:58 +0530 Subject: [PATCH 69/78] Updated: Code review changes implmented Co-authored-by: Name --- classes/controller/FrontController.php | 37 +++++++++- .../hotel-reservation-theme/access-denied.tpl | 68 ++++--------------- 2 files changed, 47 insertions(+), 58 deletions(-) diff --git a/classes/controller/FrontController.php b/classes/controller/FrontController.php index 420e39ef2..64584dd2f 100644 --- a/classes/controller/FrontController.php +++ b/classes/controller/FrontController.php @@ -667,9 +667,42 @@ class FrontControllerCore extends Controller $this->displayMaintenancePage(); header('HTTP/1.1 403 Forbidden'); - $this->context->smarty->assign($this->initLogoAndFavicon()); + + $this->setMedia(); + $this->initHeader(); + + if (!isset($this->context->cart)) { + $this->context->cart = new Cart(); + } + + if (!$this->useMobileTheme()) { + $this->context->smarty->assign(array( + 'HOOK_HEADER' => Hook::exec('displayHeader'), + 'HOOK_TOP' => Hook::exec('displayTop'), + 'HOOK_LEFT_COLUMN' => ($this->display_column_left ? Hook::exec('displayLeftColumn') : ''), + 'HOOK_RIGHT_COLUMN' => ($this->display_column_right ? Hook::exec('displayRightColumn', array('cart' => $this->context->cart)) : ''), + 'show_breadcrump' => $this->show_breadcrump, + )); + } else { + $this->context->smarty->assign('HOOK_MOBILE_HEADER', Hook::exec('displayMobileHeader')); + } + + $this->initFooter(); + + $this->context->smarty->assign(array( + 'css_files' => $this->css_files, + 'js_files' => ($this->getLayout() && (bool)Configuration::get('PS_JS_DEFER')) ? array() : $this->js_files, + 'js_defer' => (bool)Configuration::get('PS_JS_DEFER'), + 'errors' => $this->errors, + 'display_header' => $this->display_header, + 'display_footer' => $this->display_footer, + )); + $front_controller = preg_match('/ModuleFrontController$/', get_class($this)) ? new FrontController() : $this; - $this->layout = $front_controller->getTemplatePath($this->getThemeDir().'access-denied.tpl'); + $template = $this->context->smarty->fetch($front_controller->getTemplatePath($this->getThemeDir().'access-denied.tpl')); + $this->context->controller = $this; + $this->context->smarty->assign('template', $template); + $this->layout = $this->getLayout(); } /** diff --git a/themes/hotel-reservation-theme/access-denied.tpl b/themes/hotel-reservation-theme/access-denied.tpl index 14968e4cd..fc039caaf 100644 --- a/themes/hotel-reservation-theme/access-denied.tpl +++ b/themes/hotel-reservation-theme/access-denied.tpl @@ -19,62 +19,18 @@ * @copyright Since 2010 Webkul * @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0 *} - - - - - {$meta_title|escape:'html':'UTF-8'} - - - - - -
    - + -

    {l s='Access denied'}

    -

    {l s='You do not have permission to access this page.'}

    - - +
    +
    From 1129022077e12423da822444d9a77778d4d9b061 Mon Sep 17 00:00:00 2001 From: shazar rahman Date: Fri, 4 Sep 2026 14:58:08 +0530 Subject: [PATCH 70/78] Updated: Code and flow review changes done Co-authored-by: Name --- classes/controller/FrontController.php | 2 -- themes/hotel-reservation-theme/access-denied.tpl | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/classes/controller/FrontController.php b/classes/controller/FrontController.php index 64584dd2f..01e39fdb8 100644 --- a/classes/controller/FrontController.php +++ b/classes/controller/FrontController.php @@ -679,8 +679,6 @@ class FrontControllerCore extends Controller $this->context->smarty->assign(array( 'HOOK_HEADER' => Hook::exec('displayHeader'), 'HOOK_TOP' => Hook::exec('displayTop'), - 'HOOK_LEFT_COLUMN' => ($this->display_column_left ? Hook::exec('displayLeftColumn') : ''), - 'HOOK_RIGHT_COLUMN' => ($this->display_column_right ? Hook::exec('displayRightColumn', array('cart' => $this->context->cart)) : ''), 'show_breadcrump' => $this->show_breadcrump, )); } else { diff --git a/themes/hotel-reservation-theme/access-denied.tpl b/themes/hotel-reservation-theme/access-denied.tpl index fc039caaf..e612ae996 100644 --- a/themes/hotel-reservation-theme/access-denied.tpl +++ b/themes/hotel-reservation-theme/access-denied.tpl @@ -21,6 +21,7 @@ *}
    +

    {l s='Access denied'}

    From 0f86fa55fd14bba80440c204615d652071588e0e Mon Sep 17 00:00:00 2001 From: azhar bhat Date: Fri, 4 Sep 2026 16:03:19 +0530 Subject: [PATCH 71/78] Fixed : Added title field and updated ui --- .../classes/HotelHeaderImage.php | 56 ++-- .../classes/HotelHelper.php | 17 +- .../classes/HotelReservationSystemDb.php | 10 +- .../admin/AdminHotelHeaderImageController.php | 282 ++++++++++-------- .../hotelreservationsystem.php | 14 +- .../views/css/qhrs_header_media.css | 8 +- .../js/admin/qhrs_hotel_header_image_admin.js | 251 ++++++++-------- .../views/js/qhrs_hotel_header_media_front.js | 52 ++-- .../_partials/htl-header-image-row.tpl | 24 +- .../hotel_header_image/helpers/view/view.tpl | 201 ++++++++----- .../templates/hook/headerHotelDescBlock.tpl | 6 +- themes/hotel-reservation-theme/header.tpl | 10 +- 12 files changed, 498 insertions(+), 433 deletions(-) diff --git a/modules/hotelreservationsystem/classes/HotelHeaderImage.php b/modules/hotelreservationsystem/classes/HotelHeaderImage.php index 659b0b0ad..d9eee4643 100644 --- a/modules/hotelreservationsystem/classes/HotelHeaderImage.php +++ b/modules/hotelreservationsystem/classes/HotelHeaderImage.php @@ -47,15 +47,16 @@ class HotelHeaderImage extends ObjectModel const CONTENT_ALIGN_LEFT = 1; const CONTENT_ALIGN_CENTER = 2; const CONTENT_ALIGN_RIGHT = 3; + const TITLE_LIMIT = 128; public $name; public $position; - public $tag_line; - public $tag_line_color = '#ffffff'; - public $tag_line_font_size = 16; - public $tag_line_font_weight = '400'; + public $title; + public $description; + public $description_color = '#ffffff'; + public $description_font_size = 16; + public $description_font_weight = '400'; public $active; - public $show_hotel_chain_name = 1; public $date_add; public $date_upd; @@ -64,16 +65,16 @@ class HotelHeaderImage extends ObjectModel 'primary' => 'id_header_image', 'multilang' => true, 'fields' => array( - 'name' => array('type' => self::TYPE_STRING, 'size' => 512), - 'tag_line' => array('type' => self::TYPE_STRING, 'lang' => true, 'size' => 512), - 'tag_line_color' => array('type' => self::TYPE_STRING, 'validate' => 'isColor', 'size' => 7), - 'tag_line_font_size' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'), - 'tag_line_font_weight' => array('type' => self::TYPE_STRING, 'size' => 10), - 'position' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'), - 'active' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), - 'show_hotel_chain_name' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), - 'date_add' => array('type' => self::TYPE_DATE, 'validate' => 'isDate'), - 'date_upd' => array('type' => self::TYPE_DATE, 'validate' => 'isDate'), + 'name' => array('type' => self::TYPE_STRING, 'size' => 512), + 'title' => array('type' => self::TYPE_STRING, 'lang' => true, 'size' => self::TITLE_LIMIT), + 'description' => array('type' => self::TYPE_STRING, 'lang' => true, 'size' => 512), + 'description_color' => array('type' => self::TYPE_STRING, 'validate' => 'isColor', 'size' => 7), + 'description_font_size' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'), + 'description_font_weight' => array('type' => self::TYPE_STRING, 'size' => 10), + 'position' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'), + 'active' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'), + 'date_add' => array('type' => self::TYPE_DATE, 'validate' => 'isDate'), + 'date_upd' => array('type' => self::TYPE_DATE, 'validate' => 'isDate'), ), ); @@ -118,8 +119,8 @@ class HotelHeaderImage extends ObjectModel * Fetches image rows, optionally filtered by active status. * * @param int|null $active 1 = active only, 0 = inactive only, null = all - * @param int|null $idLang Language id for tag_line; null = context default - * @param bool $withAllLangs true = also include tag_lines[id_lang] map (admin edit forms) + * @param int|null $idLang Language id for title/description; null = context default + * @param bool $withAllLangs true = also include titles[id_lang]/descriptions[id_lang] maps (admin edit forms) * @return array */ public static function getItems($active = 1, $idLang = null, $withAllLangs = false) @@ -129,7 +130,7 @@ class HotelHeaderImage extends ObjectModel } if (!$withAllLangs) { - $sql = 'SELECT m.*, IFNULL(ml.`tag_line`, \'\') AS `tag_line` + $sql = 'SELECT m.*, IFNULL(ml.`title`, \'\') AS `title`, IFNULL(ml.`description`, \'\') AS `description` FROM `'._DB_PREFIX_.'htl_header_image` m LEFT JOIN `'._DB_PREFIX_.'htl_header_image_lang` ml ON (m.`id_header_image` = ml.`id_header_image` @@ -141,7 +142,7 @@ class HotelHeaderImage extends ObjectModel return Db::getInstance()->executeS($sql) ?: array(); } - $sql = 'SELECT m.*, ml.`id_lang`, IFNULL(ml.`tag_line`, \'\') AS `lang_tag_line` + $sql = 'SELECT m.*, ml.`id_lang`, IFNULL(ml.`title`, \'\') AS `lang_title`, IFNULL(ml.`description`, \'\') AS `lang_description` FROM `'._DB_PREFIX_.'htl_header_image` m LEFT JOIN `'._DB_PREFIX_.'htl_header_image_lang` ml ON m.`id_header_image` = ml.`id_header_image`'; @@ -160,16 +161,21 @@ class HotelHeaderImage extends ObjectModel $id = (int)$row['id_header_image']; if (!isset($itemsMap[$id])) { $itemsMap[$id] = $row; - $itemsMap[$id]['tag_line'] = ''; - $itemsMap[$id]['tag_lines'] = array(); - unset($itemsMap[$id]['id_lang'], $itemsMap[$id]['lang_tag_line']); + $itemsMap[$id]['title'] = ''; + $itemsMap[$id]['titles'] = array(); + $itemsMap[$id]['description'] = ''; + $itemsMap[$id]['descriptions'] = array(); + unset($itemsMap[$id]['id_lang'], $itemsMap[$id]['lang_title'], $itemsMap[$id]['lang_description']); } if (isset($row['id_lang'])) { $langId = (int)$row['id_lang']; - $tagLine = $row['lang_tag_line']; - $itemsMap[$id]['tag_lines'][$langId] = $tagLine; + $title = $row['lang_title']; + $description = $row['lang_description']; + $itemsMap[$id]['titles'][$langId] = $title; + $itemsMap[$id]['descriptions'][$langId] = $description; if ($langId === (int)$idLang) { - $itemsMap[$id]['tag_line'] = $tagLine; + $itemsMap[$id]['title'] = $title; + $itemsMap[$id]['description'] = $description; } } } diff --git a/modules/hotelreservationsystem/classes/HotelHelper.php b/modules/hotelreservationsystem/classes/HotelHelper.php index 4b38980b2..28da9cbaa 100644 --- a/modules/hotelreservationsystem/classes/HotelHelper.php +++ b/modules/hotelreservationsystem/classes/HotelHelper.php @@ -1070,7 +1070,7 @@ class HotelHelper Configuration::updateValue('WK_TITLE_HEADER_BLOCK', $home_banner_default_title); Configuration::updateValue('WK_CONTENT_HEADER_BLOCK', $home_banner_default_content); Configuration::updateValue('QLO_HEADER_MEDIA_TYPE', HotelHeaderImage::MEDIA_TYPE_IMAGE); - Configuration::updateValue('QLO_HEADER_SLIDER_NAV_TYPE', HotelHeaderImage::NAV_TYPE_DOTS); + Configuration::updateValue('QLO_HEADER_SLIDER_NAV_TYPE', HotelHeaderImage::NAV_TYPE_ARROWS); Configuration::updateValue('QLO_HEADER_SLIDER_AUTO_PLAY', 1); Configuration::updateValue('QLO_HEADER_SLIDER_INTERVAL', 5000); Configuration::updateValue('QLO_HEADER_SLIDER_ANIM_TYPE', HotelHeaderImage::ANIMATION_TYPE_SLIDE); @@ -1092,20 +1092,13 @@ class HotelHelper ); $newId = (int)Db::getInstance()->Insert_ID(); if ($newId) { - $defaultTagLines = array( - 'en' => 'A place where comfort and luxury are blended with nature!', - 'nl' => 'Een plek waar comfort en luxe worden gecombineerd met de natuur!', - 'fr' => 'Un endroit où le confort et le luxe se mêlent à la nature!', - 'de' => 'Ein Ort, an dem Komfort und Luxus mit der Natur verschmelzen!', - 'ru' => 'Место, где комфорт и роскошь сочетаются с природой!', - 'es' => '¡Un lugar donde el confort y el lujo se mezclan con la naturaleza!', - ); foreach (Language::getLanguages(false) as $lang) { - $tagLine = isset($defaultTagLines[$lang['iso_code']]) ? $defaultTagLines[$lang['iso_code']] : $defaultTagLines['en']; + $title = isset($homeBannerTitleLang[$lang['iso_code']]) ? $homeBannerTitleLang[$lang['iso_code']] : $homeBannerTitleLang['en']; + $description = isset($homeBannerContentLang[$lang['iso_code']]) ? $homeBannerContentLang[$lang['iso_code']] : $homeBannerContentLang['en']; Db::getInstance()->execute( 'INSERT INTO `'._DB_PREFIX_.'htl_header_image_lang` - (`id_header_image`, `id_lang`, `tag_line`) - VALUES ('.$newId.', '.(int)$lang['id_lang'].', \''.pSQL($tagLine).'\')' + (`id_header_image`, `id_lang`, `title`, `description`) + VALUES ('.$newId.', '.(int)$lang['id_lang'].', \''.pSQL($title).'\', \''.pSQL($description).'\')' ); } } diff --git a/modules/hotelreservationsystem/classes/HotelReservationSystemDb.php b/modules/hotelreservationsystem/classes/HotelReservationSystemDb.php index aeee8e2d6..3f3a33b27 100644 --- a/modules/hotelreservationsystem/classes/HotelReservationSystemDb.php +++ b/modules/hotelreservationsystem/classes/HotelReservationSystemDb.php @@ -454,12 +454,11 @@ class HotelReservationSystemDb "CREATE TABLE IF NOT EXISTS `"._DB_PREFIX_."htl_header_image` ( `id_header_image` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, `name` VARCHAR(512) NOT NULL, - `tag_line_color` VARCHAR(7) NOT NULL DEFAULT '#ffffff', - `tag_line_font_size` TINYINT(3) UNSIGNED NOT NULL DEFAULT '16', - `tag_line_font_weight` VARCHAR(10) NOT NULL DEFAULT '400', + `description_color` VARCHAR(7) NOT NULL DEFAULT '#ffffff', + `description_font_size` TINYINT(3) UNSIGNED NOT NULL DEFAULT '16', + `description_font_weight` VARCHAR(10) NOT NULL DEFAULT '400', `position` INT(10) UNSIGNED NOT NULL DEFAULT '0', `active` TINYINT(1) UNSIGNED NOT NULL DEFAULT '1', - `show_hotel_chain_name` TINYINT(1) UNSIGNED NOT NULL DEFAULT '1', `date_add` DATETIME NOT NULL, `date_upd` DATETIME NOT NULL, PRIMARY KEY (`id_header_image`) @@ -467,7 +466,8 @@ class HotelReservationSystemDb "CREATE TABLE IF NOT EXISTS `"._DB_PREFIX_."htl_header_image_lang` ( `id_header_image` INT(10) UNSIGNED NOT NULL, `id_lang` INT(11) NOT NULL, - `tag_line` VARCHAR(512) NOT NULL DEFAULT '', + `title` VARCHAR(512) NOT NULL DEFAULT '', + `description` VARCHAR(512) NOT NULL DEFAULT '', PRIMARY KEY (`id_header_image`, `id_lang`) ) ENGINE="._MYSQL_ENGINE_." DEFAULT CHARSET=utf8;", diff --git a/modules/hotelreservationsystem/controllers/admin/AdminHotelHeaderImageController.php b/modules/hotelreservationsystem/controllers/admin/AdminHotelHeaderImageController.php index 01a815008..e80de7e96 100644 --- a/modules/hotelreservationsystem/controllers/admin/AdminHotelHeaderImageController.php +++ b/modules/hotelreservationsystem/controllers/admin/AdminHotelHeaderImageController.php @@ -70,7 +70,8 @@ class AdminHotelHeaderImageController extends ModuleAdminController $imageItems = HotelHeaderImage::getItems(null, $defaultLangId, true); $shopId = (int)$this->context->shop->id; foreach ($imageItems as &$item) { - $item['tag_lines_json'] = json_encode((object)$item['tag_lines']); + $item['titles_json'] = json_encode((object)$item['titles']); + $item['descriptions_json'] = json_encode((object)$item['descriptions']); $srcPath = _PS_IMG_DIR_.'hotel_header_media/'.$item['name']; $cacheName = 'htl_header_image_mini_'.(int)$item['id_header_image'].'_'.$shopId.'.jpg'; $item['thumb'] = ImageManager::thumbnail($srcPath, $cacheName, 45, 'jpg', false); @@ -130,7 +131,7 @@ class AdminHotelHeaderImageController extends ModuleAdminController 'showVideoUrlPreview' => $showVideoUrlPreview, 'config' => array( 'QLO_HEADER_MEDIA_TYPE' => $mediaType, - 'QLO_HEADER_SLIDER_NAV_TYPE' => (int)Tools::getValue('QLO_HEADER_SLIDER_NAV_TYPE', (int)(Configuration::get('QLO_HEADER_SLIDER_NAV_TYPE') ?: HotelHeaderImage::NAV_TYPE_DOTS)), + 'QLO_HEADER_SLIDER_NAV_TYPE' => (int)Tools::getValue('QLO_HEADER_SLIDER_NAV_TYPE', (int)(Configuration::get('QLO_HEADER_SLIDER_NAV_TYPE') ?: HotelHeaderImage::NAV_TYPE_ARROWS)), 'QLO_HEADER_SLIDER_AUTO_PLAY' => (int)Tools::getValue('QLO_HEADER_SLIDER_AUTO_PLAY', (int)Configuration::get('QLO_HEADER_SLIDER_AUTO_PLAY')), 'QLO_HEADER_SLIDER_INTERVAL' => Tools::getValue('QLO_HEADER_SLIDER_INTERVAL', (int)Configuration::get('QLO_HEADER_SLIDER_INTERVAL') ?: 5000), 'QLO_HEADER_SLIDER_ANIM_TYPE' => (int)Tools::getValue('QLO_HEADER_SLIDER_ANIM_TYPE', (int)(Configuration::get('QLO_HEADER_SLIDER_ANIM_TYPE') ?: HotelHeaderImage::ANIMATION_TYPE_SLIDE)), @@ -141,6 +142,8 @@ class AdminHotelHeaderImageController extends ModuleAdminController 'imgBaseUrl' => $this->context->link->getMediaLink(_PS_IMG_.'hotel_header_media/'), 'maxUpload' => Tools::formatBytes(Tools::getMaxUploadSize()), 'maxImageUpload' => Tools::formatBytes(Tools::getMaxUploadSize((int)Configuration::get('PS_LIMIT_UPLOAD_IMAGE_VALUE') * 1024 * 1024)), + 'descriptionLimit' => (int)Configuration::get('PS_SHORT_DESC_LIMIT') ?: Configuration::PS_SHORT_DESC_LIMIT, + 'titleLimit' => HotelHeaderImage::TITLE_LIMIT, ); return parent::renderView(); @@ -195,7 +198,7 @@ class AdminHotelHeaderImageController extends ModuleAdminController } $autoPlay = 0; $interval = 0; - $navType = HotelHeaderImage::NAV_TYPE_DOTS; + $navType = HotelHeaderImage::NAV_TYPE_ARROWS; $animType = HotelHeaderImage::ANIMATION_TYPE_SLIDE; $contentAlign = HotelHeaderImage::CONTENT_ALIGN_CENTER; @@ -210,9 +213,9 @@ class AdminHotelHeaderImageController extends ModuleAdminController $interval = (int)$interval; } - $navType = (int)Tools::getValue('QLO_HEADER_SLIDER_NAV_TYPE', HotelHeaderImage::NAV_TYPE_DOTS); + $navType = (int)Tools::getValue('QLO_HEADER_SLIDER_NAV_TYPE', HotelHeaderImage::NAV_TYPE_ARROWS); if (!in_array($navType, array(HotelHeaderImage::NAV_TYPE_DOTS, HotelHeaderImage::NAV_TYPE_ARROWS, HotelHeaderImage::NAV_TYPE_BOTH))) { - $navType = HotelHeaderImage::NAV_TYPE_DOTS; + $navType = HotelHeaderImage::NAV_TYPE_ARROWS; } $animType = (int)Tools::getValue('QLO_HEADER_SLIDER_ANIM_TYPE', HotelHeaderImage::ANIMATION_TYPE_SLIDE); if (!in_array($animType, array(HotelHeaderImage::ANIMATION_TYPE_SLIDE, HotelHeaderImage::ANIMATION_TYPE_FADE, HotelHeaderImage::ANIMATION_TYPE_ZOOM, HotelHeaderImage::ANIMATION_TYPE_BLUR))) { @@ -231,7 +234,7 @@ class AdminHotelHeaderImageController extends ModuleAdminController array_shift($imagesToDrop); } if ($imagesToDrop && Tools::getValue('confirm_delete_images') !== '1') { - $this->errors[] = $this->l('Switching to Video will delete all images except the first one. Please confirm this action.'); + $this->errors[] = $this->l('Switching to Video will delete all images except the one in the first position. Please confirm this action.'); return; } @@ -254,9 +257,9 @@ class AdminHotelHeaderImageController extends ModuleAdminController Configuration::updateValue('QLO_HEADER_CONTENT_ALIGN', $contentAlign); Configuration::updateValue('QLO_HEADER_SLIDER_NAV_TYPE', $navType); Configuration::updateValue('QLO_HEADER_SLIDER_AUTO_PLAY', $autoPlay); + Configuration::updateValue('QLO_HEADER_SLIDER_ANIM_TYPE', $animType); if ($autoPlay) { Configuration::updateValue('QLO_HEADER_SLIDER_INTERVAL', $interval); - Configuration::updateValue('QLO_HEADER_SLIDER_ANIM_TYPE', $animType); } } Tools::redirectAdmin(self::$currentIndex.'&conf=4&token='.$this->token); @@ -360,33 +363,54 @@ class AdminHotelHeaderImageController extends ModuleAdminController $this->ajaxDie(json_encode($response)); } - $tagLineByLang = array(); + $titleByLang = array(); + $descriptionByLang = array(); foreach (Language::getLanguages(false) as $lang) { - $tagLineByLang[$lang['id_lang']] = trim(Tools::getValue('tag_line_'.$lang['id_lang'], '')); + $titleByLang[$lang['id_lang']] = trim(Tools::getValue('title_'.$lang['id_lang'], '')); + $descriptionByLang[$lang['id_lang']] = trim(Tools::getValue('description_'.$lang['id_lang'], '')); } - foreach ($tagLineByLang as $tagLineValue) { - if (!Validate::isGenericName($tagLineValue)) { + foreach (array_merge($titleByLang, $descriptionByLang) as $textValue) { + if (!Validate::isGenericName($textValue)) { if (file_exists($destPath)) { unlink($destPath); } - $response['errors'][] = $this->l('Invalid tag line. Characters < > = { } are not allowed.'); + $response['errors'][] = $this->l('Invalid title or description. Characters < > = { } are not allowed.'); + $this->ajaxDie(json_encode($response)); + } + } + foreach ($titleByLang as $titleValue) { + if (Tools::strlen($titleValue) > HotelHeaderImage::TITLE_LIMIT) { + if (file_exists($destPath)) { + unlink($destPath); + } + $response['errors'][] = sprintf($this->l('Title is too long: %d chars max.'), HotelHeaderImage::TITLE_LIMIT); + $this->ajaxDie(json_encode($response)); + } + } + $descriptionLimit = (int)Configuration::get('PS_SHORT_DESC_LIMIT') ?: Configuration::PS_SHORT_DESC_LIMIT; + foreach ($descriptionByLang as $descriptionValue) { + if (Tools::strlen($descriptionValue) > $descriptionLimit) { + if (file_exists($destPath)) { + unlink($destPath); + } + $response['errors'][] = sprintf($this->l('Description is too long: %d chars max.'), $descriptionLimit); $this->ajaxDie(json_encode($response)); } } - $tagLineColor = Tools::getValue('tag_line_color', '#ffffff'); - if (!preg_match('/^#[0-9a-fA-F]{6}$/', $tagLineColor)) { - $response['errors'][] = $this->l('Invalid tag line color. Use a 6-digit hex value (e.g. #ffffff).'); + $descriptionColor = Tools::getValue('description_color', '#ffffff'); + if (!preg_match('/^#[0-9a-fA-F]{6}$/', $descriptionColor)) { + $response['errors'][] = $this->l('Invalid description color. Use a 6-digit hex value (e.g. #ffffff).'); $this->ajaxDie(json_encode($response)); } - $tagLineFontSize = (int)Tools::getValue('tag_line_font_size', 16); - if ($tagLineFontSize < 8 || $tagLineFontSize > 72) { - $response['errors'][] = $this->l('Tag line font size must be between 8 and 72 pixels.'); + $descriptionFontSize = (int)Tools::getValue('description_font_size', 16); + if ($descriptionFontSize < 8 || $descriptionFontSize > 72) { + $response['errors'][] = $this->l('Description font size must be between 8 and 72 pixels.'); $this->ajaxDie(json_encode($response)); } - $tagLineFontWeight = Tools::getValue('tag_line_font_weight', '400'); - if (!in_array($tagLineFontWeight, array('300', '400', '600', '700'))) { - $response['errors'][] = $this->l('Invalid tag line font weight.'); + $descriptionFontWeight = Tools::getValue('description_font_weight', '400'); + if (!in_array($descriptionFontWeight, array('400', '700'))) { + $response['errors'][] = $this->l('Invalid description font weight.'); $this->ajaxDie(json_encode($response)); } @@ -394,11 +418,11 @@ class AdminHotelHeaderImageController extends ModuleAdminController $objImage->name = $uniqueName; $objImage->position = $objImage->getHigherPosition(); $objImage->active = (int)(bool)Tools::getValue('active', 1); - $objImage->show_hotel_chain_name = (int)(bool)Tools::getValue('show_hotel_chain_name', 1); - $objImage->tag_line = $tagLineByLang; - $objImage->tag_line_color = $tagLineColor; - $objImage->tag_line_font_size = $tagLineFontSize; - $objImage->tag_line_font_weight = $tagLineFontWeight; + $objImage->title = $titleByLang; + $objImage->description = $descriptionByLang; + $objImage->description_color = $descriptionColor; + $objImage->description_font_size = $descriptionFontSize; + $objImage->description_font_weight = $descriptionFontWeight; if (!$objImage->save()) { if (file_exists($destPath)) { @@ -417,13 +441,14 @@ class AdminHotelHeaderImageController extends ModuleAdminController 'id_header_image' => (int)$objImage->id, 'name' => $uniqueName, 'thumb' => $thumb, - 'tag_line' => isset($tagLineByLang[$defaultLangId]) ? $tagLineByLang[$defaultLangId] : '', - 'tag_lines_json' => json_encode((object)$tagLineByLang), - 'tag_line_color' => $tagLineColor, - 'tag_line_font_size' => $tagLineFontSize, - 'tag_line_font_weight' => $tagLineFontWeight, + 'title' => isset($titleByLang[$defaultLangId]) ? $titleByLang[$defaultLangId] : '', + 'titles_json' => json_encode((object)$titleByLang), + 'description' => isset($descriptionByLang[$defaultLangId]) ? $descriptionByLang[$defaultLangId] : '', + 'descriptions_json' => json_encode((object)$descriptionByLang), + 'description_color' => $descriptionColor, + 'description_font_size' => $descriptionFontSize, + 'description_font_weight' => $descriptionFontWeight, 'active' => (int)$objImage->active, - 'show_hotel_chain_name' => (int)$objImage->show_hotel_chain_name, ), 'position' => count(HotelHeaderImage::getItems(null)), 'current' => self::$currentIndex, @@ -454,9 +479,28 @@ class AdminHotelHeaderImageController extends ModuleAdminController $this->ajaxDie(json_encode($response)); } - $tagLineByLang = array(); + $titleByLang = array(); + $descriptionByLang = array(); foreach (Language::getLanguages(false) as $lang) { - $tagLineByLang[$lang['id_lang']] = trim(Tools::getValue('tag_line_'.$lang['id_lang'], '')); + $titleByLang[$lang['id_lang']] = trim(Tools::getValue('title_'.$lang['id_lang'], '')); + $descriptionByLang[$lang['id_lang']] = trim(Tools::getValue('description_'.$lang['id_lang'], '')); + } + foreach ($titleByLang as $titleValue) { + if (!Validate::isGenericName($titleValue)) { + $response['errors'][] = $this->l('Invalid title. Characters < > = { } are not allowed.'); + $this->ajaxDie(json_encode($response)); + } + if (Tools::strlen($titleValue) > HotelHeaderImage::TITLE_LIMIT) { + $response['errors'][] = sprintf($this->l('Title is too long: %d chars max.'), HotelHeaderImage::TITLE_LIMIT); + $this->ajaxDie(json_encode($response)); + } + } + $descriptionLimit = (int)Configuration::get('PS_SHORT_DESC_LIMIT') ?: Configuration::PS_SHORT_DESC_LIMIT; + foreach ($descriptionByLang as $descriptionValue) { + if (Tools::strlen($descriptionValue) > $descriptionLimit) { + $response['errors'][] = sprintf($this->l('Description is too long: %d chars max.'), $descriptionLimit); + $this->ajaxDie(json_encode($response)); + } } $activeVal = Tools::getValue('active'); @@ -472,24 +516,19 @@ class AdminHotelHeaderImageController extends ModuleAdminController $objImage->active = $newActive; } - $showHotelChainNameVal = Tools::getValue('show_hotel_chain_name'); - if ($showHotelChainNameVal !== false) { - $objImage->show_hotel_chain_name = (int)(bool)$showHotelChainNameVal; - } - - $tagLineColor = Tools::getValue('tag_line_color', $objImage->tag_line_color); - if (!preg_match('/^#[0-9a-fA-F]{6}$/', $tagLineColor)) { - $response['errors'][] = $this->l('Invalid tag line color. Use a 6-digit hex value (e.g. #ffffff).'); + $descriptionColor = Tools::getValue('description_color', $objImage->description_color); + if (!preg_match('/^#[0-9a-fA-F]{6}$/', $descriptionColor)) { + $response['errors'][] = $this->l('Invalid description color. Use a 6-digit hex value (e.g. #ffffff).'); $this->ajaxDie(json_encode($response)); } - $tagLineFontSize = (int)Tools::getValue('tag_line_font_size', $objImage->tag_line_font_size); - if ($tagLineFontSize < 8 || $tagLineFontSize > 72) { - $response['errors'][] = $this->l('Tag line font size must be between 8 and 72 pixels.'); + $descriptionFontSize = (int)Tools::getValue('description_font_size', $objImage->description_font_size); + if ($descriptionFontSize < 8 || $descriptionFontSize > 72) { + $response['errors'][] = $this->l('Description font size must be between 8 and 72 pixels.'); $this->ajaxDie(json_encode($response)); } - $tagLineFontWeight = Tools::getValue('tag_line_font_weight', $objImage->tag_line_font_weight); - if (!in_array($tagLineFontWeight, array('300', '400', '600', '700'))) { - $response['errors'][] = $this->l('Invalid tag line font weight.'); + $descriptionFontWeight = Tools::getValue('description_font_weight', $objImage->description_font_weight); + if (!in_array($descriptionFontWeight, array('400', '700'))) { + $response['errors'][] = $this->l('Invalid description font weight.'); $this->ajaxDie(json_encode($response)); } @@ -524,10 +563,11 @@ class AdminHotelHeaderImageController extends ModuleAdminController } $oldImageName = $objImage->name; - $objImage->tag_line = $tagLineByLang; - $objImage->tag_line_color = $tagLineColor; - $objImage->tag_line_font_size = $tagLineFontSize; - $objImage->tag_line_font_weight = $tagLineFontWeight; + $objImage->title = $titleByLang; + $objImage->description = $descriptionByLang; + $objImage->description_color = $descriptionColor; + $objImage->description_font_size = $descriptionFontSize; + $objImage->description_font_weight = $descriptionFontWeight; if ($newImageName) { $objImage->name = $newImageName; } @@ -549,13 +589,14 @@ class AdminHotelHeaderImageController extends ModuleAdminController $defaultLangId = (int)Configuration::get('PS_LANG_DEFAULT'); $response['success'] = true; $response['active'] = (int)$objImage->active; - $response['show_hotel_chain_name'] = (int)$objImage->show_hotel_chain_name; $response['confirmations'] = $this->l('Image updated successfully.'); - $response['tag_line'] = isset($tagLineByLang[$defaultLangId]) ? $tagLineByLang[$defaultLangId] : ''; - $response['tag_lines_json'] = json_encode((object)$tagLineByLang); - $response['tag_line_color'] = $tagLineColor; - $response['tag_line_font_size'] = $tagLineFontSize; - $response['tag_line_font_weight'] = $tagLineFontWeight; + $response['title'] = isset($titleByLang[$defaultLangId]) ? $titleByLang[$defaultLangId] : ''; + $response['titles_json'] = json_encode((object)$titleByLang); + $response['description'] = isset($descriptionByLang[$defaultLangId]) ? $descriptionByLang[$defaultLangId] : ''; + $response['descriptions_json'] = json_encode((object)$descriptionByLang); + $response['description_color'] = $descriptionColor; + $response['description_font_size'] = $descriptionFontSize; + $response['description_font_weight'] = $descriptionFontWeight; if ($newImageName) { $shopId = (int)$this->context->shop->id; @@ -653,34 +694,6 @@ class AdminHotelHeaderImageController extends ModuleAdminController $this->ajaxDie(json_encode($response)); } - public function ajaxProcessToggleImageHotelName() - { - $response = array('errors' => array(), 'success' => false); - $id = (int)Tools::getValue('id_header_image'); - $showHotelChainName = (int)(bool)Tools::getValue('show_hotel_chain_name'); - - if (!$id) { - $response['errors'][] = $this->l('Invalid item ID.'); - $this->ajaxDie(json_encode($response)); - } - - $objImage = new HotelHeaderImage($id); - if (!Validate::isLoadedObject($objImage)) { - $response['errors'][] = $this->l('Image not found.'); - $this->ajaxDie(json_encode($response)); - } - - $objImage->show_hotel_chain_name = $showHotelChainName; - if (!$objImage->save()) { - $response['errors'][] = $this->l('Unable to update hotel name status.'); - $this->ajaxDie(json_encode($response)); - } - - $response['success'] = true; - $response['confirmations'] = $this->l('The status has been successfully updated.'); - $this->ajaxDie(json_encode($response)); - } - public function ajaxProcessSaveImagePositions() { $ids = Tools::getValue('image_ids', array()); @@ -728,41 +741,58 @@ class AdminHotelHeaderImageController extends ModuleAdminController } } - $showHotelChainNameVal = Tools::getValue('show_hotel_chain_name', ''); - if ($showHotelChainNameVal !== '' && !in_array($showHotelChainNameVal, array('0', '1'))) { - $response['errors'][] = $this->l('Invalid hotel name status selected.'); - $this->ajaxDie(json_encode($response)); - } - - $updateTagline = (bool)Tools::getValue('update_tagline', false); - $tagLineByLang = array(); - if ($updateTagline) { + $updateTitle = (bool)Tools::getValue('update_title', false); + $titleByLang = array(); + if ($updateTitle) { foreach (Language::getLanguages(false) as $lang) { - $tagLineByLang[$lang['id_lang']] = trim(Tools::getValue('tag_line_'.$lang['id_lang'], '')); + $titleByLang[$lang['id_lang']] = trim(Tools::getValue('title_'.$lang['id_lang'], '')); } - foreach ($tagLineByLang as $tagLineValue) { - if (!Validate::isGenericName($tagLineValue)) { - $response['errors'][] = $this->l('Invalid tag line. Characters < > = { } are not allowed.'); + foreach ($titleByLang as $titleValue) { + if (!Validate::isGenericName($titleValue)) { + $response['errors'][] = $this->l('Invalid title. Characters < > = { } are not allowed.'); + $this->ajaxDie(json_encode($response)); + } + if (Tools::strlen($titleValue) > HotelHeaderImage::TITLE_LIMIT) { + $response['errors'][] = sprintf($this->l('Title is too long: %d chars max.'), HotelHeaderImage::TITLE_LIMIT); $this->ajaxDie(json_encode($response)); } } } - $tagLineColor = trim(Tools::getValue('tag_line_color', '')); - if ($tagLineColor !== '' && !preg_match('/^#[0-9a-fA-F]{6}$/', $tagLineColor)) { - $response['errors'][] = $this->l('Invalid tag line color. Use a 6-digit hex value (e.g. #ffffff).'); + $updateDescription = (bool)Tools::getValue('update_description', false); + $descriptionByLang = array(); + if ($updateDescription) { + $descriptionLimit = (int)Configuration::get('PS_SHORT_DESC_LIMIT') ?: Configuration::PS_SHORT_DESC_LIMIT; + foreach (Language::getLanguages(false) as $lang) { + $descriptionByLang[$lang['id_lang']] = trim(Tools::getValue('description_'.$lang['id_lang'], '')); + } + foreach ($descriptionByLang as $descriptionValue) { + if (!Validate::isGenericName($descriptionValue)) { + $response['errors'][] = $this->l('Invalid description. Characters < > = { } are not allowed.'); + $this->ajaxDie(json_encode($response)); + } + if (Tools::strlen($descriptionValue) > $descriptionLimit) { + $response['errors'][] = sprintf($this->l('Description is too long: %d chars max.'), $descriptionLimit); + $this->ajaxDie(json_encode($response)); + } + } + } + + $descriptionColor = trim(Tools::getValue('description_color', '')); + if ($descriptionColor !== '' && !preg_match('/^#[0-9a-fA-F]{6}$/', $descriptionColor)) { + $response['errors'][] = $this->l('Invalid description color. Use a 6-digit hex value (e.g. #ffffff).'); $this->ajaxDie(json_encode($response)); } - $tagLineFontSize = (int)Tools::getValue('tag_line_font_size', 0); - if ($tagLineFontSize !== 0 && ($tagLineFontSize < 8 || $tagLineFontSize > 72)) { - $response['errors'][] = $this->l('Tag line font size must be between 8 and 72 pixels.'); + $descriptionFontSize = (int)Tools::getValue('description_font_size', 0); + if ($descriptionFontSize !== 0 && ($descriptionFontSize < 8 || $descriptionFontSize > 72)) { + $response['errors'][] = $this->l('Description font size must be between 8 and 72 pixels.'); $this->ajaxDie(json_encode($response)); } - $tagLineFontWeight = Tools::getValue('tag_line_font_weight', ''); - if ($tagLineFontWeight !== '' && !in_array($tagLineFontWeight, array('300', '400', '600', '700'))) { - $response['errors'][] = $this->l('Invalid tag line font weight.'); + $descriptionFontWeight = Tools::getValue('description_font_weight', ''); + if ($descriptionFontWeight !== '' && !in_array($descriptionFontWeight, array('400', '700'))) { + $response['errors'][] = $this->l('Invalid description font weight.'); $this->ajaxDie(json_encode($response)); } @@ -777,33 +807,35 @@ class AdminHotelHeaderImageController extends ModuleAdminController if ($activeVal !== '') { $objImage->active = (int)$activeVal; } - if ($showHotelChainNameVal !== '') { - $objImage->show_hotel_chain_name = (int)$showHotelChainNameVal; + if ($updateTitle) { + $objImage->title = $titleByLang; } - if ($updateTagline) { - $objImage->tag_line = $tagLineByLang; + if ($updateDescription) { + $objImage->description = $descriptionByLang; } - if ($tagLineColor !== '') { - $objImage->tag_line_color = $tagLineColor; + if ($descriptionColor !== '') { + $objImage->description_color = $descriptionColor; } - if ($tagLineFontSize !== 0) { - $objImage->tag_line_font_size = $tagLineFontSize; + if ($descriptionFontSize !== 0) { + $objImage->description_font_size = $descriptionFontSize; } - if ($tagLineFontWeight !== '') { - $objImage->tag_line_font_weight = $tagLineFontWeight; + if ($descriptionFontWeight !== '') { + $objImage->description_font_weight = $descriptionFontWeight; } $objImage->save(); - $currentTagLines = is_array($objImage->tag_line) ? $objImage->tag_line : array(); + $currentTitles = is_array($objImage->title) ? $objImage->title : array(); + $currentDescriptions = is_array($objImage->description) ? $objImage->description : array(); $updatedRows[] = array( 'id' => (int)$objImage->id, 'active' => (int)$objImage->active, - 'show_hotel_chain_name' => (int)$objImage->show_hotel_chain_name, - 'tag_line' => isset($currentTagLines[$defaultLangId]) ? $currentTagLines[$defaultLangId] : '', - 'tag_lines_json' => json_encode((object)$currentTagLines), - 'tag_line_color' => $objImage->tag_line_color, - 'tag_line_font_size' => $objImage->tag_line_font_size, - 'tag_line_font_weight' => $objImage->tag_line_font_weight, + 'title' => isset($currentTitles[$defaultLangId]) ? $currentTitles[$defaultLangId] : '', + 'titles_json' => json_encode((object)$currentTitles), + 'description' => isset($currentDescriptions[$defaultLangId]) ? $currentDescriptions[$defaultLangId] : '', + 'descriptions_json' => json_encode((object)$currentDescriptions), + 'description_color' => $objImage->description_color, + 'description_font_size' => $objImage->description_font_size, + 'description_font_weight' => $objImage->description_font_weight, ); } diff --git a/modules/hotelreservationsystem/hotelreservationsystem.php b/modules/hotelreservationsystem/hotelreservationsystem.php index a416bc000..3cf4581f2 100644 --- a/modules/hotelreservationsystem/hotelreservationsystem.php +++ b/modules/hotelreservationsystem/hotelreservationsystem.php @@ -102,7 +102,7 @@ class HotelReservationSystem extends Module 'QLO_HEADER_ANIM_TYPE_SLIDE' => HotelHeaderImage::ANIMATION_TYPE_SLIDE, 'headerMediaItems' => $headerMediaItems, 'headerSliderConfig' => array( - 'nav_type' => (int)(Configuration::get('QLO_HEADER_SLIDER_NAV_TYPE') ?: HotelHeaderImage::NAV_TYPE_DOTS), + 'nav_type' => (int)(Configuration::get('QLO_HEADER_SLIDER_NAV_TYPE') ?: HotelHeaderImage::NAV_TYPE_ARROWS), 'auto_play' => (int)Configuration::get('QLO_HEADER_SLIDER_AUTO_PLAY'), 'interval' => (int)Configuration::get('QLO_HEADER_SLIDER_INTERVAL'), 'anim_type' => (int)(Configuration::get('QLO_HEADER_SLIDER_ANIM_TYPE') ?: HotelHeaderImage::ANIMATION_TYPE_SLIDE), @@ -301,14 +301,12 @@ class HotelReservationSystem extends Module if (Tools::getValue('controller') == 'index') { $headerMediaItems = $this->context->smarty->getTemplateVars('headerMediaItems'); $firstItem = ($headerMediaItems && !empty($headerMediaItems[0])) ? $headerMediaItems[0] : array(); - $tagLine = !empty($firstItem['tag_line']) ? $firstItem['tag_line'] : ''; $this->context->smarty->assign(array( - 'WK_HTL_CHAIN_NAME' => Configuration::get('WK_HTL_CHAIN_NAME', $this->context->language->id), - 'wkHeaderMediaTagLine' => $tagLine, - 'wkTagLineColor' => !empty($firstItem['tag_line_color']) ? $firstItem['tag_line_color'] : '#ffffff', - 'wkTagLineFontSize' => !empty($firstItem['tag_line_font_size']) ? (int)$firstItem['tag_line_font_size'] : 16, - 'wkTagLineFontWeight' => !empty($firstItem['tag_line_font_weight']) ? $firstItem['tag_line_font_weight'] : '400', - 'QLO_HOTEL_NAME_ENABLE' => !empty($firstItem['show_hotel_chain_name']), + 'wkHeaderMediaTitle' => !empty($firstItem['title']) ? $firstItem['title'] : '', + 'wkHeaderMediaDescription' => !empty($firstItem['description']) ? $firstItem['description'] : '', + 'wkDescriptionColor' => !empty($firstItem['description_color']) ? $firstItem['description_color'] : '#ffffff', + 'wkDescriptionFontSize' => !empty($firstItem['description_font_size']) ? (int)$firstItem['description_font_size'] : 16, + 'wkDescriptionFontWeight' => !empty($firstItem['description_font_weight']) ? $firstItem['description_font_weight'] : '400', 'wkHeaderContentAlign' => (int)(Configuration::get('QLO_HEADER_CONTENT_ALIGN') ?: HotelHeaderImage::CONTENT_ALIGN_CENTER), 'QLO_HEADER_MEDIA_TYPE' => (int)(Configuration::get('QLO_HEADER_MEDIA_TYPE') ?: HotelHeaderImage::MEDIA_TYPE_IMAGE), 'QLO_HEADER_MEDIA_TYPE_IMAGE' => HotelHeaderImage::MEDIA_TYPE_IMAGE, diff --git a/modules/hotelreservationsystem/views/css/qhrs_header_media.css b/modules/hotelreservationsystem/views/css/qhrs_header_media.css index e32734df0..210a4d8d1 100644 --- a/modules/hotelreservationsystem/views/css/qhrs_header_media.css +++ b/modules/hotelreservationsystem/views/css/qhrs_header_media.css @@ -123,15 +123,15 @@ @keyframes wkSlideOut { from { opacity:1; transform:translateX(0); } to { opacity:0; transform:translateX(-60px); } } @keyframes wkSlideIn { from { opacity:0; transform:translateX(60px); } to { opacity:1; transform:translateX(0); } } -/* Tag-line slide animation on slide change */ -.js-header-tagline { +/* Description slide animation on slide change */ +.js-header-description { transition: opacity 0.4s ease, transform 0.4s ease; } -.js-header-tagline.wk-tagline-out { +.js-header-description.wk-tagline-out { opacity: 0; transform: translateY(-10px); } -.js-header-tagline.wk-tagline-in { +.js-header-description.wk-tagline-in { opacity: 1; transform: translateY(0); } diff --git a/modules/hotelreservationsystem/views/js/admin/qhrs_hotel_header_image_admin.js b/modules/hotelreservationsystem/views/js/admin/qhrs_hotel_header_image_admin.js index cd8529abd..3f65caad4 100644 --- a/modules/hotelreservationsystem/views/js/admin/qhrs_hotel_header_image_admin.js +++ b/modules/hotelreservationsystem/views/js/admin/qhrs_hotel_header_image_admin.js @@ -80,9 +80,9 @@ $(document).ready(function () { $(document).on('change', 'input[name="QLO_HEADER_SLIDER_AUTO_PLAY"]', function () { if ($(this).val() === '1') { - $('#qlo-slide-interval-group, #qlo-slide-anim-group').show(200); + $('#qlo-slide-interval-group').show(200); } else { - $('#qlo-slide-interval-group, #qlo-slide-anim-group').hide(200); + $('#qlo-slide-interval-group').hide(200); } }); @@ -193,14 +193,14 @@ $(document).ready(function () { $(document).on('click', '.qlo-edit-img', function () { var $row = $(this).closest('tr.qlo-img-row'); var id = parseInt($row.data('id'), 10); - var tagLines = $row.data('tag-lines') || {}; + var titles = $row.data('titles') || {}; + var descriptions = $row.data('descriptions') || {}; var isActive = $row.find('.js-toggle-active').hasClass('action-enabled'); - var showHotelName = $row.find('.js-toggle-hotel-name').hasClass('action-enabled'); var thumbSrc = $row.attr('data-thumb-src') || ''; - var tlColor = $row.attr('data-tag-line-color') || '#ffffff'; - var tlFontSize = parseInt($row.attr('data-tag-line-font-size') || 16, 10); - var tlFontWeight = $row.attr('data-tag-line-font-weight') || '400'; - openImgForm('edit', id, tagLines, isActive, thumbSrc, tlColor, tlFontSize, tlFontWeight, showHotelName); + var descColor = $row.attr('data-description-color') || '#ffffff'; + var descFontSize = parseInt($row.attr('data-description-font-size') || 16, 10); + var descFontWeight = $row.attr('data-description-font-weight') || '400'; + openImgForm('edit', id, titles, descriptions, isActive, thumbSrc, descColor, descFontSize, descFontWeight); }); function updateBulkUpdateTriggerState() { @@ -215,40 +215,56 @@ $(document).ready(function () { return false; } $('#qlo-bulk-active').val(''); - $('#qlo-bulk-hotelname').val(''); - $('#qlo_bulk_update_tagline_off').prop('checked', true); - $('#qlo-bulk-tagline-fields-wrap').hide(); - $('.qlo-bulk-tagline-field').val(''); - $('#qlo-bulk-tl-color').val('').trigger('keyup'); - $('#qlo-bulk-tl-font-size').val(0); - $('#qlo-bulk-tl-font-weight').val(''); + $('#qlo_bulk_update_title_off').prop('checked', true); + $('#qlo-bulk-title-fields-wrap').hide(); + $('.qlo-bulk-title-field').val(''); + $('#qlo_bulk_update_description_off').prop('checked', true); + $('#qlo-bulk-description-fields-wrap').hide(); + $('.qlo-bulk-description-field').val(''); + $('#qlo-bulk-desc-color').val('').trigger('keyup'); + $('#qlo-bulk-desc-font-size').val(0); + $('#qlo-bulk-desc-font-weight').val(''); hideOtherLanguage(qloHmDefaultLangId); }); - $(document).on('change', 'input[name="qlo_bulk_update_tagline"]', function () { + $(document).on('change', 'input[name="qlo_bulk_update_title"]', function () { if ($(this).val() === '1') { - $('#qlo-bulk-tagline-fields-wrap').show(200); + $('#qlo-bulk-title-fields-wrap').show(200); } else { - $('#qlo-bulk-tagline-fields-wrap').hide(200); + $('#qlo-bulk-title-fields-wrap').hide(200); + } + }); + + $(document).on('change', 'input[name="qlo_bulk_update_description"]', function () { + if ($(this).val() === '1') { + $('#qlo-bulk-description-fields-wrap').show(200); + } else { + $('#qlo-bulk-description-fields-wrap').hide(200); } }); $('#qlo-bulk-update-apply').on('click', function () { var ids = $('.qlo-img-checkbox:checked').map(function () { return $(this).val(); }).get(); - var updateTagline = $('input[name="qlo_bulk_update_tagline"]:checked').val() === '1'; + var updateTitle = $('input[name="qlo_bulk_update_title"]:checked').val() === '1'; + var updateDescription = $('input[name="qlo_bulk_update_description"]:checked').val() === '1'; var postData = { ajax: 1, action: 'bulkUpdateImages', token: qloHmToken, id_header_image: ids, active: $('#qlo-bulk-active').val(), - show_hotel_chain_name: $('#qlo-bulk-hotelname').val(), - update_tagline: updateTagline ? 1 : 0, - tag_line_color: $.trim($('#qlo-bulk-tl-color').val()), - tag_line_font_size: $('#qlo-bulk-tl-font-size').val(), - tag_line_font_weight: $('#qlo-bulk-tl-font-weight').val() + update_title: updateTitle ? 1 : 0, + update_description: updateDescription ? 1 : 0, + description_color: $.trim($('#qlo-bulk-desc-color').val()), + description_font_size: $('#qlo-bulk-desc-font-size').val(), + description_font_weight: $('#qlo-bulk-desc-font-weight').val() }; - if (updateTagline) { - $('.qlo-bulk-tagline-field').each(function () { - postData['tag_line_' + $(this).data('lang')] = $.trim($(this).val()); + if (updateTitle) { + $('.qlo-bulk-title-field').each(function () { + postData['title_' + $(this).data('lang')] = $.trim($(this).val()); + }); + } + if (updateDescription) { + $('.qlo-bulk-description-field').each(function () { + postData['description_' + $(this).data('lang')] = $.trim($(this).val()); }); } @@ -283,7 +299,7 @@ $(document).ready(function () { showErrorMessage((typeof qloHmI18n !== 'undefined') ? qloHmI18n.noFileSelected : 'Please select at least one image file.'); return; } - uploadImagesWithTagLine(files); + uploadImagesWithDetails(files); }); $('#qlo-img-form-save-btn').on('click', function () { @@ -318,31 +334,6 @@ $(document).ready(function () { }).always(function () { $('#page-loader').hide(); }); }); - $(document).on('click', '.qlo-img-row .js-toggle-hotel-name', function (e) { - e.preventDefault(); - var $link = $(this); - var $row = $link.closest('tr.qlo-img-row'); - var id = parseInt($row.data('id'), 10); - var showHotelName = $link.hasClass('action-enabled') ? 0 : 1; - $('#page-loader').show(); - $.ajax({ - url: qloHmCurrentIndex, - type: 'POST', - data: { ajax: 1, action: 'toggle_image_hotel_name', id_header_image: id, show_hotel_chain_name: showHotelName, token: qloHmToken }, - success: function (raw) { - var data = safeParseJsonHm(raw); - if (data && data.success) { - $link.toggleClass('action-enabled action-disabled'); - $link.find('i').toggleClass('hidden'); - showSuccessMessage(data.confirmations); - } else { - showErrorMessage(data ? data.errors.join('
    ') : qloHmI18n.updateFailed); - } - }, - error: function () { showErrorMessage(qloHmI18n.requestFailed); } - }).always(function () { $('#page-loader').hide(); }); - }); - $(document).on('click', '.qlo-delete-img', function () { var id = parseInt($(this).data('id'), 10); $('#page-loader').show(); @@ -412,7 +403,7 @@ $(document).ready(function () { }); } - function openImgForm(mode, id, tagLines, isActive, imgUrl, tlColor, tlFontSize, tlFontWeight, showHotelName) { + function openImgForm(mode, id, titles, descriptions, isActive, imgUrl, descColor, descFontSize, descFontWeight) { $('#qlo-img-form-id').val(id || ''); $('#qlo-form-upload-progress').hide(); @@ -423,7 +414,6 @@ $(document).ready(function () { $('#qlo-img-edit-footer').show(); $('#qlo-img-form-file-group').hide(); $('#qlo-img-form-add-active-group').hide(); - $('#qlo-img-form-add-hotelname-group').hide(); $('#qlo-img-form-edit-group').show(); $('#qlo-img-edit-file-group').show(); var editFileInput = document.getElementById('qlo-img-edit-file'); @@ -432,21 +422,20 @@ $(document).ready(function () { } $('#qlo-img-edit-file-name').val(''); revokeEditImagePreview(); - $('#qlo-img-form-edit-hotelname-group').show(); if (isActive) { $('#qlo_img_active_edit_on').prop('checked', true); } else { $('#qlo_img_active_edit_off').prop('checked', true); } - if (showHotelName) { - $('#qlo_img_hotelname_edit_on').prop('checked', true); - } else { - $('#qlo_img_hotelname_edit_off').prop('checked', true); - } - tagLines = tagLines || {}; - $('.qlo-form-tagline-field').each(function () { + titles = titles || {}; + descriptions = descriptions || {}; + $('.qlo-form-title-field').each(function () { var langId = $(this).data('lang'); - $(this).val(tagLines[langId] || ''); + $(this).val(titles[langId] || ''); + }); + $('.qlo-form-description-field').each(function () { + var langId = $(this).data('lang'); + $(this).val(descriptions[langId] || ''); }); if (imgUrl) { $('#qlo-img-edit-thumb').attr('src', imgUrl); @@ -454,10 +443,10 @@ $(document).ready(function () { } else { $('#qlo-img-edit-preview-group').hide(); } - $('#qlo-img-tl-color').val(tlColor || '#ffffff'); - $('#qlo-img-tl-font-size').val(tlFontSize || 16); - var fw = tlFontWeight || '400'; - $('#qlo-img-tl-font-weight').val(fw); + $('#qlo-img-desc-color').val(descColor || '#ffffff'); + $('#qlo-img-desc-font-size').val(descFontSize || 16); + var fw = descFontWeight || '400'; + $('#qlo-img-desc-font-weight').val(fw); } else { $('#qlo-img-modal-title-edit').hide(); $('#qlo-img-modal-title-add').show(); @@ -465,9 +454,7 @@ $(document).ready(function () { $('#qlo-img-add-footer').show(); $('#qlo-img-form-file-group').show(); $('#qlo-img-form-add-active-group').show(); - $('#qlo-img-form-add-hotelname-group').show(); $('#qlo-img-form-edit-group').hide(); - $('#qlo-img-form-edit-hotelname-group').hide(); $('#qlo-img-edit-preview-group').hide(); $('#qlo-img-edit-file-group').hide(); var fileInput = document.getElementById('qlo-img-form-file'); @@ -476,14 +463,14 @@ $(document).ready(function () { } $('#qlo-img-form-file-name').val(''); $('#qlo_img_active_add_on').prop('checked', true); - $('#qlo_img_hotelname_add_on').prop('checked', true); - $('.qlo-form-tagline-field').val(''); - $('#qlo-img-tl-color').val('#ffffff'); - $('#qlo-img-tl-font-size').val(16); - $('#qlo-img-tl-font-weight').val('400'); + $('.qlo-form-title-field').val(''); + $('.qlo-form-description-field').val(''); + $('#qlo-img-desc-color').val('#ffffff'); + $('#qlo-img-desc-font-size').val(16); + $('#qlo-img-desc-font-weight').val('400'); } - $('#qlo-img-tl-color').trigger('keyup'); + $('#qlo-img-desc-color').trigger('keyup'); hideOtherLanguage(id_language); @@ -495,21 +482,29 @@ $(document).ready(function () { revokeEditImagePreview(); } - function getFormTagLines() { - var tagLines = {}; - $('.qlo-form-tagline-field').each(function () { - tagLines[$(this).data('lang')] = $.trim($(this).val()); + function getFormTitles() { + var titles = {}; + $('.qlo-form-title-field').each(function () { + titles[$(this).data('lang')] = $.trim($(this).val()); }); - return tagLines; + return titles; + } + + function getFormDescriptions() { + var descriptions = {}; + $('.qlo-form-description-field').each(function () { + descriptions[$(this).data('lang')] = $.trim($(this).val()); + }); + return descriptions; } function saveEditedImage(id) { - var tagLines = getFormTagLines(); - var active = parseInt($('input[name="qlo_img_active_edit"]:checked').val() || 0, 10); - var showHotelName = parseInt($('input[name="qlo_img_hotelname_edit"]:checked').val() || 0, 10); - var tlColor = $('#qlo-img-tl-color').val() || '#ffffff'; - var tlFontSize = parseInt($('#qlo-img-tl-font-size').val() || 16, 10); - var tlFontWeight = $('#qlo-img-tl-font-weight').val() || '400'; + var titles = getFormTitles(); + var descriptions = getFormDescriptions(); + var active = parseInt($('input[name="qlo_img_active_edit"]:checked').val() || 0, 10); + var descColor = $('#qlo-img-desc-color').val() || '#ffffff'; + var descFontSize = parseInt($('#qlo-img-desc-font-size').val() || 16, 10); + var descFontWeight = $('#qlo-img-desc-font-weight').val() || '400'; var editFileInput = document.getElementById('qlo-img-edit-file'); var newFile = editFileInput && editFileInput.files && editFileInput.files[0] ? editFileInput.files[0] : null; @@ -523,13 +518,15 @@ $(document).ready(function () { fd.append('action', 'edit_image'); fd.append('id_header_image', id); fd.append('active', active); - fd.append('show_hotel_chain_name', showHotelName); fd.append('token', qloHmToken); - fd.append('tag_line_color', tlColor); - fd.append('tag_line_font_size', tlFontSize); - fd.append('tag_line_font_weight', tlFontWeight); - $.each(tagLines, function (langId, val) { - fd.append('tag_line_' + langId, val); + fd.append('description_color', descColor); + fd.append('description_font_size', descFontSize); + fd.append('description_font_weight', descFontWeight); + $.each(titles, function (langId, val) { + fd.append('title_' + langId, val); + }); + $.each(descriptions, function (langId, val) { + fd.append('description_' + langId, val); }); if (newFile) { fd.append('header_image_file', newFile); @@ -557,14 +554,14 @@ $(document).ready(function () { }).always(function () { $('#page-loader').hide(); }); } - function uploadImagesWithTagLine(files) { - var index = 0; - var tagLines = getFormTagLines(); - var active = parseInt($('input[name="qlo_img_active_add"]:checked').val() || 1, 10); - var showHotelName = parseInt($('input[name="qlo_img_hotelname_add"]:checked').val() || 1, 10); - var tlColor = $('#qlo-img-tl-color').val() || '#ffffff'; - var tlFontSize = parseInt($('#qlo-img-tl-font-size').val() || 16, 10); - var tlFontWeight = $('#qlo-img-tl-font-weight').val() || '400'; + function uploadImagesWithDetails(files) { + var index = 0; + var titles = getFormTitles(); + var descriptions = getFormDescriptions(); + var active = parseInt($('input[name="qlo_img_active_add"]:checked').val() || 1, 10); + var descColor = $('#qlo-img-desc-color').val() || '#ffffff'; + var descFontSize = parseInt($('#qlo-img-desc-font-size').val() || 16, 10); + var descFontWeight = $('#qlo-img-desc-font-weight').val() || '400'; $('#qlo-form-upload-progress').show(); $('#qlo-img-form-upload-btn').prop('disabled', true); $('#page-loader').show(); @@ -589,12 +586,14 @@ $(document).ready(function () { fd.append('action', 'uploadImage'); fd.append('token', qloHmToken); fd.append('active', active); - fd.append('show_hotel_chain_name', showHotelName); - fd.append('tag_line_color', tlColor); - fd.append('tag_line_font_size', tlFontSize); - fd.append('tag_line_font_weight', tlFontWeight); - $.each(tagLines, function (langId, val) { - fd.append('tag_line_' + langId, val); + fd.append('description_color', descColor); + fd.append('description_font_size', descFontSize); + fd.append('description_font_weight', descFontWeight); + $.each(titles, function (langId, val) { + fd.append('title_' + langId, val); + }); + $.each(descriptions, function (langId, val) { + fd.append('description_' + langId, val); }); $.ajax({ @@ -648,16 +647,25 @@ $(document).ready(function () { if (data.thumb_src) { $row.attr('data-thumb-src', data.thumb_src); } - var updatedTagLines = safeParseJsonHm(data.tag_lines_json) || {}; - $row.attr('data-tag-lines', data.tag_lines_json); - $row.data('tag-lines', updatedTagLines); - $row.attr('data-tag-line-color', data.tag_line_color || '#ffffff'); - $row.attr('data-tag-line-font-size', data.tag_line_font_size || 16); - $row.attr('data-tag-line-font-weight', data.tag_line_font_weight || '400'); - var display = data.tag_line || ''; - $row.find('.qlo-img-tagline-cell').text( - display.length > 50 ? display.substring(0, 50) + '...' : (display || '—') + var updatedTitles = safeParseJsonHm(data.titles_json) || {}; + $row.attr('data-titles', data.titles_json); + $row.data('titles', updatedTitles); + var titleDisplay = data.title || ''; + $row.find('.qlo-img-title-cell').text( + titleDisplay.length > 50 ? titleDisplay.substring(0, 50) + '...' : (titleDisplay || '—') ); + + var updatedDescriptions = safeParseJsonHm(data.descriptions_json) || {}; + $row.attr('data-descriptions', data.descriptions_json); + $row.data('descriptions', updatedDescriptions); + $row.attr('data-description-color', data.description_color || '#ffffff'); + $row.attr('data-description-font-size', data.description_font_size || 16); + $row.attr('data-description-font-weight', data.description_font_weight || '400'); + var descDisplay = data.description || ''; + $row.find('.qlo-img-description-cell').text( + descDisplay.length > 50 ? descDisplay.substring(0, 50) + '...' : (descDisplay || '—') + ); + var nowActive = parseInt(data.active, 10); var $toggle = $row.find('.js-toggle-active'); $toggle.toggleClass('action-enabled', !!nowActive) @@ -666,17 +674,6 @@ $(document).ready(function () { $toggle.find('i.icon-remove').toggleClass('hidden', !!nowActive); $toggle.attr('href', $toggle.attr('href').replace(/id_header_image=\d+/, 'id_header_image=' + id) .replace(/active=\d+/, 'active=' + (nowActive ? 0 : 1))); - - if (data.show_hotel_chain_name !== undefined) { - var nowShowHotelName = parseInt(data.show_hotel_chain_name, 10); - var $hnToggle = $row.find('.js-toggle-hotel-name'); - $hnToggle.toggleClass('action-enabled', !!nowShowHotelName) - .toggleClass('action-disabled', !nowShowHotelName); - $hnToggle.find('i.icon-check').toggleClass('hidden', !nowShowHotelName); - $hnToggle.find('i.icon-remove').toggleClass('hidden', !!nowShowHotelName); - $hnToggle.attr('href', $hnToggle.attr('href').replace(/id_header_image=\d+/, 'id_header_image=' + id) - .replace(/show_hotel_chain_name=\d+/, 'show_hotel_chain_name=' + (nowShowHotelName ? 0 : 1))); - } } function safeParseJsonHm(raw) { diff --git a/modules/hotelreservationsystem/views/js/qhrs_hotel_header_media_front.js b/modules/hotelreservationsystem/views/js/qhrs_hotel_header_media_front.js index 779e4e024..e1fcb647a 100644 --- a/modules/hotelreservationsystem/views/js/qhrs_hotel_header_media_front.js +++ b/modules/hotelreservationsystem/views/js/qhrs_hotel_header_media_front.js @@ -75,57 +75,61 @@ $(document).ready(function () { }; - var $taglineEl = $('.js-header-tagline'); - var $hotelNameEl = $('.js-header-hotel-name'); - var _fadeTimer = null; - var _shownIndex = -1; + var $descriptionEl = $('.js-header-description'); + var $titleEl = $('.js-header-title'); + var _fadeTimer = null; + var _shownIndex = -1; var $slides = $imgCarousel.find('.owl-item:not(.cloned) .header-slide-img'); - var taglines = $slides.map(function () { return $(this).data('tagline') || ''; }).get(); - var tlStyles = $slides.map(function () { + var titles = $slides.map(function () { return $(this).data('title') || ''; }).get(); + var descriptions = $slides.map(function () { return $(this).data('description') || ''; }).get(); + var descStyles = $slides.map(function () { return { - color: $(this).data('tl-color') || '#ffffff', - fontSize: $(this).data('tl-font-size') || 16, - fontWeight: $(this).data('tl-font-weight') || '400' + color: $(this).data('desc-color') || '#ffffff', + fontSize: $(this).data('desc-font-size') || 16, + fontWeight: $(this).data('desc-font-weight') || '400' }; }).get(); - var showHotelNames = $slides.map(function () { return $(this).data('show-hotel-name') == 1; }).get(); - function showTagline(tag, style, showHotelName) { - if ($hotelNameEl.length) { - $hotelNameEl.toggle(!!showHotelName); - } + function showTitle(title) { + if (!$titleEl.length) { return; } + $titleEl.text(title); + $titleEl.toggle(!!title); + } - if (!$taglineEl.length) { return; } + function showDescription(text, style) { + if (!$descriptionEl.length) { return; } if (_fadeTimer) { clearTimeout(_fadeTimer); _fadeTimer = null; } style = style || { color: '#ffffff', fontSize: 16, fontWeight: '400' }; - $taglineEl.css({ + $descriptionEl.css({ color: style.color, fontSize: style.fontSize + 'px', fontWeight: style.fontWeight }); - $taglineEl.removeClass('wk-tagline-out wk-tagline-in').text(tag); - if (!tag) { $taglineEl.hide(); return; } + $descriptionEl.removeClass('wk-tagline-out wk-tagline-in').text(text); + if (!text) { $descriptionEl.hide(); return; } - $taglineEl.show(); - $taglineEl[0].offsetHeight; // force reflow so the transition actually runs - $taglineEl.addClass('wk-tagline-in'); + $descriptionEl.show(); + $descriptionEl[0].offsetHeight; // force reflow so the transition actually runs + $descriptionEl.addClass('wk-tagline-in'); _fadeTimer = setTimeout(function () { _fadeTimer = null; - $taglineEl.removeClass('wk-tagline-in'); + $descriptionEl.removeClass('wk-tagline-in'); }, 400); } _shownIndex = owl.relative(owl.current()); - showTagline(taglines[_shownIndex] || '', tlStyles[_shownIndex], showHotelNames[_shownIndex]); + showTitle(titles[_shownIndex] || ''); + showDescription(descriptions[_shownIndex] || '', descStyles[_shownIndex]); $imgCarousel.on('changed.owl.carousel', function () { var realIndex = owl.relative(owl.current()); if (realIndex === _shownIndex) { return; } // loop-snap second fire — no-op _shownIndex = realIndex; - showTagline(taglines[realIndex] || '', tlStyles[realIndex], showHotelNames[realIndex]); + showTitle(titles[realIndex] || ''); + showDescription(descriptions[realIndex] || '', descStyles[realIndex]); }); diff --git a/modules/hotelreservationsystem/views/templates/admin/hotel_header_image/_partials/htl-header-image-row.tpl b/modules/hotelreservationsystem/views/templates/admin/hotel_header_image/_partials/htl-header-image-row.tpl index 7bfe4b101..574d9e148 100644 --- a/modules/hotelreservationsystem/views/templates/admin/hotel_header_image/_partials/htl-header-image-row.tpl +++ b/modules/hotelreservationsystem/views/templates/admin/hotel_header_image/_partials/htl-header-image-row.tpl @@ -23,18 +23,22 @@ + data-titles="{$img.titles_json|escape:'htmlall':'UTF-8'}" + data-descriptions="{$img.descriptions_json|escape:'htmlall':'UTF-8'}" + data-description-color="{$img.description_color|default:'#ffffff'|escape:'htmlall':'UTF-8'}" + data-description-font-size="{$img.description_font_size|default:16|intval}" + data-description-font-weight="{$img.description_font_weight|default:'400'|escape:'htmlall':'UTF-8'}"> {if $img.thumb}{$img.thumb nofilter}{else}{/if} - - {if $img.tag_line}{$img.tag_line|truncate:50:'...'|escape:'html':'UTF-8'}{else}—{/if} + + {if $img.title}{$img.title|truncate:50:'...'|escape:'html':'UTF-8'}{else}—{/if} + + + {if $img.description}{$img.description|truncate:50:'...'|escape:'html':'UTF-8'}{else}—{/if}

    @@ -49,14 +53,6 @@ - - - - - -
    diff --git a/modules/hotelreservationsystem/views/templates/admin/hotel_header_image/helpers/view/view.tpl b/modules/hotelreservationsystem/views/templates/admin/hotel_header_image/helpers/view/view.tpl index 6e50c1681..8a49eb9fc 100644 --- a/modules/hotelreservationsystem/views/templates/admin/hotel_header_image/helpers/view/view.tpl +++ b/modules/hotelreservationsystem/views/templates/admin/hotel_header_image/helpers/view/view.tpl @@ -51,8 +51,8 @@

    {l s='Visible only when 2 or more active images exist.' mod='hotelreservationsystem'}

    @@ -85,7 +85,7 @@
    -
    - -
    - -

    {l s='Leave \'Select status\' to keep the current status.' mod='hotelreservationsystem'}

    -
    -
    - -
    - +
    - - - - + + + + -

    {l s='Turn on to overwrite the tag line text for all selected images.' mod='hotelreservationsystem'}

    +

    {l s='Turn on to overwrite the title text for all selected images.' mod='hotelreservationsystem'}

    -