1(function ($, settings) { 2 $.fn.homelocalBookingWidget = function (options) { 3 const app = this; 4 const $widget = $(this); 5 options = $.extend({}, options); 6 options.max_num_days = parseInt(options.max_num_days, 10) || 365; 7 options.max_stay = parseInt(options.max_stay, 10) || 365; 8 9 let selector = { 10 widget: ".homelocal-booking-widget", 11 widgetBody: ".homelocal-booking-widget-body", 12 widgetLoader: ".widget-loader", 13 priceLoader: ".price-loader", 14 bookingOverview: ".homelocal-booking-overview", 15 quoteNotice: ".quote-notice", 16 couponNotice: ".coupon-notice", 17 checkoutBtn: ".checkout-button", 18 rentUnit: ".rent-unit", 19 totalNights: ".total-nights", 20 priceDetailsModal: "#homelocal-booking-price-details-modal", 21 stickyWidgetModal: "#homelocal-sticky-booking-widget-modal", 22 stickyWidgetContainer: ".homelocal-sticky-booking-widget-container", 23 stickyBtn: ".modal-button", 24 stickyRentUnit: ".rent-unit", 25 stickyTotalNights: ".total-nights", 26 stickyPrice: ".price", 27 stickyNotice: ".notice", 28 stickyPriceLoader: ".price-loader", 29 stickyDates: ".dates", 30 calendarModal: "#homelocal-availability-calendar-modal", 31 }; 32 33 const $sticky = $(selector.stickyWidgetContainer); 34 35 const calendarUtils = new HomeLocalCalendarUtils( 36 homerunner_calendar_utils_settings, 37 { 38 min_stay: options.min_stay, 39 max_stay: options.max_stay, 40 max_num_days: options.max_num_days, 41 timezone: options.timezone, 42 }, 43 ); 44 45 let quote = {}; 46 // Coupon event waiting for the next quote render: "applied" | "removed" | null (#877). 47 let pendingCouponEvent = null; 48 let addons = []; 49 const travelCoverageFeeTypes = Array.isArray( 50 settings.travel_coverage_fee_types, 51 ) 52 ? settings.travel_coverage_fee_types 53 : ["travel_insurance"]; 54 const travelInsuranceGroup = `travel_insurance_${Math.random() 55 .toString(36) 56 .slice(2, 9)}`; 57 let ratePlanSelected = options.rateplan_id || null; 58 const dateFormat = "yy-mm-dd"; 59 const luxonDateFormat = "yyyy-MM-dd"; 60 61 app.loadPrice = (done) => { 62 $widget.addClass("hfse-loading"); 63 64 $.ajax({ 65 url: `${settings.wp_api}wp/v2/properties/${options.property_id}/calendar`, 66 type: "GET", 67 data: { 68 _: new Date().getTime(), 69 }, 70 beforeSend: function (xhr) { 71 if (settings.user_nonce) { 72 xhr.setRequestHeader("X-Homerunner-Nonce", settings.user_nonce); 73 } 74 }, 75 }) 76 .done((data) => { 77 if (typeof done === "function") { 78 done(data); 79 } 80 }) 81 .always(() => { 82 $widget.removeClass("hfse-loading"); 83 }); 84 }; 85 86 app.init = () => { 87 if ($(selector.stickyWidgetContainer).length > 0) { 88 $(selector.stickyWidgetContainer).appendTo(".hk-scope-modal"); 89 } 90 91 if (options.show_min_stay) { 92 app.renderMinimumNights(); 93 } 94 95 app.registerActions(); 96 if (options.show_coupon) { 97 app.registerCouponActions(); 98 } 99 app.registerAddonActions(); 100 101 if (options.show_datepicker) { 102 $widget.find(selector.widgetLoader).show(); 103 if (settings.use_calendar) { 104 app.initCalendar(); 105 } else { 106 app.initDatepicker(); 107 } 108 } else if (options.quote_id) { 109 app.refreshQuote(options.quote_id); 110 } else if (options.checkin && options.checkout && options.guests) { 111 app.updateBookingCost(); 112 } 113 }; 114 115 app.debounce = (func, timeout = 300) => { 116 let timer; 117 return (...args) => { 118 clearTimeout(timer); 119 timer = setTimeout(() => { 120 func.apply(this, args); 121 }, timeout); 122 }; 123 }; 124 125 app.onAvailabilityCalendarLoaded = (e, property_id, data) => { 126 if ( 127 property_id === options.property_id && 128 !calendarUtils.hasPriceData() 129 ) { 130 calendarUtils.setPriceData(data); 131 132 if ("first" === settings.min_stay_source && options.show_min_stay) { 133 app.renderMinimumNights(); 134 } 135 } 136 }; 137 138 app.updateUrl = () => { 139 const url = new URL(window.location.href); 140 const checkin = $widget.find("#checkin").val(); 141 const checkout = $widget.find("#checkout").val(); 142 const guests = $widget.find("#guests").val(); 143 144 if (checkin) { 145 url.searchParams.set("checkin", checkin); 146 } else { 147 url.searchParams.delete("checkin"); 148 } 149 150 if (checkout) { 151 url.searchParams.set("checkout", checkout); 152 } else { 153 url.searchParams.delete("checkout"); 154 } 155 156 if (guests && guests !== "1") { 157 url.searchParams.set("guests", guests); 158 } else { 159 url.searchParams.delete("guests"); 160 } 161 162 window.history.replaceState({}, "", url); 163 }; 164 165 app.initCalendar = () => { 166 $(document).on( 167 "homelocal_availability_calendar_loaded", 168 app.onAvailabilityCalendarLoaded, 169 ); 170 171 $widget.find("#checkin_placeholder").on("click", () => { 172 hkUIkit.modal(selector.calendarModal).show(); 173 }); 174 175 $widget.find("#checkout_placeholder").on("click ", () => { 176 hkUIkit.modal(selector.calendarModal).show(); 177 }); 178 179 $widget.on("change", "#checkout", () => { 180 hkUIkit.modal(selector.calendarModal).hide(); 181 182 app.updateChekinPlaceholder(); 183 app.updateChekoutPlaceholder(); 184 app.updateUrl(); 185 }); 186 187 $widget.on( 188 "change", 189 "#checkout,#guests", 190 app.debounce(() => { 191 app.updateBookingCost(); 192 }, 10), 193 ); 194 195 $widget.on("change", "#guests", () => { 196 app.updateUrl(); 197 }); 198 199 const checkin = $widget.find("#checkin").val(); 200 const checkout = $widget.find("#checkout").val(); 201 202 if (checkin) { 203 app.updateChekinPlaceholder(); 204 } 205 206 if (checkout) { 207 app.updateChekoutPlaceholder(); 208 } 209 210 if (checkin && checkout) { 211 if (calendarUtils.hasPriceData()) { 212 app.updateBookingCost(); 213 } else { 214 $(document).on( 215 "homelocal_availability_calendar_loaded", 216 app.debounce(app.updateBookingCost, 300), 217 ); 218 } 219 } else { 220 $widget.find(selector.widgetBody).show(); 221 $widget.find(selector.widgetLoader).hide(); 222 223 $widget 224 .find(selector.checkoutBtn) 225 .prop("disabled", false)
226 .text(settings.select_dates) 227 .show(); 228 if (options.sticky) { 229 $sticky 230 .find(selector.stickyBtn) 231 .prop("disabled", false) 232 .text(settings.select_dates); 233 } 234 235 app.showNoDatesRent(); 236 } 237 }; 238 239 app.initDatepicker = () => { 240 $widget 241 .find("#checkin_placeholder,#checkout_placeholder") 242 .prop("disabled", true); 243 244 app.loadPrice((data) => { 245 $widget 246 .find("#checkin_placeholder,#checkout_placeholder") 247 .prop("disabled", false); 248 249 calendarUtils.setPriceData(data); 250 251 $(document).trigger("homelocal_calendar_data_loaded", [ 252 options.property_id, 253 data, 254 ]); 255 256 const minDate = calendarUtils.getFirstCheckinDay() 257 ? calendarUtils.getFirstCheckinDay() 258 : 1; 259 260 const checkinDp = $widget.find("#checkin").datepicker({ 261 minDate: calendarUtils.getDateZero(minDate).toFormat(luxonDateFormat), 262 maxDate: options.max_num_days, 263 dateFormat: dateFormat, 264 beforeShowDay: (d) => { 265 const day = $.datepicker.formatDate(dateFormat, d); 266 if (!calendarUtils.canCheckin(day).status) { 267 return [false, "unavailable", settings.date_unavailable]; 268 } else { 269 return [true, "available", "available"]; 270 } 271 }, 272 }); 273 274 const checkoutDp = $widget.find("#checkout").datepicker({ 275 minDate: 1, 276 maxDate: 1, 277 dateFormat: dateFormat, 278 beforeShowDay: (d) => { 279 const checkin = checkinDp.val(); 280 if (!checkin) { 281 return [false, "unavailable", settings.date_unavailable]; 282 } 283 284 const checkout = $.datepicker.formatDate(dateFormat, d); 285 286 if (!calendarUtils.canCheckinout(checkin, checkout).status) { 287 return [false, "unavailable", settings.date_unavailable]; 288 } else { 289 return [true, "available", "available"]; 290 } 291 }, 292 }); 293 294 // show checkout selector after changing checkin 295 checkinDp.on("change", function () { 296 const checkin = app.getFormattedDate(this); 297 298 app.updateChekinPlaceholder(); 299 300 if (null !== checkin) { 301 const checkinDate = calendarUtils.getDateZero(checkin); 302 const checkoutMin = checkinDate.plus({ 303 days: calendarUtils.getMinstay(checkin), 304 }); 305 306 app.renderMinimumNights(calendarUtils.getMinstay(checkin)); 307 308 checkoutDp.datepicker( 309 "option", 310 "minDate", 311 checkoutMin.toFormat(luxonDateFormat), 312 ); 313 314 const checkoutMax = checkinDate.plus({ days: options.max_stay }); 315 316 checkoutDp.datepicker( 317 "option", 318 "maxDate", 319 checkoutMax.toFormat(luxonDateFormat), 320 ); 321 322 setTimeout(function () { 323 checkoutDp.datepicker("show"); 324 }); 325 } 326 }); 327 328 checkoutDp.on("change", function () { 329 app.updateChekinPlaceholder(); 330 app.updateChekoutPlaceholder(); 331 }); 332 333 $widget.find("#checkin_placeholder").on("focus", function () { 334 $widget.find("#checkin").datepicker("show"); 335 }); 336 337 $widget.find("#checkout_placeholder").on("focus", function () { 338 $widget.find("#checkout").datepicker("show"); 339 }); 340 341 if ("first" === settings.min_stay_source && options.show_min_stay) { 342 app.renderMinimumNights(); 343 } 344 }); 345 346 $widget.on( 347 "change", 348 "#checkout,#guests", 349 app.debounce(app.updateBookingCost), 350 ); 351 352 const checkin = $widget.find("#checkin").val(); 353 const checkout = $widget.find("#checkout").val(); 354 355 if (checkin) { 356 app.updateChekinPlaceholder(); 357 } 358 359 if (checkout) { 360 app.updateChekoutPlaceholder(); 361 } 362 363 if (checkin && checkout) { 364 if (calendarUtils.hasPriceData()) { 365 app.updateBookingCost(); 366 } else { 367 $(document).on( 368 "homelocal_calendar_data_loaded", 369 app.updateBookingCost, 370 ); 371 } 372 } else { 373 $widget.find(selector.widgetBody).show(); 374 $widget.find(selector.widgetLoader).hide(); 375 376 $widget 377 .find(selector.checkoutBtn) 378 .prop("disabled", false)
379 .text(settings.select_dates) 380 .show(); 381 if (options.sticky) { 382 $sticky 383 .find(selector.stickyBtn) 384 .prop("disabled", false) 385 .text(settings.select_dates); 386 } 387 388 app.showNoDatesRent(); 389 } 390 }; 391 392 app.updateChekinPlaceholder = () => { 393 const checkin = $widget.find("#checkin").val(); 394 395 if (checkin) { 396 $widget 397 .find("#checkin_placeholder") 398 .val( 399 calendarUtils.getDateZero(checkin).toFormat(options.date_format), 400 ); 401 } else { 402 $widget.find("#checkin_placeholder").val(""); 403 } 404 }; 405 406 app.updateChekoutPlaceholder = () => { 407 const checkout = $widget.find("#checkout").val(); 408 409 if (checkout) { 410 $widget 411 .find("#checkout_placeholder") 412 .val( 413 calendarUtils.getDateZero(checkout).toFormat(options.date_format), 414 ); 415 } else { 416 $widget.find("#checkout_placeholder").val(""); 417 } 418 }; 419 420 app.showNoDatesRent = () => { 421 if (options.show_rent) { 422 if (settings.price_without_dates === "base_price") { 423 app.renderBaseRent(options.base_rent); 424 } else if (settings.price_without_dates === "average_price") { 425 app.renderAvgRent(options.avg_rent); 426 } else if (settings.price_without_dates === "lowest_price") { 427 app.renderLowestRent(); 428 } else { 429 app.hideRent(); 430 } 431 } 432 }; 433 434 app.renderRentAfterDate = (total, totalNights, discount = 0) => { 435 let price = 0, 436 actualPrice = parseFloat(total) / totalNights; 437 if (discount > 0) { 438 price = parseFloat(total - discount) / totalNights; 439 } else { 440 price = parseFloat(total) / totalNights; 441 } 442 443 let rentText = settings.rent_after_date_template.replace( 444 "{price}", 445 `<span class="rent-after-date rent-amount">${app.price(price)}</span>`, 446 ); 447 448 if (actualPrice && actualPrice > price) { 449 rentText = settings.rent_after_date_template.replace( 450 "{price}", 451 `<del class="rent-deleted">${app.price( 452 actualPrice, 453 )}</del><span class="rent-after-date rent-amount">${app.price( 454 price, 455 )}</span>`, 456 ); 457 } 458 459 $sticky.find(selector.stickyRentUnit).html(rentText).show(); 460 461 if (options.show_bedroom_price && options.bedrooms_count > 1) { 462 rentText += `<span class="rent-bedroom-sep">·</span><span class="bedroom-price-text">${settings.per_bedroom_text.replace( 463 "{price}", 464 app.price(Math.round(price / options.bedrooms_count)), 465 )}</span>`; 466 } 467 468 $widget.find(selector.rentUnit).html(rentText).show(); 469 }; 470 471 app.renderBaseRent = (price) => { 472 $widget 473 .find(selector.rentUnit) 474 .html( 475 settings.base_rent_template.replace( 476 "{price}", 477 `<span class="base-rent rent-amount">${app.price(price)}</span>`, 478 ), 479 ) 480 .show(); 481 482 $sticky 483 .find(selector.stickyRentUnit) 484 .html( 485 settings.base_rent_template.replace( 486 "{price}", 487 `<span class="base-rent rent-amount">${app.price(price)}</span>`, 488 ), 489 ) 490 .show(); 491 }; 492 493 app.renderAvgRent = (price) => { 494 $widget 495 .find(selector.rentUnit) 496 .html( 497 settings.avg_price_template.replace( 498 "{price}", 499 `<span class="average-rent rent-amount">${app.price(price)}</span>`, 500 ), 501 ) 502 .show(); 503 504 $sticky 505 .find(selector.stickyRentUnit) 506 .html( 507 settings.avg_price_template.replace( 508 "{price}", 509 `<span class="average-rent rent-amount">${app.price(price)}</span>`, 510 ), 511 ) 512 .show(); 513 }; 514 515 app.renderLowestRent = () => { 516 const price = options.lowest_rent; 517 $widget 518 .find(selector.rentUnit) 519 .html( 520 settings.lowest_rent_template.replace( 521 "{price}", 522 `<span class="lowest-rent rent-amount">${app.price(price)}</span>`, 523 ), 524 ) 525 .show(); 526 527 $sticky 528 .find(selector.stickyRentUnit) 529 .html( 530 settings.lowest_rent_template.replace( 531 "{price}", 532 `<span class="lowest-rent rent-amount">${app.price(price)}</span>`, 533 ), 534 ) 535 .show(); 536 }; 537 538 app.hideRent = () => { 539 $widget.find(selector.rentUnit).empty().hide(); 540 541 $sticky.find(selector.stickyRentUnit).hide(); 542 }; 543 544 app.renderMinimumNights = (minStay = null) => { 545 if (!minStay) { 546 minStay = options.min_stay; 547 548 if ("first" === settings.min_stay_source) { 549 if (calendarUtils.hasPriceData()) { 550 const day = calendarUtils.getFirstCheckinDay(); 551 if (day && calendarUtils.canCheckin(day).status) { 552 minStay = calendarUtils.getMinstay(day); 553 } 554 } 555 } 556 } 557 558 let template = settings.min_night_text; 559 if (parseInt(minStay, 10) > 1) { 560 template = settings.min_nights_text; 561 } 562 563 $widget 564 .find(selector.totalNights) 565 .text(template.replace("{nights}", minStay)) 566 .show(); 567 568 $sticky 569 .find(selector.stickyTotalNights) 570 .text(template.replace("{nights}", minStay)) 571 .show(); 572 }; 573 574 app.hideMinimumNights = () => { 575 $widget.find(selector.totalNights).hide(); 576 577 $sticky.find(selector.stickyTotalNights).hide(); 578 }; 579 580 app.renderTotal = (total, totalBeforeTax, discountItems = []) => { 581 if (options.total_after_tax) {
582 $widget.find(".total-price-label").text(settings.total_after_tax_label); 583 $widget.find(".total-price").text(app.price(total)); 584 } else { 585 $widget 586 .find(".total-price-label") 587 .text(settings.total_before_tax_label); 588 589 $widget.find(".total-price").text(app.price(totalBeforeTax)); 590 } 591 592 // const $discountItems = $widget.find(".discount-items"); 593 // $discountItems.empty(); 594 // discountItems.forEach((item) => { 595 // const lineItem = $(` 596 // <div class="hk-flex hk-flex-between"> 597 // <span class="hk-text-bold">${item.title}</span> 598 // <span class="hk-text-bold">-${app.price(item.amount)}</span> 599 // </div> 600 // `); 601 // $discountItems.append(lineItem); 602 // }); 603 // $discountItems.show(); 604 }; 605 606 app.renderNights = (night) => { 607 if (night === 1) { 608 $widget 609 .find(selector.totalNights) 610 .text(settings.total_night_text.replace("{nights}", night)) 611 .show(); 612 } else { 613 $widget 614 .find(selector.totalNights) 615 .text(settings.total_nights_text.replace("{nights}", night)) 616 .show(); 617 } 618 }; 619 620 app.registerActions = () => { 621 $widget.on("change", 'input[name="rateplan"]', function () { 622 if (!quote || !quote.id) { 623 return false; 624 } 625 626 ratePlanSelected = $(this).val(); 627 const item = quote.rateplans.find( 628 (item) => item.id === ratePlanSelected, 629 ); 630 631 if (item) { 632 app.updatePriceBreakdown(); 633 634 // Percentage addons are priced against the selected rate plan. 635 if (quote.available_addons) { 636 app.renderAddons(); 637 } 638 639 if (options.show_coupon) { 640 app.renderCoupon(); 641 } 642 } else { 643 console.warn(settings.invalid_rateplan); 644 } 645 }); 646 647 $widget.on("click", selector.checkoutBtn, function () { 648 const checkin = $widget.find("#checkin").val(); 649 const checkout = $widget.find("#checkout").val(); 650 651 if (!checkin) { 652 if (settings.use_calendar) { 653 $widget.find("#checkin_placeholder").trigger("click"); 654 } else { 655 // $widget.find("#checkin").focus(); 656 $widget.find("#checkin_placeholder").focus(); 657 } 658 return false; 659 } else if (!checkout) { 660 if (settings.use_calendar) { 661 $widget.find("#checkout_placeholder").trigger("click"); 662 } else { 663 $widget.find("#checkout_placeholder").focus(); 664 } 665 return false; 666 } 667 668 if (!quote.id) { 669 return false; 670 } 671 672 if (options.reservation_target === "hosted") { 673 $widget.find(selector.bookingOverview).hide(); 674 $widget.find(selector.priceLoader).show(); 675 $widget 676 .find(selector.checkoutBtn) 677 .prop("disabled", true) 678 .text(options.booking_button_text_processing); 679 680 $.ajax({ 681 url: 682 settings.plugin_api + 683 "quotes/" + 684 quote.id + 685 "/checkout-url?_=" + 686 new Date().getTime(), 687 type: "GET", 688 data: { 689 rateplan_id: ratePlanSelected, 690 }, 691 }) 692 .done(function (response) { 693 if (response.success) { 694 window.location.href = response.data.url; 695 } else { 696 app.quoteNotice(response.message, "warning"); 697 } 698 }) 699 .fail(function (jqXHR, textStatus, errorThrown) { 700 app.quoteNotice(jqXHR.responseJSON.message, "warning"); 701 }) 702 .always(function () { 703 $widget.find(selector.bookingOverview).show(); 704 $widget.find(selector.priceLoader).hide(); 705 $widget 706 .find(selector.checkoutBtn) 707 .prop("disabled", false) 708 .text(options.booking_button_text); 709 }); 710 711 return false; 712 } 713 714 if ($("#homelocal-booking-widget-checkout-modal").length > 0) { 715 hkUIkit.modal("#homelocal-booking-widget-checkout-modal").show(); 716 return false;
717 } 718 719 $widget.find(selector.checkoutBtn).prop("disabled", true); 720 721 let url = options.checkout_url; 722 // add qid to the url, check if ? already exists 723 if (url.indexOf("?") > -1) { 724 url += "&qid=" + quote.id.toString(); 725 } else { 726 url += "?qid=" + quote.id.toString(); 727 } 728 729 if (ratePlanSelected) { 730 url += "&rpid=" + ratePlanSelected; 731 } 732 733 window.location.href = url; 734 }); 735 736 $(document).on( 737 "click", 738 ".homelocal-checkout-page-select-btn", 739 function () { 740 let url = $( 741 'input[name="homelocal_checkout_provider_url"]:checked', 742 ).val(); 743 744 if (!url) { 745 return false; 746 } 747 748 if (url.indexOf("?") > -1) { 749 // add qid to the url, check if ? already exists 750 url += "&qid=" + quote.id.toString(); 751 } else { 752 url += "?qid=" + quote.id.toString(); 753 } 754 755 if (ratePlanSelected) { 756 url += "&rpid=" + ratePlanSelected; 757 } 758 759 window.location.href = url; 760 return false; 761 }, 762 ); 763 764 $(document).on( 765 "change", 766 'input[name="homelocal_checkout_provider_url"]', 767 function () { 768 // add a active class to closest label. 769 $( 770 "#homelocal-booking-widget-checkout-modal .homelocal-booking-widget-checkout-item", 771 ).removeClass("hk-selected"); 772 $(this) 773 .closest(".homelocal-booking-widget-checkout-item") 774 .addClass("hk-selected"); 775 }, 776 ); 777 778 $widget.on("updating", function () { 779 $widget 780 .find(selector.checkoutBtn) 781 .prop("disabled", true) 782 .text(options.booking_button_text_processing) 783 .show(); 784 785 $sticky 786 .find(selector.stickyBtn) 787 .prop("disabled", true) 788 .text(options.booking_button_text_processing); 789 790 $sticky.find(selector.stickyPriceLoader).show(); 791 $sticky.find(selector.stickyNotice).empty(); 792 $sticky.find(selector.stickyPrice).hide(); 793 $sticky.find(selector.stickyTotalNights).hide(); 794 }); 795 796 $widget.on("unavailable", function (e, error) { 797 $widget 798 .find(selector.checkoutBtn) 799 .prop("disabled", true) 800 .text(options.booking_button_text) 801 .show(); 802 803 $sticky 804 .find(selector.stickyBtn) 805 .text(options.booking_button_text) 806 .prop("disabled", false); 807 808 $sticky 809 .find(selector.stickyNotice) 810 .html(`<div class="hk-text-danger">${error}</div>`) 811 .show(); 812 813 $sticky.find(selector.stickyDates).show(); 814 $sticky.find(selector.stickyRentUnit).hide(); 815 $sticky.find(selector.stickyPrice).hide(); 816 $sticky.find(selector.stickyPriceLoader).hide(); 817 }); 818 819 $widget.on("available", function () { 820 $widget 821 .find(selector.checkoutBtn) 822 .prop("disabled", false) 823 .text(options.booking_button_text) 824 .show(); 825 826 $sticky 827 .find(selector.stickyBtn) 828 .prop("disabled", false) 829 .text(options.booking_button_text) 830 .show(); 831 832 $sticky.find(selector.stickyPrice).show(); 833 $sticky.find(selector.stickyNotice).empty(); 834 $sticky.find(selector.stickyPriceLoader).hide(); 835 $sticky.find(selector.stickyTotalNights).hide(); 836 }); 837 838 let priceFromModalWidget = false; 839 $(document).on( 840 "click", 841 "#homelocal-sticky-booking-widget-modal .price-details-modal-button", 842 function () { 843 priceFromModalWidget = true; 844 }, 845 ); 846 847 $(document).on("beforeshow", selector.stickyWidgetModal, function () { 848 $widget.appendTo($(`${selector.stickyWidgetModal} .hk-widget-content`)); 849 $sticky.hide(); 850 }); 851 852 $(document).on("hidden", selector.stickyWidgetModal, function () { 853 $widget.appendTo($(".homelocal-booking-widget-container")); 854 $sticky.show(); 855 }); 856 857 if (options.price_modal) { 858 $(document).on("beforeshow", selector.priceDetailsModal, function (e) { 859 $sticky.hide(); 860 }); 861 862 $(document).on("hidden", selector.priceDetailsModal, function () { 863 if (priceFromModalWidget) { 864 priceFromModalWidget = false;
865 hkUIkit.modal(selector.stickyWidgetModal).show(); 866 } else { 867 $sticky.show(); 868 } 869 }); 870 } 871 872 $(document).on("click", selector.stickyBtn, function () { 873 const checkin = $widget.find("#checkin").val(); 874 const checkout = $widget.find("#checkout").val(); 875 876 if ((checkin && checkout) || !settings.use_calendar) { 877 hkUIkit.modal(selector.stickyWidgetModal).show(); 878 } else { 879 hkUIkit.modal(selector.calendarModal).show(); 880 } 881 }); 882 }; 883 884 app.registerAddonActions = () => { 885 $widget.on("click", ".addon-increase-btn", function () { 886 const $input = $(this).prev(); 887 const addonId = $input.data("id"); 888 const addon = quote.available_addons.find( 889 (addon) => addon.id === addonId, 890 ); 891 892 const selectedAddon = quote.selected_addons.find( 893 (item) => item.id === addonId, 894 ); 895 896 let qty = 0; 897 if (selectedAddon) { 898 qty = parseInt(selectedAddon.quantity, 10); 899 } 900 901 if (qty < addon.max_qty) { 902 qty++; 903 $input.text(qty); 904 905 app.addAddon(quote.id, addonId, qty); 906 } 907 }); 908 909 $widget.on("change", ".travel-insurance-input", function () { 910 app.setTravelInsurance(parseInt(this.value, 10) || 0); 911 }); 912 913 $widget.on("click", ".addon-reduce-btn", function () { 914 const $input = $(this).next(); 915 const addonId = parseInt($input.data("id"), 10); 916 const addon = quote.available_addons.find( 917 (addon) => addon.id === addonId, 918 ); 919 920 const selectedAddon = quote.selected_addons.find( 921 (item) => item.id === addonId, 922 ); 923 924 let qty = 0; 925 if (selectedAddon) { 926 qty = parseInt(selectedAddon.quantity, 10); 927 } 928 929 if (qty > 0) { 930 qty--; 931 $input.text(qty); 932 933 app.addAddon(quote.id, addonId, qty); 934 } 935 936 return false; 937 }); 938 }; 939 940 app.registerCouponActions = () => { 941 const applyCoupon = (e) => { 942 const coupon = $widget.find("#coupon").val() || options.coupon; 943 944 if (!quote.id) { 945 app.couponNotice(settings.coupon_require_dates, "warning"); 946 } else if (!coupon) { 947 app.couponNotice(settings.empty_coupon_code, "warning"); 948 } else { 949 app.applyCoupon(quote.id, coupon); 950 } 951 952 e.preventDefault(); 953 }; 954 955 $widget.on("click", "#apply-coupon-btn", applyCoupon); 956 // $widget.on("keypress", "#coupon", applyCoupon); 957 // listen for enter key 958 $widget.on("keypress", "#coupon", function (e) { 959 if (e.which === 13) { 960 applyCoupon(e); 961 } 962 }); 963 964 $widget.on("change", "#have_coupon", function () { 965 if ($(this).is(":checked")) { 966 $widget.find(".coupon-input-row").show(); 967 } else { 968 $widget.find(".coupon-input-row").hide(); 969 } 970 }); 971 972 if ($widget.find("#coupon").val()) { 973 $widget.find("#have_coupon").prop("checked", true).trigger("change"); 974 } 975 976 // Add-ons (e.g. the toolkit Promo Code Chip) ask for the coupon to go. 977 // Central keeps a coupon on PUT with an empty value, so a fresh quote 978 // without the coupon is the only way to drop it (#877). 979 $widget.on("homelocal:coupon:remove", function () { 980 app.removeCoupon(); 981 }); 982 }; 983 984 app.removeCoupon = () => { 985 if (!quote || !quote.id || !(quote.coupons || []).length) { 986 return false; 987 } 988 989 const { checkin, checkout, guests } = quote; 990 991 $widget.find("#coupon").val(""); 992 $widget.find("#have_coupon").prop("checked", false).trigger("change"); 993 // Otherwise the shortcode / URL coupon is re-applied on the next date change. 994 options.coupon = ""; 995 996 pendingCouponEvent = "removed"; 997 app.createQuote({ checkin, checkout, guests }); 998 999 return true; 1000 }; 1001 1002 // Fire homelocal:coupon:applied / homelocal:coupon:removed once the quote 1003 // that reflects the change has rendered, so listeners see final totals. 1004 app.fireCouponEvent = () => { 1005 const event = pendingCouponEvent; 1006 pendingCouponEvent = null; 1007 1008 if (!event) { 1009 return; 1010 } 1011 1012 if ("applied" === event) { 1013 const coupon = (quote.coupons || [])[0]; 1014 if (!coupon) { 1015 return; 1016 } 1017 1018 $widget.trigger("homelocal:coupon:applied", [ 1019 { 1020 code: coupon.code, 1021 type: coupon.type, 1022 discount: coupon.discount, 1023 quote, 1024 }, 1025 ]); 1026 } else { 1027 $widget.trigger("homelocal:coupon:removed", [{ quote }]); 1028 } 1029 }; 1030 1031 app.quoteNotice = (text = "", type = "primary") => { 1032 if (text) { 1033 $widget.find(selector.bookingOverview).hide(); 1034 $widget 1035 .find(selector.quoteNotice) 1036 .html(`<div class="hk-alert hk-alert-${type}">${text}</div>`); 1037 } else { 1038 $widget.find(selector.bookingOverview).show(); 1039 $widget.find(selector.quoteNotice).empty(); 1040 } 1041 }; 1042 1043 app.updateBookingCost = () => { 1044 const checkin = $widget.find("#checkin").val() || options.checkin; 1045 const checkout = $widget.find("#checkout").val() || options.checkout; 1046 const guests = $widget.find("#guests").val() || options.guests; 1047 const coupon = $widget.find("#coupon").val() || options.coupon; 1048 1049 if (!checkin || !checkout) { 1050 return; 1051 } 1052 1053 if (checkin && checkout && options.sticky) { 1054 app.updateStickyDates(checkin, checkout); 1055 } 1056 1057 if (!guests) { 1058 app.quoteNotice(settings.select_guests, "warning"); 1059 1060 $widget.find(selector.widgetBody).show(); 1061 $widget.find(selector.widgetLoader).hide(); 1062 1063 $widget 1064 .find(selector.checkoutBtn) 1065 .prop("disabled", true)
1066 .text(settings.select_dates) 1067 .show(); 1068 if (options.sticky) { 1069 $sticky 1070 .find(selector.stickyBtn) 1071 .prop("disabled", true) 1072 .text(settings.select_dates); 1073 } 1074 } else if (!calendarUtils.canCheckinout(checkin, checkout).status) { 1075 $widget.find(selector.widgetBody).show(); 1076 $widget.find(selector.widgetLoader).hide(); 1077 1078 $widget 1079 .find(selector.checkoutBtn) 1080 .prop("disabled", true) 1081 .text(settings.select_dates) 1082 .show(); 1083 if (options.sticky) { 1084 $sticky 1085 .find(selector.stickyBtn) 1086 .prop("disabled", true) 1087 .text(settings.select_dates); 1088 } 1089 1090 app.quoteNotice( 1091 calendarUtils.canCheckinout(checkin, checkout).message, 1092 "warning", 1093 ); 1094 } else if (checkin && checkout) { 1095 app.createQuote({ checkin, checkout, guests, coupon }); 1096 } 1097 }; 1098 1099 app.updateStickyPrice = () => { 1100 const checkin = quote.checkin; 1101 const checkout = quote.checkout; 1102 const rateplan = quote.rateplans.find( 1103 (item) => item.id === ratePlanSelected, 1104 ); 1105 1106 const total = rateplan.total; 1107 const totalRent = rateplan.total_rent; 1108 const totalBeforeTax = rateplan.total - rateplan.total_taxes; 1109 const discount = rateplan.discount || 0; 1110 const totalNights = calendarUtils.calcNights(checkin, checkout); 1111 1112 let totalPrice = 0; 1113 if (options.total_after_tax) { 1114 totalPrice = total; 1115 } else { 1116 totalPrice = totalBeforeTax; 1117 } 1118 1119 let price = 0, 1120 actualPrice = parseFloat(totalPrice) / totalNights; 1121 1122 if ("average_price" === settings.price_with_dates) { 1123 actualPrice = parseFloat(totalPrice) / totalNights; 1124 price = parseFloat(totalPrice - discount) / totalNights; 1125 } else if ("average_rent" === settings.price_with_dates) { 1126 actualPrice = parseFloat(totalRent) / totalNights; 1127 price = parseFloat(totalRent - discount) / totalNights; 1128 } 1129 1130 $sticky.find(selector.stickyRentUnit).hide(); 1131 1132 let rentText = `<span class="rent-after-date rent-amount">${app.price( 1133 price, 1134 )}</span>`; 1135 1136 if (actualPrice && actualPrice > price) { 1137 rentText = `<del class="rent-deleted">${app.price( 1138 actualPrice, 1139 )}</del><span class="rent-after-date rent-amount">${app.price( 1140 price, 1141 )}</span>`; 1142 } 1143 1144 const content = ` 1145 <span class="hk-text-bold"> 1146 ${settings.modal_rent_after_date_template.replace( 1147 "{price}", 1148 rentText, 1149 )} • ${settings.modal_total_rent_template.replace( 1150 "{price}", 1151 `<span class="total-rent">${app.price(totalPrice)}</span>`, 1152 )} 1153 </span> 1154 <span class="hk-icon hk-position-relative" style="bottom:2px;"> 1155 <svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"> 1156 <path d="M12 19c-3.86 0-7-3.14-7-7s3.14-7 7-7 7 3.14 7 7-3.14 7-7 7zm0-12.297c-2.913 0-5.297 2.384-5.297 5.297 0 2.914 2.384 5.297 5.297 5.297 2.914 0 5.297-2.383 5.297-5.297 0-2.913-2.383-5.297-5.297-5.297z" fill="#000"/> 1157 <path d="M11.129 10.45h1.703v5.448h-1.703v-5.449zM12.87 8.822a.87.87 0 1 1-1.741 0 .87.87 0 0 1 1.74 0z" fill="#000"/> 1158 </svg> 1159 </span> 1160 `; 1161 1162 if (options.price_modal) { 1163 $sticky 1164 .find(selector.stickyPrice) 1165 .html( 1166 `<a href="${selector.priceDetailsModal}" class="hk-link-reset hk-button-text hk-text-bold" hk-toggle>${content}</a>`, 1167 ) 1168 .show(); 1169 } else { 1170 $sticky 1171 .find(selector.stickyPrice) 1172 .html( 1173 `<span class="hk-link-reset hk-button-text hk-text-bold">${content}</span>`, 1174 ) 1175 .show(); 1176 } 1177 }; 1178 1179 app.updateStickyDates = (checkin, checkout) => { 1180 const checkinDate = calendarUtils.getDateZero(checkin); 1181 const checkoutDate = calendarUtils.getDateZero(checkout); 1182 let dateStr = `${checkinDate.toFormat("MMM d")}
1182 - ${checkoutDate.toFormat( 1183 "MMM d", 1184 )}`; 1185 1186 $sticky.find(selector.stickyRentUnit).hide(); 1187 1188 $sticky 1189 .find(selector.stickyDates) 1190 .html( 1191 ` 1192 <a href="${ 1193 settings.use_calendar 1194 ? selector.calendarModal 1195 : selector.stickyWidgetModal 1196 }" class="hk-link-reset hk-button-text hk-flex-inline hk-flex-center hk-flex-middle"" hk-toggle> 1197 <span>${dateStr}</span> 1198 <span class="hk-margin-small-left hk-icon"> 1199 <svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"> 1200 <path d="M8 4.8a.8.8 0 0 0-1.6 0v.8A2.4 2.4 0 0 0 4 8v9.6A2.4 2.4 0 0 0 6.4 20h3.2v-1.6H6.4a.8.8 0 0 1-.8-.8v-7.2h12.8V8A2.4 2.4 0 0 0 16 5.6v-.8a.8.8 0 0 0-1.6 0v.8H8v-.8z" fill="#000"/> 1201 <path d="M8 12a.8.8 0 0 0 0 1.6h4a.8.8 0 0 0 0-1.6H8zM7.2 16.001a.8.8 0 0 1 .8-.8h1.6a.8.8 0 0 1 0 1.6H8a.8.8 0 0 1-.8-.8z" fill="#000"/> 1202 <path fill-rule="evenodd" clip-rule="evenodd" d="M18.165 11.434a.8.8 0 0 0-1.131 0l-5.2 5.2a.8.8 0 0 0-.22.409l-.4 2a.8.8 0 0 0 .942.94l2-.4a.8.8 0 0 0 .409-.218l5.2-5.2a.8.8 0 0 0 0-1.131l-1.6-1.6zm-5.029 6.16L16.4 14.33l.469.468-3.263 3.263-.586.117.117-.585z" fill="#000"/> 1203 </svg> 1204 </span> 1205 </a> 1206 `, 1207 ) 1208 .show(); 1209 }; 1210 1211 app.updatePriceBreakdown = () => { 1212 const rateplan = quote.rateplans.find( 1213 (item) => item.id === ratePlanSelected, 1214 ); 1215 1216 const total = rateplan.total; 1217 const discount = rateplan.discount || 0; 1218 const totalRent = rateplan.total_rent; 1219 const totalBeforeTax = rateplan.total - rateplan.total_taxes; 1220 const discountLineItems = rateplan.invoice_items.filter((item) => 1221 ["PROMOTION", "DISCOUNT"].includes(item.type), 1222 ); 1223 1224 app.renderTotal(total, totalBeforeTax, discountLineItems); 1225 1226 // const checkin = quote.checkin; 1227 // const checkout = quote.checkout; 1228 // const totalNights = calendarUtils.calcNights(checkin, checkout); 1229 const totalNights = quote.nights; 1230 1231 app.renderNights(totalNights); 1232 1233 if (options.show_rent) { 1234 if ("average_price" === settings.price_with_dates) { 1235 if (options.total_after_tax) { 1236 app.renderRentAfterDate(total, totalNights, discount); 1237 } else { 1238 app.renderRentAfterDate(totalBeforeTax, totalNights, discount); 1239 } 1240 } else if ("average_rent" === settings.price_with_dates) { 1241 app.renderRentAfterDate(totalRent, totalNights, discount); 1242 } 1243 } 1244 1245 if (options.sticky) { 1246 app.updateStickyPrice(); 1247 app.updateStickyDates(quote.checkin, quote.checkout); 1248 } 1249 1250 if (options.price_modal) { 1251 app.updatePriceDetailsModal(rateplan, total, totalBeforeTax); 1252 } 1253 }; 1254 1255 app.updatePriceDetailsModal = (rateplan, total, subTotal = 0) => { 1256 const $modal = $(selector.priceDetailsModal); 1257 const $tbody = $modal.find("table tbody"); 1258 $tbody.empty(); 1259 1260 let rent_rows = "", 1261 fee_rows = "", 1262 manual_rows = "", 1263 tax_rows = ""; 1264 refundable_rows = ""; 1265 1266 rateplan.invoice_items.forEach(function (item) { 1267 let label = item.title; 1268 if (settings.rent_item_types.includes(item.type)) { 1269 if (!rent_rows) { 1270 rent_rows += `<tr> 1271 <td>${settings.rent_group_label}</td> 1272 <td>${label}</td> 1273 <td class="hk-text-right">${app.price(item.amount)}</td> 1274 </tr>`; 1275 } else { 1276 rent_rows += `<tr> 1277 <td></td> 1278 <td>${label}</td> 1279 <td class="hk-text-right">${app.price(item.amount)}</td> 1280 </tr>`; 1281 } 1282 } 1283 1284 if (settings.fee_item_types.includes(item.type)) { 1285 if (!fee_rows) { 1286 fee_rows += `<tr> 1287 <td>${settings.fees_group_label}</td> 1288 <td>${label}</td> 1289 <td class="hk-text-right">${app.price(item.amount)}</td> 1290 </tr>`; 1291 } else { 1292 fee_rows += `<tr> 1293 <td></td> 1294 <td>${label}</td> 1295 <td class="hk-text-right">${app.price(item.amount)}</td> 1296 </tr>`; 1297 } 1298 } 1299 1300 //manual_rows 1301 if (settings.manual_item_types.includes(item.type)) { 1302 if (!manual_rows) { 1303 manual_rows += `<tr> 1304 <td>${settings.manual_group_label}</td> 1305 <td>${label}</td> 1306 <td class="hk-text-right">${app.price(item.amount)}</td> 1307 </tr>`; 1308 } else { 1309 manual_rows += `<tr> 1310 <td></td> 1311 <td>${label}</td> 1312 <td class="hk-text-right">${app.price(item.amount)}</td> 1313 </tr>`; 1314 } 1315 } 1316 1317 if (settings.tax_item_types.includes(item.type)) { 1318 if (!tax_rows) { 1319 tax_rows += `<tr> 1320 <td>${settings.taxes_group_label}</td> 1321 <td>${label}</td> 1322 <td class="hk-text-right">${app.price(item.amount)}</td> 1323 </tr>`; 1324 } else { 1325 tax_rows += `<tr> 1326 <td></td> 1327 <td>${label}</td> 1328 <td class="hk-text-right">${app.price(item.amount)}</td> 1329 </tr>`; 1330 } 1331 } 1332 1333 if (settings.refundable_item_types.includes(item.type)) { 1334 if (!refundable_rows) { 1335 refundable_rows += `<tr> 1336 <td>${settings.others_group_label}</td> 1337 <td>${label}</td> 1338 <td class="hk-text-right">${app.price(item.amount)}</td> 1339 </tr>`; 1340 } else { 1341 refundable_rows += `<tr> 1342 <td></td> 1343 <td>${label}</td> 1344 <td class="hk-text-right">${app.price(item.amount)}</td> 1345 </tr>`; 1346 } 1347 } 1348 }); 1349 1350 $tbody.append(rent_rows); 1351 $tbody.append(fee_rows); 1352 if (manual_rows) { 1353 $tbody.append(manual_rows); 1354 } 1355 if (refundable_rows) { 1356 $tbody.append(refundable_rows); 1357 } 1358 1359 if ("1" === settings.show_subtotal && subTotal) { 1360 $tbody.append(` 1361 <tr class="hk-background-muted"> 1362 <td class="hk-text-bold">${settings.subtotal_group_label}</td> 1363 <td></td> 1364 <td class="hk-text-right hk-text-bold">${app.price(subTotal)}</td> 1365 </tr>`); 1366 } 1367 1368 $tbody.append(tax_rows); 1369 1370 $tbody.append(` 1371 <tr class="hk-background-muted"> 1372 <td class="hk-text-bold">${settings.total_group_label}</td> 1373 <td></td> 1374 <td class="hk-text-right hk-text-bold">${app.price(total)}</td> 1375 </tr>`); 1376 }; 1377 1378 app.createQuote = ({ checkin, checkout, guests, coupon }) => { 1379 $widget.find(selector.bookingOverview).hide(); 1380 $widget.find(selector.quoteNotice).empty(); 1381 $widget.find(".discount-items").hide(); 1382 $widget.find(".coupon-row").hide(); 1383 $widget.find(".rateplans").hide(); 1384 $widget.find(selector.priceLoader).show(); 1385 1386 $widget.trigger("updating"); 1387 1388 const totalNights = calendarUtils.calcNights(checkin, checkout); 1389 app.renderNights(totalNights); 1390 1391 $.post(`${settings.plugin_api}quotes`, { 1392 checkin, 1393 checkout, 1394 guests, 1395 coupon, 1396 property_id: options.property_id, 1397 traffic_source: options.traffic_source, 1398 }) 1399 .done(function (response) { 1400 $widget.find(selector.bookingOverview).show(); 1401 1402 // store quote data in cache. 1403 quote = response; 1404 if (!quote.rateplans) { 1405 quote.rateplans = []; 1406 } 1407 1408 app.renderRateplans(); 1409 1410 if (quote.available_addons) { 1411 app.renderAddons(); 1412 } 1413 1414 if (options.show_coupon) { 1415 app.renderCoupon(); 1416 } 1417 1418 app.fireCouponEvent(); 1419 1420 $widget.trigger("available"); 1421 }) 1422 .fail(function (xhr) { 1423 pendingCouponEvent = null; 1424 1425 if (options.show_rent) { 1426 app.hideRent(); 1427 } 1428 1429 let error = settings.internal_error; 1430 if (xhr.responseJSON && xhr.responseJSON.message) { 1431 error = xhr.responseJSON.message; 1432 } 1433 1434 app.quoteNotice(error, "danger"); 1435 1436 if (options.show_coupon && coupon) { 1437 $widget.find(".coupon-row").show(); 1438 } 1439 1440 $widget.trigger("unavailable", [error]); 1441 }) 1442 .always(function () { 1443 $widget.find(selector.widgetBody).show(); 1444 $widget.find(selector.widgetLoader).hide(); 1445 $widget.find(selector.priceLoader).hide(); 1446 }); 1447 }; 1448 1449 app.renderRateplans = () => { 1450 const $rateplans = $widget.find(".rateplans"); 1451 $rateplans.empty(); 1452 1453 if (quote.rateplans.length > 1) { 1454 quote.rateplans.forEach((item) => { 1455 let prText = ""; 1456 1457 if (item.priceAdjustment) { 1458 const prAd = item.priceAdjustment; 1459 if (prAd.amount !== 0) { 1460 if ("increase" === prAd.direction) { 1461 if (prAd.type === "percent") { 1462 prText = `+${prAd.amount}%`; 1463 } else { 1464 prText = "+" + app.price(prAd.amount); 1465 } 1466 } else { 1467 if (prAd.type === "percent") { 1468 prText = `-${prAd.amount}%`; 1469 } else { 1470 prText = "-" + app.price(prAd.amount); 1471 } 1472 } 1473 1474 if (prText) { 1475 prText = ` (${prText})`; 1476 } 1477 } 1478 } 1479 1480 $rateplans.append(` 1481 <label class="hk-flex hk-flex-middle hk-margin-small rateplan">
1482 <span class="hk-flex-none"> 1483 <input type="radio" name="rateplan" class="hk-radio" value="${ 1484 item.id 1485 }" /> 1486 </span> 1487 <span class="hk-flex-auto hk-margin-small-left"> 1488 <div><span class="hk-text-bold">${ 1489 item.title || item.name 1490 }</span>${prText}</div> 1491 ${ 1492 item.description 1493 ? `<div class="hk-text-small">${item.description}</div>` 1494 : "" 1495 } 1496 </span> 1497 </label> 1498 `); 1499 }); 1500 1501 $rateplans.show(); 1502 1503 if (ratePlanSelected) { 1504 $rateplans 1505 .find(`input[value="${ratePlanSelected}"]`) 1506 .prop("checked", true) 1507 .trigger("change"); 1508 } else { 1509 $rateplans 1510 .find("input:first") 1511 .prop("checked", true) 1512 .trigger("change"); 1513 } 1514 } else if (quote.rateplans.length === 1) { 1515 $rateplans.append( 1516 `<input type="hidden" name="rateplan" class="hk-radio" value="${quote.rateplans[0].id}" />`, 1517 ); 1518 $rateplans.find("input:first").trigger("change"); 1519 } 1520 }; 1521 1522 app.rateplanHasStayDiscount = () => { 1523 if (!quote || !Array.isArray(quote.rateplans)) { 1524 return false; 1525 } 1526 1527 const rateplan = quote.rateplans.find( 1528 (item) => item.id === ratePlanSelected, 1529 ); 1530 1531 if (!rateplan || !Array.isArray(rateplan.invoice_items)) { 1532 return false; 1533 } 1534 1535 return rateplan.invoice_items.some( 1536 (item) => "STAY_DISCOUNT" === item.type, 1537 ); 1538 }; 1539 1540 app.shouldHideCoupon = () => { 1541 if (!options.hide_coupon_on_stay_discount) { 1542 return false; 1543 } 1544 1545 // Never hide an applied coupon's state (e.g. coupon via URL). 1546 if (quote && quote.coupons && quote.coupons.length > 0) { 1547 return false; 1548 } 1549 1550 return app.rateplanHasStayDiscount(); 1551 }; 1552 1553 app.renderCoupon = () => { 1554 if (app.shouldHideCoupon()) { 1555 $widget.find(".coupon-row").hide(); 1556 app.couponNotice(); 1557 return; 1558 } 1559 1560 $widget.find(".coupon-row").show(); 1561 1562 if (quote.coupons && quote.coupons.length > 0) { 1563 const coupon = quote.coupons[0]; 1564 1565 let discountText = ""; 1566 1567 if (coupon.type === "PERCENT") { 1568 discountText = `${coupon.discount}%`; 1569 } else { 1570 discountText = app.price(Math.abs(coupon.discount)); 1571 } 1572 1573 $widget.find("#have_coupon").prop("checked", true).trigger("change"); 1574 $widget.find("#coupon").val(coupon.code); 1575 app.couponNotice( 1576 settings.coupon_discount_applied.replace("{amount}", discountText), 1577 "success", 1578 ); 1579 } else { 1580 $widget.find("#have_coupon").prop("checked", false).trigger("change"); 1581 $widget.find("#coupon").val(""); 1582 app.couponNotice(); 1583 } 1584 }; 1585 1586 app.couponNotice = (text = "", type = "primary") => { 1587 if (text) { 1588 $widget 1589 .find(selector.couponNotice) 1590 .html(`<div class="hk-alert hk-alert-${type}">${text}</div>`); 1591 } else { 1592 $widget.find(selector.couponNotice).empty(); 1593 } 1594 }; 1595 1596 app.travelInsuranceNotice = (text = "", type = "primary") => { 1597 const $notice = $widget.find(".travel-insurance-notice"); 1598 1599 if (text) { 1600 $notice.html(`<div class="hk-alert hk-alert-${type}">${text}</div>`); 1601 } else { 1602 $notice.empty(); 1603 } 1604 }; 1605 1606 app.removeLangFromUrl = (url) => { 1607 const parsed = new URL(url); 1608 1609 // 1. Force base hostname without language subdomains 1610 // If hostname is like fr.fridaysflats.com â take the main domain 1611 const hostParts = parsed.hostname.split("."); 1612 if (hostParts.length > 2) { 1613 // remove first part only if it's a language code 1614 const possibleLang = hostParts[0]; 1615 if (settings.available_languages.includes(possibleLang)) { 1616 hostParts.shift(); 1617 parsed.hostname = hostParts.join("."); 1618 } 1619 } 1620 1621 // 2. Remove `/xx/` language folder before `/wp-json/`
1622 const segments = parsed.pathname.split("/").filter(Boolean); 1623 const hasTrailingSlash = parsed.pathname.endsWith("/"); 1624 1625 if (segments.length >= 2) { 1626 const first = segments[0]; 1627 if (settings.available_languages.includes(first)) { 1628 segments.shift(); // remove language 1629 } 1630 } 1631 1632 parsed.pathname = "/" + segments.join("/"); 1633 if (hasTrailingSlash) { 1634 parsed.pathname += "/"; 1635 } 1636 1637 return parsed.toString(); 1638 }; 1639 1640 app.applyCoupon = (quoteId, coupon) => { 1641 $widget.trigger("updating"); 1642 1643 app.couponNotice("Applying coupon, please wait.", "warning"); 1644 $widget.find("#apply-coupon-btn").prop("disabled", true); 1645 1646 // @see https://github.com/hudsoncreativestudio/homerunner-plugin/issues/549 1647 let url = `${settings.plugin_api}quotes/${quoteId}`; 1648 if ("1" === settings.force_direct_quote_endpoint) { 1649 // Remove language code from URL (subdomain and path) when using direct endpoint 1650 url = app.removeLangFromUrl(url); 1651 } 1652 1653 $.ajax({ 1654 method: "PUT", 1655 url: url, 1656 data: { 1657 coupon, 1658 }, 1659 }) 1660 .done(function () { 1661 app.couponNotice("Coupon Applied.", "warning"); 1662 1663 if ("1" === options.coupon_reload) { 1664 window.location.reload(); 1665 } else { 1666 pendingCouponEvent = "applied"; 1667 app.refreshQuote(); 1668 } 1669 }) 1670 .fail(function (xhr) { 1671 if (xhr.responseJSON && xhr.responseJSON.message) { 1672 app.couponNotice(xhr.responseJSON.message, "danger"); 1673 } else { 1674 app.couponNotice(settings.internal_error, "danger"); 1675 } 1676 $widget.trigger("available"); 1677 }) 1678 .always(function () { 1679 $widget.find("#apply-coupon-btn").prop("disabled", false); 1680 }); 1681 }; 1682 1683 app.refreshQuote = (id = null) => { 1684 if (!id) { 1685 id = quote.id; 1686 } 1687 1688 $widget.find(selector.bookingOverview).hide(); 1689 $widget.find(selector.priceLoader).show(); 1690 $widget.trigger("updating"); 1691 1692 $.get(`${settings.plugin_api}quotes/${id}?_=${new Date().getTime()}`) 1693 .done(function (response) { 1694 $widget.find(selector.quoteNotice).empty(); 1695 $widget.find(selector.bookingOverview).show(); 1696 1697 // store quote data in cache. 1698 quote = response; 1699 if (!quote.rateplans) { 1700 quote.rateplans = []; 1701 } 1702 1703 if ($widget.find("#checkin").length > 0) { 1704 $widget.find("#checkin").val(quote.checkin); 1705 } 1706 if ($widget.find("#checkout").length > 0) { 1707 $widget.find("#checkout").val(quote.checkout); 1708 } 1709 if ($widget.find("#guests").length > 0) { 1710 $widget.find("#guests").val(quote.guests); 1711 } 1712 1713 app.renderRateplans(); 1714 app.renderCoupon(); 1715 1716 if (quote.available_addons) { 1717 app.renderAddons(); 1718 } 1719 1720 app.fireCouponEvent(); 1721 1722 $widget.trigger("available"); 1723 }) 1724 .fail(function (xhr) { 1725 pendingCouponEvent = null; 1726 1727 if (options.show_rent) { 1728 app.hideRent(); 1729 } 1730 1731 if (xhr.responseJSON && xhr.responseJSON.message) { 1732 app.quoteNotice(xhr.responseJSON.message, "warning"); 1733 } else { 1734 app.quoteNotice(settings.internal_error, "warning"); 1735 } 1736 1737 $widget.trigger("unavailable"); 1738 }) 1739 .always(function () { 1740 $widget.find(selector.widgetBody).show(); 1741 $widget.find(selector.widgetLoader).hide(); 1742 $widget.find(selector.priceLoader).hide(); 1743 }); 1744 }; 1745 1746 app.postAddon = (quoteId, addonId, quantity) => { 1747 return $.ajax({ 1748 method: "POST", 1749 url: `${settings.plugin_api}quotes/${quoteId}/addons`, 1750 data: { 1751 addon_id: addonId, 1752 quantity: quantity, 1753 }, 1754 }); 1755 }; 1756 1757 app.addAddon = (quoteId, addonId, quantity) => { 1758 $widget.trigger("updating"); 1759
1760 app.couponNotice(settings.updating_addon, "warning"); 1761 1762 app 1763 .postAddon(quoteId, addonId, quantity) 1764 .done(function () { 1765 app.couponNotice(settings.addon_updated, "success"); 1766 app.refreshQuote(); 1767 }) 1768 .fail(function (xhr) { 1769 if (xhr.responseJSON && xhr.responseJSON.message) { 1770 app.couponNotice(xhr.responseJSON.message, "danger"); 1771 } else { 1772 app.couponNotice(settings.internal_error, "danger"); 1773 } 1774 $widget.trigger("available"); 1775 }) 1776 .always(function () {}); 1777 }; 1778 1779 app.isTravelCoverage = (item) => { 1780 return ( 1781 !!item && 1782 typeof item.target_fee === "string" && 1783 travelCoverageFeeTypes.indexOf(item.target_fee) !== -1 1784 ); 1785 }; 1786 1787 /** 1788 * Addons flagged `is_visible: false` stay in the quote but are never listed. 1789 */ 1790 app.isAddonVisible = (item) => { 1791 return !!item && false !== item.is_visible; 1792 }; 1793 1794 app.addonPrice = (item) => { 1795 return typeof item.amount_type === "string" && 1796 "percentage" === item.amount_type 1797 ? item.amount + "%" 1798 : app.price(item.amount); 1799 }; 1800 1801 /** 1802 * Amount an addon costs against the selected rate plan, null when the quote 1803 * does not carry the computed map (older cached quote bodies). 1804 */ 1805 app.rateplanAddonAmount = (item) => { 1806 const rateplans = quote.rateplans || []; 1807 const rateplan = 1808 rateplans.find((plan) => plan.id === ratePlanSelected) || rateplans[0]; 1809 const amounts = (rateplan && rateplan.addon_amounts) || {}; 1810 const amount = amounts[item.id]; 1811 1812 return amount === undefined || amount === null ? null : parseFloat(amount); 1813 }; 1814 1815 /** 1816 * Id of the travel coverage addon currently applied to the quote, 0 when none. 1817 */ 1818 app.selectedTravelCoverageId = (coverage) => { 1819 const selected = (quote.selected_addons || []).find((item) => { 1820 return ( 1821 coverage.some((addon) => addon.id === item.id) && 1822 parseInt(item.quantity, 10) > 0 1823 ); 1824 }); 1825 1826 return selected ? selected.id : 0; 1827 }; 1828 1829 /** 1830 * Travel coverage always shows a currency amount, never the raw percentage. 1831 */ 1832 app.travelCoveragePrice = (item) => { 1833 const amount = app.rateplanAddonAmount(item); 1834 1835 return null === amount ? app.addonPrice(item) : app.price(amount); 1836 }; 1837 1838 /** 1839 * Travel coverage rows render as an exclusive radio group, not as steppers. 1840 */ 1841 app.renderTravelInsurance = (coverage) => { 1842 const $section = $widget.find(".travel-insurance"); 1843 1844 if (!$section.length) { 1845 return; 1846 } 1847 1848 $section.find(".travel-insurance-items").empty(); 1849 1850 if (!coverage.length || "1" !== options.show_addons) { 1851 $section.hide(); 1852 return; 1853 } 1854 1855 $section.show(); 1856 1857 const selectedId = app.selectedTravelCoverageId(coverage); 1858 1859 coverage.forEach((item) => { 1860 $section.find(".travel-insurance-items").append(` 1861 <label class="travel-insurance-option"> 1862 <input class="travel-insurance-input" type="radio" name="${travelInsuranceGroup}" 1863 value="${item.id}" ${selectedId === item.id ? "checked" : ""} /> 1864 <span class="travel-insurance-label"> 1865 <span class="travel-insurance-name">${item.name}</span> 1866 ${ 1867 item.description && item.description !== item.name 1868 ? `<span class="travel-insurance-description">${item.description}</span>` 1869 : "" 1870 } 1871 </span> 1872 <span class="travel-insurance-price">${app.travelCoveragePrice( 1873 item, 1874 )}</span> 1875 </label> 1876 `); 1877 }); 1878 1879 $section.find(".travel-insurance-items").append(` 1880 <label class="travel-insurance-option"> 1881 <input class="travel-insurance-input" type="radio" name="${travelInsuranceGroup}" 1882 value="0" ${selectedId ? "" : "checked"} /> 1883 <span class="travel-insurance-label"> 1884 <span class="travel-insurance-name">${settings.travel_insurance_none}</span> 1885 </span> 1886 </label> 1887 `); 1888 }; 1889 1890 /** 1891 * Apply a travel coverage addon, removing the previously selected one first. 1892 * `addonId` 0 means "no insurance". 1893 */ 1894 app.setTravelInsurance = (addonId) => { 1895 const coverage = (quote.available_addons || []).filter( 1896 app.isTravelCoverage, 1897 ); 1898 const currentId = app.selectedTravelCoverageId(coverage); 1899 1900 if (currentId === addonId) { 1901 return; 1902 } 1903 1904 $widget.find(".travel-insurance-input").prop("disabled", true); 1905 $widget.trigger("updating"); 1906 app.travelInsuranceNotice( 1907 addonId 1908 ? settings.adding_travel_insurance 1909 : settings.removing_travel_insurance, 1910 "warning", 1911 ); 1912 1913 const onFail = (xhr) => { 1914 if (xhr.responseJSON && xhr.responseJSON.message) { 1915 app.travelInsuranceNotice(xhr.responseJSON.message, "danger"); 1916 } else { 1917 app.travelInsuranceNotice(settings.internal_error, "danger"); 1918 } 1919 1920 // Re-read the quote so the radios match what is actually applied. 1921 app.refreshQuote(); 1922 }; 1923 1924 const applySelected = () => { 1925 if (!addonId) { 1926 app.travelInsuranceNotice(); 1927 app.refreshQuote(); 1928 return; 1929 } 1930 1931 app 1932 .postAddon(quote.id, addonId, 1) 1933 .done(() => { 1934 app.travelInsuranceNotice(); 1935 app.refreshQuote(); 1936 }) 1937 .fail(onFail); 1938 }; 1939 1940 if (currentId) { 1941 app.postAddon(quote.id, currentId, 0).done(applySelected).fail(onFail); 1942 } else { 1943 applySelected(); 1944 } 1945 }; 1946 1947 app.renderAddons = () => { 1948 $widget.find(".addon-items").empty(); 1949 1950 const available = (quote.available_addons || []).filter( 1951 app.isAddonVisible, 1952 ); 1953 const coverage = available.filter(app.isTravelCoverage); 1954 const addonItems = available.filter((item) => !app.isTravelCoverage(item)); 1955 1956 app.renderTravelInsurance(coverage); 1957 1958 // return if empty. 1959 if (!addonItems.length) { 1960 $widget.find(".addons").hide(); 1961 return; 1962 } 1963 1964 if ("1" !== options.show_addons) { 1965 $widget.find(".addons").hide(); 1966 return; 1967 } 1968 1969 $widget.find(".addons").show(); 1970
1971 addonItems.forEach((item) => { 1972 let qty = 0; 1973 const isAdded = quote.selected_addons.find( 1974 (addon) => addon.id === item.id, 1975 ); 1976 if (isAdded) { 1977 qty = isAdded.quantity; 1978 } 1979 1980 const min = 0; 1981 const max = item.max_qty || 1; 1982 const price = app.addonPrice(item); 1983 1984 $widget.find(".addon-items").append(` 1985 <div class="addon"> 1986 <a class="hk-link-text addon-name" href="#" role="button">${item.name}</a> 1987 <div hk-drop="pos: left-center; auto-update: false;" class="addon-description-container"> 1988 <div class="hk-card hk-card-default hk-card-small hk-card-body hk-margin-remove-first-child addon-description"> 1989 <div class="hk-panel hk-margin-top">${item.description || ""}</div> 1990 </div> 1991 </div> 1992 <span class="addon-price">${price}</span> 1993 <span class="addon-control"> 1994 <button class="addon-reduce-btn" type="button" ${min === qty ? "disabled" : ""}> 1995 <span hk-icon="icon: minus; ratio: .5"></span> 1996 </button> 1997 <span class="addon-input" data-id="${item.id}" >${qty}</span> 1998 <button class="addon-increase-btn" type="button" ${ 1999 max === qty ? "disabled" : "" 2000 }> 2001 <span hk-icon="icon: plus; ratio: .5"></span> 2002 </button> 2003 </span> 2004 </div> 2005 `); 2006 }); 2007 }; 2008 2009 app.price = (price) => { 2010 return $.homelocalPrice(price, options.currency, false); 2011 }; 2012 2013 app.getFormattedDate = (element) => { 2014 var date; 2015 try { 2016 date = element.value; 2017 } catch (error) { 2018 date = null; 2019 } 2020 2021 return date; 2022 }; 2023 2024 app.resetWidget = () => { 2025 quote = null; 2026 2027 $widget.find(selector.bookingOverview).hide(); 2028 $widget.find(selector.quoteNotice).empty(); 2029 $widget.find("#checkin,#checkin_placeholder").val(""); 2030 $widget.find("#checkout,#checkout_placeholder").val(""); 2031 $widget.find(".discount-items").hide(); 2032 $widget.find(".coupon-row").hide(); 2033 $widget.find(".rateplans").hide(); 2034 2035 $widget 2036 .find(selector.checkoutBtn) 2037 .prop("disabled", false) 2038 .text(settings.select_dates) 2039 .show(); 2040 2041 if (options.sticky) { 2042 $sticky 2043 .find(selector.stickyBtn) 2044 .prop("disabled", false) 2045 .text(settings.select_dates); 2046 $sticky.find(selector.stickyTotalNights).show(); 2047 $sticky.find(selector.stickyDates).hide(); 2048 $sticky.find(selector.stickyPrice).hide(); 2049 $sticky.find(selector.stickyNotice).empty().hide(); 2050 } 2051 2052 app.showNoDatesRent(); 2053 2054 if (options.show_min_stay) { 2055 app.renderMinimumNights(); 2056 } else { 2057 app.hideMinimumNights(); 2058 } 2059 2060 app.updateUrl(); 2061 }; 2062 2063 app.init(); 2064 2065 // Public API for add-on scripts: $widget.data("homelocalBookingWidget").removeCoupon() 2066 $widget.data("homelocalBookingWidget", app); 2067 2068 $(document).on("homelocal_booking_widget_reset", app.resetWidget); 2069 2070 // update nights when checkin selected. 2071 $(document).on( 2072 "homelocal_availability_calendar_set_checkin_" + options.property_id, 2073 function (e, day) { 2074 if (options.show_min_stay) { 2075 setTimeout(function () { 2076 app.renderMinimumNights(calendarUtils.getMinstay(day)); 2077 }, 2); 2078 } 2079 }, 2080 ); 2081 }; 2082})(jQuery, homelocal_booking_widget_settings);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.