1; 2(function ($, window, document, undefined) { 3 "use strict"; 4 var pluginName = "amReservePicker", 5 defaults = { 6 apiServer: false, 7 client: false, 8 timezone: '', 9 mode: 0, 10 roomId: false, 11 unmediatedOnly: false, 12 mediatedOnly: false, 13 selectedDate: false, 14 selectedLocation: false, 15 selectedRoom: false, 16 classId: 0, 17 sessionId: '', 18 chargingMode: 0, 19 testMode: 0, 20 payment_currency: 'USD', 21 ezproxy: {}, 22 embeddedMode: false, 23 parentOrigin: '', 24 enableStaffNotes: false 25 }; 26 // The actual plugin constructor 27 function Plugin(element, options) { 28 this.clientSettings = {}; 29 this.locations = []; 30 this.selectedLocations = []; 31 this.locationsById = {}; 32 this.element = element; 33 this.$element = $(element); 34 this.settings = $.extend(true, {}, defaults, options); 35 this._defaults = defaults; 36 this._name = pluginName; 37 this.initComplete = false; 38 this.paused = false; 39 this.locationOptions = {}; 40 this._updating = false; 41 this.groupTypeSelect = null; 42 this.responsiveGroupTypeSelect = null; 43 this.locationOptions = { 44 only_with_external_rooms: 1 45 }; 46 this.storage = window.sessionStorage || false; 47 this._cartOpen = false; 48 this.cartHolder = null; 49 this.cartButton = null; 50 this.cartPanel = null; 51 this.cartList = null; 52 this.cartFooter = null; 53 this.cartTotal = null; 54 this.cartCheckout = null; 55 this.cartCount = null; 56 this.pendingFocus = null; 57 this.liveRegion = null; 58 this._embeddedHeightFrame = null; 59 this._lastEmbeddedHeight = 0; 60 this._embeddedHeightObserver = null; 61 this._embeddedResizeObserver = null; 62 this.init(); 63 } 64 65 $.extend(true, Plugin.prototype, $.fn['amShared']); 66 $.extend(Plugin.prototype, { 67 init: function () { 68 var self = this; 69 Promise.all([ 70 self._getJSON(self.settings.apiServer + '/v1/' + self.settings.client + '/settings') 71 ]).then(function (result) { 72 self.clientSettings = result[0] || {}; 73 var activeBlockReservationSetting; 74 if (typeof self.clientSettings.rooms_allow_active_block_reservation !== "undefined") { 75 activeBlockReservationSetting = self.clientSettings.rooms_allow_active_block_reservation; 76 } else if ( 77 self.settings.clientSettings && 78 typeof self.settings.clientSettings.rooms_allow_active_block_reservation !== "undefined" 79 ) { 80 activeBlockReservationSetting = 81 self.settings.clientSettings.rooms_allow_active_block_reservation; 82 } 83 if (typeof activeBlockReservationSetting !== "undefined") { 84 self.clientSettings.rooms_allow_active_block_reservation = 85 activeBlockReservationSetting === true; 86 } 87 var unmediatedParam = ''; 88 if (self.settings.unmediatedOnly) { 89 self.locationOptions.only_with_unmediated = 1; 90 unmediatedParam = '&only_unmediated=1'; 91 } 92 93 if (self.settings.mode !== 1) { 94 self.locationOptions.only_with_patron_rooms = 0; 95 } 96 97 self.locationOptions.mode = self.settings.mode; 98 var selectedClass = self.clientSettings.rooms_group_billing_remember ? self._readCookie('selectedClass' + self.settings.client) : false; 99 // CH 14359 - Check if the classId is already set. If it is then it is being passed in directly 100 // Also check to make sure that selected class isn't being set by the cookie. 101 if (selectedClass) { 102 self.locationOptions.class_id = selectedClass; 103 } 104 105 Promise.all([ 106 self._getJSON(self.settings.apiServer + '/v1/' + self.settings.client + '/locations', self.locationOptions), 107 self._getJSON(self.settings.apiServer + '/v2/' + self.settings.client + '/userclasses?mode=' + self.settings.mode + '&locations=1' + unmediatedParam) 108 ]).then(function (result) { 109 110 self.locations = result[0]; 111 self.userclasses = result[1]; 112 113 self.locationsById = {}; 114 self.locations.forEach(function (location) { 115 self.locationsById[location.id] = location; 116 }); 117 118 self.bookings = self.storage && self.clientSettings.rooms_cart_enabled ? JSON.parse(self.storage.getItem('communicoReserveCart') || '[]') : []; 119 self._buildInterface(); 120 self._setupEmbeddedBridge(); 121 self._bindEvents(); 122 self._updateLocations(); 123 self.initComplete = true; 124 125 // Should work 126 if (self.settings.classId > 0) { 127 if (typeof self.groupTypeSelect !== 'undefined') { 128 if (self.groupTypeSelect.is(":visible")) { 129 self.groupTypeSelect.val(self.settings.classId); 130 } 131 } 132 } 133 134 self._update(); 135 }).catch(function (err) { 136 console.log(err); 137 }); 138 }).catch(function (err) { 139 console.log(err); 140 }); 141 }, 142 _buildInterface: function () { 143 var self = this; 144 var now = moment(); //TODO: needs to use timezone 145 146 var selectedBranches = this._readCookie('reserveBranches'); 147 148 if (this.settings.selectedLocation) { 149 selectedBranches = [this.settings.selectedLocation]; 150 }
151 this.holder = $('<div class="amnp-holder amnp-holder-v3"></div>').appendTo(this.element); 152 if (this.clientSettings.rooms_cart_enabled) { 153 this.cartHolder = $('<div class="amnp-picker-cart-holder"></div>').appendTo(this.holder); 154 var cartPanelId = 'amnp-picker-cart-' + Math.random().toString(36).slice(2); 155 this.cartButton = $('<button type="button" class="amnp-picker-cart-button"><i class="fa fa-shopping-cart link"></i> Room basket (<span class="basket-total">0</span>)</button>') 156 .attr({ 157 'aria-haspopup': 'true', 158 'aria-expanded': 'false', 159 'aria-controls': cartPanelId 160 }) 161 .appendTo(this.cartHolder); 162 this.cartCount = this.cartButton.find('.basket-total'); 163 this.cartPanel = $('<div class="amnp-picker-cart" role="region" aria-label="Room basket" aria-hidden="true"></div>') 164 .attr('id', cartPanelId) 165 .hide() 166 .appendTo(this.cartHolder); 167 this.cartList = $('<div class="amnp-picker-cart-list"></div>').appendTo(this.cartPanel); 168 this.cartFooter = $('<div class="amnp-picker-cart-footer"></div>').appendTo(this.cartPanel); 169 this.cartTotal = $('<div class="amnp-picker-cart-total"></div>').appendTo(this.cartFooter); 170 this.cartCheckout = $('<button type="button" class="amnp-picker-cart-checkout btn btn-info"><i class="fa fa-shopping-cart"></i> Reserve</button>').attr('aria-label', 'Reserve rooms in basket').appendTo(this.cartFooter); 171 } 172 if (this.clientSettings.events_single_branch_library) { 173 this.holder.addClass('amnp-single-branch'); 174 } 175 176 this.header = $('<div class="amnp-picker-header"><h1>Reserve a room</h1><p>Choose a date and room then pick a time</p></div>').appendTo(this.holder); 177 this.navigation = $('<div class="amnp-navigation link"></div>').appendTo(this.holder); 178 this.navigationPrev = $('<button type="button" class="amnp-navigation-control amnp-navigation-prev" data-by="-1" aria-label="Previous day"><i class="fa fa-chevron-left" aria-hidden="true"></i></button>').appendTo(this.navigation); 179 this.navigationTitle = $('<span class="amnp-navigation-title" aria-live="polite"></span>').appendTo(this.navigation); 180 this.navigationNext = $('<button type="button" class="amnp-navigation-control amnp-navigation-next" data-by="1" aria-label="Next day"><i class="fa fa-chevron-right" aria-hidden="true"></i></button>').appendTo(this.navigation); 181 182 this.responsiveKey = $('<div class="amnp-responsive-key"></div>').appendTo(this.holder); 183 this.responsiveDatePicker = $('<div class="input-group date"><input type="text" class="form-control" aria-label="Date picker for mobile"><div class="input-group-addon"><span class="am-events"></span></div></div>').appendTo(this.responsiveKey); 184 $('<br>').appendTo(this.responsiveKey); 185 var branchPicker = $('<div class="amnp-choose-branch" />').appendTo(this.responsiveKey); 186 $('<br><h5>Select a branch:</h5>').appendTo(branchPicker); 187 this.responsiveLocationSelect = $('<select aria-label="Mobile Location Select" />').prop('multiple', true).appendTo(branchPicker); 188 this.responsiveLocationSelect.multiselect({ 189 includeSelectAllOption: true, 190 selectAllValue: 'all', 191 buttonWidth: '100%', 192 onChange: function (option, checked, select) { 193 if (!self._updating) { 194 if (self.locations) { 195 if (!checked) { 196 //self.locations.splice(self.locations.indexOf($(option).val()), 1); 197 } else { 198 //self.locations.push($(option).val());) 199 } 200 } 201 self._update(); 202 } 203 }, 204 onSelectAll: function (e) { 205 if (!self._updating) self._update(); 206 //self.responsiveLocationSelect.children('options[value!="all"]').attr('checked', true); 207 }, 208 onDeselectAll: function (e) { 209 if (!self._updating) self._update(); 210 //self.responsiveLocationSelect.children('options[value!="all"]').prop('checked', false); 211 } 212 }); 213 214 this.key = $('<div class="amnp-key"></div>').appendTo(this.holder); 215 if (!this.settings.embeddedMode) { 216 if (this.settings.mode === 1) { 217 this.navigation.sticky({ topSpacing: 80, wrapperClassName: 'sticky-nav' }); 218 this.key.sticky({ topSpacing: 140, bottomSpacing: 50, wrapperClassName: 'sticky-key' }); 219 } else { 220 this.navigation.sticky({ wrapperClassName: 'sticky-nav' }); 221 this.key.sticky({ topSpacing: 60, bottomSpacing: ($('#footer').outerHeight() || 300) + 50, wrapperClassName: 'sticky-key' }); 222 } 223 } 224 225 this.stage = $('<div class="amnp-stage"></div>').appendTo(this.holder);
226 this.loader = $('<div class="amnp-loader"><div class="amnp-spinner"></div></div>').appendTo(this.holder); 227 this.liveRegion = $('<div class="amnp-a11y-live" role="status" aria-live="polite"></div>').css({ 228 position: 'absolute', 229 width: '1px', 230 height: '1px', 231 margin: '-1px', 232 border: 0, 233 padding: 0, 234 clip: 'rect(0 0 0 0)', 235 overflow: 'hidden' 236 }).appendTo(this.holder); 237 238 if (this.clientSettings.rooms_group_billing_enabled) { 239 this.groupPickerHolder = $('<div class="amnp-group-picker"><span>Booking type</span><select class="form-control" aria-label="Booking type selector"></select></div>').appendTo(this.key); 240 this.groupTypeSelect = $('select', this.groupPickerHolder); 241 242 var patronClass = false; 243 var staffClass = false; 244 this.userclasses.forEach(function (cls) { 245 var t = parseInt(cls.type, 10); 246 if (self.settings.mode !== 1 && t == 0) return; 247 $('<option value="' + cls.id + '">' + cls.name + '</option>').data('details', cls).appendTo(self.groupTypeSelect); 248 if (t === 1) { 249 patronClass = cls.id; 250 } 251 252 if (t === 0) { 253 staffClass = cls.id; 254 } 255 }); 256 257 $('<br><h5>Booking type:</h5>').appendTo(this.responsiveKey); 258 this.responsiveGroupTypeSelect = this.groupTypeSelect.clone(true).appendTo(this.responsiveKey); 259 var selectedClass = this.clientSettings.rooms_group_billing_remember ? this._readCookie('selectedClass' + this.settings.client) : false; 260 261 // CH 14359 - Check if the classId is already set. If it is then it is being passed in directly 262 // Also check to make sure that selected class isn't being set by the cookie. 263 if (parseInt(this.settings.classId) > 0 && !selectedClass) selectedClass = this.settings.classId; 264 265 if (this.settings.mode !== 1 && !selectedClass && this.clientSettings.rooms_group_billing_prompt_for_class) { 266 this._promptForClass(); 267 } else { 268 this.groupTypeSelect.val(parseInt(selectedClass || patronClass)); 269 this.responsiveGroupTypeSelect.val(parseInt(selectedClass || patronClass)); 270 } 271 272 if (this.settings.mode === 1 && this.clientSettings.rooms_staff_booking_class_default) { 273 var cls = selectedClass > 0 ? selectedClass : staffClass; 274 this.groupTypeSelect.val(parseInt(cls)); 275 this.responsiveGroupTypeSelect.val(parseInt(cls)); 276 } 277 } 278 279 //add the date picker 280 var datePickerSection = $('<div class="amnp-date-picker"/>').appendTo(this.key); 281 $('<h3>Choose date</h3>').appendTo(datePickerSection); 282 this.datePicker = $('<div/>').val(now.format('MMMM D YYYY')).appendTo(datePickerSection); 283 284 //add the branch picker 285 var branchPickerSection = $('<div class="amnp-choose-branch"/>').appendTo(this.key); 286 287 if (this.settings.roomId) { 288 branchPickerSection.hide(); 289 } 290 $('<h3>Choose branch</h3>').appendTo(branchPickerSection); 291 self.locationSelect = $('<div class="amnp-location-select"/>').appendTo(branchPickerSection); 292 self.toggleAll = $('<label><input class="checkall" type="checkbox">Check all</label>').appendTo(self.locationSelect); 293 }, 294 _reset: function () { 295 296 }, 297 _buildResponsive: function () { 298 var self = this; 299 self.responsiveLocationSelect.empty(); 300 self.locations.forEach(function (location) { 301 self.responsiveLocationSelect.append("<option value='" + location.id + "' data-location='" + location.id + "'>" + location.name + "</option>"); 302 }); 303 304 self.responsiveLocationSelect.multiselect('rebuild'); 305 }, 306 // Reads the saved branch selection back out of its cookie. Returns an array of 307 // string ids, or null when there is nothing usable to read - no cookie, a value 308 // that is not valid JSON, or a value that is not an array. 309 // 310 // _updateLocations() must not turn null into "check everything" - that is the 311 // bug this guards. The no-matching-branches recovery in _update() deliberately 312 // does fall back to every branch, which is a different situation: there the 313 // saved ids were read fine but none of them exist any more. 314 _readSavedBranches: function () { 315 var raw = this._readCookie('reserveBranches'); 316 if (!raw) return null; 317 var saved; 318 try { 319 saved = JSON.parse(raw); 320 } catch (e) { 321 return null; 322 } 323 if (!Array.isArray(saved)) return null; 324 return saved.map(function (id) { 325 return String(id); 326 }); 327 }, 328 _updateLocations: function () { 329 var self = this; 330 // The live selection is the source of truth: this runs after every room-list 331 // refresh, so rebuilding from the saved cookie instead would re-check every
332 // branch whenever that cookie can't be read back (blocked or cleared by 333 // browser privacy settings, a consent tool, or a third-party iframe context). 334 // The cookie is only consulted on the first pass, before anything is selected, 335 // so a returning visitor still gets their previous branches back. 336 var selectedBranches = self.selectedLocations && self.selectedLocations.length 337 ? self.selectedLocations.map(function (id) { return String(id); }) 338 : self._readSavedBranches(); 339 if (selectedBranches === null && self.settings.selectedLocation) { 340 selectedBranches = [String(self.settings.selectedLocation)]; 341 } 342 self._updating = true; 343 self._buildResponsive(); 344 self.locationSelect.empty().append(self.toggleAll); 345 var updateLocations = false; 346 for (var l = 0; l < self.locations.length; l++) { 347 var location = self.locations[l]; 348 var box = $('<label><input type="checkbox" data-location="' + location.id + '">' + location.name + '</label>').appendTo(self.locationSelect); 349 if (l === self.locations.length - 1) updateLocations = true; 350 351 // Both sides are normalised to strings: the ids come back from the saved 352 // cookie and from the checkbox attributes as strings, but the locations API 353 // can hand back numbers. 354 if (!selectedBranches || selectedBranches.indexOf(String(location.id)) >= 0) { 355 $('input', box).prop('checked', 'checked'); 356 self.responsiveLocationSelect.multiselect('select', location.id, updateLocations); 357 } else { 358 self.responsiveLocationSelect.multiselect('deselect', location.id, updateLocations); 359 } 360 } 361 362 var lsCnt = self.locationSelect.find('input[data-location]:checked').length; 363 var rlsCnt = self.responsiveLocationSelect.find('option[data-location]:selected').length; 364 365 if (lsCnt >= self.locations.length) { 366 if (!self.initComplete) { 367 $('input', self.locationSelect).prop('checked', 'checked'); 368 self._updating = false; 369 self._update(); 370 return; 371 } else { 372 $('input.checkall', self.locationSelect).prop('checked', 'checked').trigger('change'); 373 } 374 } 375 376 if (lsCnt === 0) { 377 self._updating = false; 378 $('input', self.locationSelect).prop('checked', 'checked'); 379 if (!self.initComplete) { 380 self._update(); 381 } else { 382 $('input.checkall', self.locationSelect).trigger('change'); 383 } 384 return; 385 } else { 386 if (self.locations.length > 1) { 387 if (lsCnt < self.locations.length) { 388 $('input.checkall', self.locationSelect).prop('checked', false); 389 } 390 } else { 391 $('input.checkall', self.locationSelect).prop('checked', 'checked'); 392 } 393 } 394 395 if (rlsCnt === 0) { 396 self.responsiveLocationSelect.multiselect('selectAll', true); 397 } 398 399 self._updating = false; 400 }, 401 _bindEvents: function () { 402 var self = this; 403 self.locationSelect.on('change', '.checkall', function (event) { 404 if (!self.initComplete) return; 405 if ($(this).is(':checked')) { 406 $('input[data-location]', self.locationSelect).prop('checked', 'checked'); 407 self.responsiveLocationSelect.multiselect('selectAll', false); 408 409 } else { 410 $('input[data-location]', self.locationSelect).prop('checked', false); 411 self.responsiveLocationSelect.multiselect('deselectAll', false); 412 } 413 self._update(); 414 }); 415 self.locationSelect.on('change', 'input[data-location]', function (event) { 416 if (!self.initComplete) return; 417 if ($(this).is(':checked')) { 418 self.responsiveLocationSelect.multiselect('select', $(this).attr('data-location'), false); 419 } else { 420 self.responsiveLocationSelect.multiselect('deselect', $(this).attr('data-location'), false); 421 } 422 self._update(); 423 }); 424 425 426 var s = this.settings.selectedDate ? moment(this.settings.selectedDate) : moment(); 427 if (this.clientSettings.rooms_allow_past_bookings && parseInt(this.settings.mode) === 1) { 428 this.responsiveDatePicker.datepicker({ 429 format: 'MM d yyyy', 430 todayHighlight: true, 431 title: "Date picker" 432 }).on('changeDate', function (event) { 433 if (!self.initComplete) return; 434 self.datePicker.datepicker('update', event.date); 435 self._update(); 436 }).datepicker('update', s.startOf('day').toDate()); 437 438 this.datePicker.datepicker({ 439 todayBtn: "linked", 440 orientation: "bottom left", 441 format: 'MM d yyyy', 442 todayHighlight: true, 443 title: "Date picker" 444 }).on('changeDate', function (event) { 445 if (!self.initComplete) return; 446 self.responsiveDatePicker.datepicker('update', event.date); 447 self._update(); 448 }).datepicker('update', s.startOf('day').toDate()); 449 450 451 } else { 452 this.responsiveDatePicker.datepicker({ 453 format: 'MM d yyyy', 454 todayHighlight: true, 455 startDate: new Date(), 456 title: "Date picker" 457 }).on('changeDate', function (event) { 458 if (!self.initComplete) return; 459 self.datePicker.datepicker('update', event.date); 460 self._update(); 461 }).datepicker('update', s.startOf('day').toDate()); 462 463 this.datePicker.datepicker({ 464 todayBtn: "linked", 465 orientation: "bottom left", 466 format: 'MM d yyyy', 467 todayHighlight: true, 468 startDate: new Date(), 469 title: "Date picker" 470 }).on('changeDate', function (event) { 471 if (!self.initComplete) return; 472 self.responsiveDatePicker.datepicker('update', event.date); 473 self._update(); 474 }).datepicker('update', s.startOf('day').toDate()); 475 } 476 477 if (this.clientSettings.rooms_cart_enabled) { 478 this.cartHolder.on('click', '.amnp-booking-summary-asset .amnp-booking-summary-booking-remove', function () { 479 var booking = $(this).closest('.amnp-booking-summary').find('.amnp-booking-summary-total .amnp-booking-summary-booking-remove').data('booking'); 480 var asset = $(this).data('asset'); 481 booking.assets = booking.assets.filter(function (item) { 482 return item !== asset; 483 }); 484 self._updateCart(); 485 }).on('click', '.amnp-booking-summary-total .amnp-booking-summary-booking-remove', function () { 486 var booking = $(this).data('booking');
487 self.rooms.forEach(function (r) { 488 r.bookings = r.bookings.filter(function (b) { 489 return b.t_id !== booking.t_id; 490 }); 491 }); 492 493 self.bookings = self.bookings.filter(function (item) { 494 return item !== booking; 495 }); 496 self._updateCart(); 497 self._render(); 498 self._openCart(false); 499 }).on('keydown', '.amnp-booking-summary-booking-remove', function (event) { 500 var key = event.key; 501 var keyCode = event.which || event.keyCode; 502 if (key === 'Enter' || key === ' ' || key === 'Spacebar' || keyCode === 13 || keyCode === 32) { 503 event.preventDefault(); 504 $(this).trigger('click'); 505 } 506 }); 507 508 this.cartCheckout.on('click', function () { 509 self._checkout(); 510 }); 511 512 this.cartButton.on('click', function () { 513 self._toggleCart(true); 514 }).on('keydown', function (event) { 515 var key = event.key; 516 var keyCode = event.which || event.keyCode; 517 if (key === 'Enter' || key === ' ' || key === 'Spacebar' || keyCode === 13 || keyCode === 32) { 518 event.preventDefault(); 519 self._toggleCart(true); 520 } else if (key === 'Escape' || keyCode === 27) { 521 self._closeCart(true); 522 } 523 }).on('blur', function (event) { 524 if (!self._cartOpen) return; 525 var related = event.relatedTarget || document.activeElement; 526 if (!related || !self.cartHolder.has(related).length) { 527 self._closeCart(false); 528 } 529 }); 530 531 this.cartPanel.on('keydown', function (event) { 532 if (event.key === 'Escape' || event.which === 27) { 533 event.preventDefault(); 534 self._closeCart(true); 535 } 536 }).on('focusout', function (event) { 537 var related = event.relatedTarget || document.activeElement; 538 if (!self._cartOpen) return; 539 if (!related || !self.cartHolder.has(related).length) { 540 self._closeCart(false); 541 } 542 }); 543 544 this.cartHolder.on('mouseenter', function () { 545 if (!self.bookings.length) return; 546 self._openCart(false); 547 }).on('mouseleave', function () { 548 self._closeCart(false); 549 }); 550 } 551 if (this.clientSettings.rooms_group_billing_enabled) { 552 this.groupTypeSelect.on('change', function (event) { 553 if (self.clientSettings.rooms_group_billing_remember) { 554 self._createCookie('selectedClass' + self.settings.client, $(this).val(), 365); 555 } 556 self._update(true); 557 }); 558 559 this.responsiveGroupTypeSelect.on('change', function (event) { 560 self.groupTypeSelect.val($(this).val()); 561 if (self.clientSettings.rooms_group_billing_remember) { 562 self._createCookie('selectedClass' + self.settings.client, $(this).val(), 365); 563 } 564 self._update(true); 565 }); 566 } 567 this.navigation.on('click', '.amnp-navigation-control', function () { 568 var newdate = moment(self.datePicker.datepicker('getDate')).add(parseInt($(this).attr('data-by'), 10), 'day').startOf('day'); 569 if (newdate.isBefore(moment().startOf('day'))) { 570 if (parseInt(self.settings.mode) === 1) { 571 if (!self.clientSettings.rooms_allow_past_bookings) return; 572 } else { 573 return; 574 } 575 } 576 self.datePicker.datepicker('setDate', newdate.toDate()); 577 }); 578 }, 579 _promptForClass: function () { 580 var self = this; 581 var selectClassPopup = $('<div class="amnp-choose-class-popup"><div class="buttons"><button class="btn btn-primary">done</button></div></div>').amPopup({ title: 'Booking Type', width: '300px' }); 582 selectClassPopup.prepend(this.groupTypeSelect.clone()); 583 selectClassPopup.prepend('<div>Please choose the type of room booking</div>'); 584 selectClassPopup.on('change', 'select', function () { 585 if (self.groupTypeSelect.is(':visible')) { 586 self.groupTypeSelect.val($(this).val()).trigger('change'); 587 } 588 589 if (self.responsiveGroupTypeSelect.is(':visible')) { 590 self.responsiveGroupTypeSelect.val($(this).val()).trigger('change'); 591 } 592 }).on('click', '.btn', function () { 593 if (self.clientSettings.rooms_group_billing_remember) { 594 if (self.groupTypeSelect.is(':visible')) { 595 self._createCookie('selectedClass' + self.settings.client, self.groupTypeSelect.val(), 365); 596 } 597 598 if (self.responsiveGroupTypeSelect.is(':visible')) { 599 self._createCookie('selectedClass' + self.settings.client, self.responsiveGroupTypeSelect.val(), 365); 600 } 601 } 602 selectClassPopup.data('plugin_amPopup').close(); 603 }).data('plugin_amPopup').open(); 604 }, 605 _hideCover: function () { 606 this.loader.hide(); 607 }, 608 _setupEmbeddedBridge: function () { 609 var self = this; 610 if (!self.settings.embeddedMode) { 611 return; 612 } 613 window.addEventListener('message', function (event) { 614 var origin = self._getParentOrigin(); 615 if (!origin || event.origin !== origin || event.source !== window.parent) { 616 return; 617 } 618 if (!event.data || typeof event.data !== 'object') { 619 return; 620 } 621 var data = event.data; 622 if (data.source !== 'classic-reserve-host' || data.type !== 'height-request') { 623 return; 624 } 625 self._scheduleHeightPost(true); 626 }); 627 if (typeof MutationObserver !== 'undefined') { 628 self._embeddedHeightObserver = new MutationObserver(function () { 629 self._scheduleHeightPost(); 630 }); 631 self._embeddedHeightObserver.observe(self.holder[0], { 632 attributes: true, 633 childList: true, 634 subtree: true 635 }); 636 } 637 if (typeof ResizeObserver !== 'undefined') { 638 self._embeddedResizeObserver = new ResizeObserver(function () { 639 self._scheduleHeightPost(); 640 }); 641 self._embeddedResizeObserver.observe(self.element); 642 } 643 window.addEventListener('load', function () { 644 self._scheduleHeightPost(); 645 }); 646 }, 647 _getParentOrigin: function () { 648 if (typeof this.settings.parentOrigin === 'string' && this.settings.parentOrigin.length > 0) { 649 return this.settings.parentOrigin; 650 } 651 if (document.referrer && document.referrer.length > 0) { 652 try { 653 return (new URL(document.referrer)).origin; 654 } catch (e) { 655 return ''; 656 } 657 } 658 return ''; 659 }, 660 _postToParent: function (type, payload) { 661 if (!this.settings.embeddedMode || !window.parent || window.parent === window) { 662 return; 663 } 664 var origin = this._getParentOrigin(); 665 if (!origin) { 666 return; 667 } 668 window.parent.postMessage({ 669 source: 'classic-reserve', 670 type: type, 671 payload: payload || {} 672 }, origin); 673 }, 674 _scheduleHeightPost: function (forcePost) { 675 var self = this; 676 if (!self.settings.embeddedMode) { 677 return; 678 } 679 if (self._embeddedHeightFrame) { 680 window.cancelAnimationFrame(self._embeddedHeightFrame); 681 } 682 self._embeddedHeightFrame = window.requestAnimationFrame(function () { 683 self._embeddedHeightFrame = null; 684 var rootElement = self.element && self.element.nodeType === 1 685 ? self.element 686 : (self.element && self.element.length ? self.element[0] : null); 687 var rootElementJQ = rootElement ? $(rootElement) : null; 688 var holderVisible = self.holder && self.holder.length && self.holder.is(':visible'); 689 var holderHeight = holderVisible ? Math.ceil(self.holder.outerHeight(true)) : 0; 690 var holderScrollHeight = holderVisible ? Math.ceil(self.holder[0].scrollHeight || 0
690) : 0; 691 var stageVisible = self.stage && self.stage.length && self.stage.is(':visible'); 692 var stageHeight = stageVisible ? Math.ceil(self.stage.outerHeight(true)) : 0; 693 var stageScrollHeight = stageVisible ? Math.ceil(self.stage[0].scrollHeight || 0) : 0; 694 var keyVisible = self.key && self.key.length && self.key.is(':visible'); 695 var keyHeight = keyVisible ? Math.ceil(self.key.outerHeight(true)) : 0; 696 var keyScrollHeight = keyVisible ? Math.ceil(self.key[0].scrollHeight || 0) : 0; 697 var elementHeight = rootElementJQ ? Math.ceil(rootElementJQ.outerHeight(true) || 0) : 0; 698 var elementScrollHeight = rootElement ? Math.ceil(rootElement.scrollHeight || 0) : 0; 699 var bookerHolder = rootElementJQ ? rootElementJQ.find('.amnp-booker-holder:visible').first() : null; 700 var bookerHeight = bookerHolder && bookerHolder.length ? Math.ceil(bookerHolder.outerHeight(true)) : 0; 701 var bookerScrollHeight = bookerHolder && bookerHolder.length ? Math.ceil(bookerHolder[0].scrollHeight || 0) : 0; 702 var contentHeight = Math.max( 703 holderHeight, 704 holderScrollHeight, 705 stageHeight, 706 stageScrollHeight, 707 keyHeight, 708 keyScrollHeight, 709 elementHeight, 710 elementScrollHeight, 711 bookerHeight, 712 bookerScrollHeight 713 ); 714 contentHeight += 16; 715 if (!contentHeight || (!forcePost && contentHeight === self._lastEmbeddedHeight)) { 716 return; 717 } 718 self._lastEmbeddedHeight = contentHeight; 719 self._postToParent('height-update', { height: contentHeight }); 720 }); 721 }, 722 _showCover: function (spinner) { 723 this.loader.css({ 724 'width': '100%', 725 'height': this.stage.outerHeight() 726 }).show(); 727 $('.amnp-spinner', this.loader).toggle(spinner); 728 }, 729 _render: function () { 730 // console.trace(); 731 var self = this; 732 733 this.stage.empty(); 734 var curDate = moment(this.datePicker.datepicker('getUTCDate')).utc(); 735 var lastBooking = this.bookings.slice(-1)[0]; 736 var hightlightRoom = false;
737 if (lastBooking) { 738 if (moment(lastBooking.start).isSame(curDate, 'd')) { 739 hightlightRoom = parseInt(lastBooking.room.id, 10); 740 } 741 } 742 743 var roomsVisible = false; 744 this.rooms.forEach(function (room) { 745 //CH#92 - Parent/Child Fix 746 var currentBookingClass = false; 747 var currentBookingType = false; 748 749 if (self.groupTypeSelect) { 750 if (self.groupTypeSelect.is(':visible')) { 751 currentBookingClass = self.groupTypeSelect.val(); 752 currentBookingType = parseInt(self.groupTypeSelect.find(":selected").data('details').type); 753 } 754 } 755 756 if (self.responsiveGroupTypeSelect) { 757 if (self.responsiveGroupTypeSelect.is(':visible')) { 758 currentBookingClass = self.responsiveGroupTypeSelect.val(); 759 currentBookingType = parseInt(self.responsiveGroupTypeSelect.find(":selected").data('details').type) 760 } 761 } 762 763 if ((!room.restrictions.viewable && self.settings.mode == 0) || (!room.restrictions.viewable && self.settings.mode === 1 && currentBookingType !== 0)) return; 764 if (self.settings.roomId && parseInt(room.id, 10) !== parseInt(self.settings.roomId, 10)) return; 765 if (room.roomHours[self.CurrentDateString]) { 766 var hours = room.roomHours[self.CurrentDateString]; 767 } else { 768 var hours = self.openingHours[room.location_id][self.CurrentDateString]; 769 } 770 771 if (self.settings.mode !== 1 && hours.open == hours.close) { 772 } else { 773 roomsVisible = true; 774 room.locations = room.locations == null ? [] : room.locations; 775 if (room.locations.indexOf(room.location_id) == -1) room.locations.push(room.location_id); 776 room.locations.forEach(function (location_id) { 777 if (self.selectedLocations.indexOf(location_id) === -1) return; 778 var location = self.locationsById[location_id]; 779 if (typeof location !== 'undefined') { 780 var locationBlock = $('.amnp-location-section[data-locationid=' + location_id + ']', self.stage); 781 if (locationBlock.length == 0) { 782 locationBlock = $('<div class="amnp-location-section" data-location-name="' + location.name + '" data-locationid="' + location_id + '"><div class="amnp-location-section-title"><h3><i class="am-locations"/>' + location.name + '</h3></div></div>').appendTo(self.stage); 783 } 784 var roomDisplay = $('<div/>').appendTo(locationBlock); 785 786 if (hightlightRoom !== false && hightlightRoom === parseInt(room.id, 10)) { 787 roomDisplay.addClass('amnp-cart-added').delay(3000).queue(function () { 788 $(this).removeClass('amnp-cart-added').dequeue(); 789 }); 790 } 791 792 roomDisplay.amReserveRoom({ 793 client: self.settings.client, 794 timezone: self.settings.timezone, 795 apiServer: self.settings.apiServer, 796 multiLocation: self.locationCount > 1, 797 room: room, 798 location: location, 799 hours: hours, 800 singleBranch: self.locations.length === 1, 801 enabledCart: self.clientSettings.rooms_cart_enabled, 802 enablePatronNotes: self.clientSettings.rooms_patron_notes_enabled, 803 enableStaffNotes: self.settings.mode === 1 && self.settings.enableStaffNotes, 804 enableExpectedAttendees: self.clientSettings.rooms_expected_attendees_enabled, 805 expectedAttendeesRequired: self.clientSettings.rooms_expected_attendees_required, 806 useSetupBreakdown: self.clientSettings.rooms_use_setup, 807 adjustSetupBreakdown: self.clientSettings.rooms_adjust_setup, 808 pinlessAuth: self.clientSettings.client_pinless_auth, 809 ecommerceEnabled: self.clientSettings.rooms_ecommerce_enabled, 810 chargingEnabled: self.clientSettings.rooms_charging_enabled, 811 loginRequired: self.clientSettings.rooms_require_login, 812 layoutRequired: self.clientSettings.rooms_layout_required, 813 emailRequired: self.clientSettings.rooms_require_email, 814 phoneRequired: self.clientSettings.rooms_require_phone, 815 roomsMaxBookingMessage: self.clientSettings.rooms_max_booking_message, 816 roomsMinBookingMessage: self.clientSettings.rooms_min_booking_message, 817 roomsNearBookingMessage: self.clientSettings.rooms_near_booking_message, 818 roomsFarBookingMessage: self.clientSettings.rooms_far_booking_message, 819 roomsNotBookableMessage: self.clientSettings.rooms_not_bookable_message, 820 groupBillingEnabled: self.clientSettings.rooms_group_billing_enabled, 821 date: curDate.format('YYYY-MM-DD'), 822 selected: parseInt(self.settings.selectedRoom, 10) === parseInt(room.id, 10), 823 bookingClass: self.clientSettings.rooms_group_billing_enabled ? currentBookingClass : false, 824 bookingType: self.clientSettings.rooms_group_billing_enabled ? currentBookingType : (self.settings.mode === 1 || self.clientSettings.rooms_staff_booking_class_default ? 0 : 1), 825 mode: self.settings.mode, 826 clientSettings: self.clientSettings, 827 payment_currency: self.settings.payment_currency, 828 payment_currency_symbol: self.settings.payment_currency_symbol 829 }).on('room:addtocart', function (event, data) { 830 data.details.t_id = Math.random().toString(36).substr(2, 5); 831 832 if (Array.isArray(data.details.assets) && data.details.assets.length > 0) {
833 var assetCheck = self._checkAssets(data.details); 834 if (!assetCheck.ok) { 835 alert(assetCheck.message); 836 return; 837 } 838 } 839 840 self.bookings.push(data.details); 841 var toUpdate = [data.details.room.id]; 842 toUpdate = toUpdate.concat(room.parentRooms, room.childRooms); 843 844 self._addCartBookingToRooms(data.details, toUpdate); 845 self._updateCart(); 846 if (data.checkout && self.bookings.length > 0) { 847 self.pendingFocus = null; 848 self._announce('Opening reservation details'); 849 self._checkout(); 850 } else { 851 var roomTitle = data.details.room && data.details.room.title ? data.details.room.title : 'Room'; 852 self.pendingFocus = { type: 'room', roomId: data.details.room.id }; 853 self._announce(roomTitle + ' added to basket'); 854 self._render(); 855 } 856 }); 857 } 858 }); 859 } 860 }); 861 if (!roomsVisible) { 862 $('<div class="amnp-picker-message"><p>No rooms found.</p><p>Please change your search criteria</p></div>').appendTo(self.stage); 863 } 864 self._updateLocations(); 865 self._sortStage(); 866 self._updateCart(); 867 self._hideCover(); 868 if (self.pendingFocus && self.pendingFocus.type === 'room') { 869 self._focusRoomById(self.pendingFocus.roomId); 870 self.pendingFocus = null; 871 } 872 self._scheduleHeightPost(); 873 }, 874 _announce: function (message) { 875 if (!this.liveRegion) return; 876 this.liveRegion.empty(); 877 if (message) { 878 this.liveRegion.text(message); 879 } 880 }, 881 _focusRoomById: function (roomId) { 882 if (!roomId) return; 883 var holder = this.stage.find('.amnp-room-holder[data-room-id="' + roomId + '"]'); 884 if (!holder.length) return; 885 var container = holder.parent(); 886 var pluginInstance = container.data('plugin_amReserveRoom'); 887 if (pluginInstance && typeof pluginInstance.focusHolder === 'function') { 888 pluginInstance.focusHolder(); 889 } else { 890 this.stage.find('.amnp-room-holder').attr('tabindex', '-1'); 891 holder.attr('tabindex', '0').focus(); 892 } 893 }, 894 _openCart: function (focusFirst) { 895 if (!this.clientSettings.rooms_cart_enabled || !this.cartPanel || this._cartOpen) return; 896 this._cartOpen = true; 897 this.cartPanel.stop(true, true).fadeIn(0); 898 this.cartPanel.attr('aria-hidden', 'false'); 899 if (this.cartButton) this.cartButton.attr('aria-expanded', 'true'); 900 if (this.loader) { 901 var loaderZ = parseInt(this.loader.css('z-index'), 10); 902 if (isNaN(loaderZ)) loaderZ = 999; 903 this.cartPanel.css('z-index', loaderZ + 1); 904 this.loader.css('pointer-events', 'none'); 905 } 906 this._showCover(false); 907 if (focusFirst) { 908 this._focusFirstCartItem(); 909 } 910 this._scheduleHeightPost(); 911 }, 912 _closeCart: function (restoreFocus) { 913 if (!this.clientSettings.rooms_cart_enabled || !this.cartPanel) return; 914 if (!this._cartOpen) { 915 if (this.cartButton) this.cartButton.attr('aria-expanded', 'false'); 916 this.cartPanel.attr('aria-hidden', 'true'); 917 return; 918 } 919 this._cartOpen = false; 920 this.cartPanel.stop(true, true).fadeOut(0); 921 this.cartPanel.attr('aria-hidden', 'true'); 922 if (this.cartButton) { 923 this.cartButton.attr('aria-expanded', 'false'); 924 if (restoreFocus) this.cartButton.focus(); 925 } 926 if (this.loader) { 927 this.loader.css('pointer-events', ''); 928 } 929 this._hideCover(); 930 this._scheduleHeightPost(); 931 }, 932 _toggleCart: function (focusFirst) { 933 if (this._cartOpen) { 934 this._closeCart(true); 935 } else { 936 this._openCart(focusFirst); 937 } 938 }, 939 _focusFirstCartItem: function () { 940 var focusables = this._getCartFocusables(); 941 if (focusables.length) { 942 focusables.first().focus(); 943 } else if (this.cartPanel) { 944 this.cartPanel.attr('tabindex', '-1').focus(); 945 this.cartPanel.removeAttr('tabindex'); 946 } 947 }, 948 _getCartFocusables: function () { 949 if (!this.cartPanel) return $(); 950 var selector = 'button, [href], [tabindex]:not([tabindex="-1"])'; 951 return this.cartPanel.find(selector).filter(':visible'); 952 }, 953 _addCartBookingToRooms: function (details, toUpdate) { 954 this.rooms.forEach(function (r) { 955 if (toUpdate.indexOf(r.id) !== -1) { 956 r.bookings.push({ 957 start_time: details.start, 958 end_time: details.end, 959 breakdown_time: details.breakdown_time, 960 setup_time: details.setup_time, 961 t_id: details.t_id 962 }); 963 } 964 }); 965 }, 966 _checkAssetOverlap: function (booking, asset) { 967 var self = this; 968 969 if (typeof self.bookings !== 'undefined' && self.bookings.length > 0) { 970 for (var b = 0; b < self.bookings.length; b++) { 971 var book = self.bookings[b]; 972 973 if (book !== booking) { 974 var bssA = moment(booking.start); 975 var bseA = moment(booking.end); 976 var bssB = moment(book.start); 977 var bseB = moment(book.end); 978 979 var bRangeA = moment.range(bssA, bseA); 980 var bRangeB = moment.range(bssB, bseB); 981 982 if (bRangeA.overlaps(bRangeB)) { 983 return booking.assets.findIndex(function (v, i) { return parseInt(v.id) === parseInt(asset.id) }) > -1; 984 } 985 } 986 } 987 } 988 989 return false;
990 }, 991 _checkAssets: function (bookingData) { 992 var self = this; 993 var incomingAssets = bookingData.assets; 994 if (typeof self.bookings !== 'undefined' && self.bookings.length > 0) { 995 for (var i = 0; i < incomingAssets.length; i++) { 996 var cartQty = 0; 997 var assetToCheck = incomingAssets[i]; 998 var assetData = assetToCheck.assetData; 999 var bookingMin = parseInt(assetData.booking_min); 1000 var bookingMax = parseInt(assetData.booking_max); 1001 var available = parseInt(assetData.quantity); 1002 var offset = 0; 1003 1004 // Start off by setting the cart qty to the incoming one. 1005 cartQty += parseInt(assetToCheck.qty); 1006 1007 // These checks have to be above the other bookings. 1008 // Reason being that we really only care about the incoming. Fairly sure this is already handled, but it is for sure now. 1009 if (cartQty < bookingMin && bookingMin > 0) { 1010 offset = bookingMin - cartQty; 1011 return { ok: false, message: 'You must select ' + offset + ' more of ' + assetData.name + ' to continue' }; 1012 } 1013 1014 if (cartQty > bookingMax && bookingMax > 0) { 1015 offset = cartQty - bookingMax; 1016 return { ok: false, message: 'You must remove ' + offset + ' of ' + assetData.name + ' from the current booking to continue.' }; 1017 } 1018 1019 // Loop through all bookings. 1020 for (var b = 0; b < self.bookings.length; b++) { 1021 var book = self.bookings[b]; 1022 if (Array.isArray(book.assets) && book.assets.length > 0) { 1023 var found = book.assets.findIndex(function (v, i) { return parseInt(v.id) === parseInt(assetToCheck.id) }); 1024 if (found > -1) { 1025 // We have a match. 1026 // Get booked asset data first first 1027 1028 // CH 22796 - Need to check if the dates/times actually overlap, function returns t/f 1029 // f = DO NOT Add because it doesn't overlap 1030 // t = add because it overlaps 1031 if (self._checkAssetOverlap(bookingData, assetToCheck)) { 1032 cartQty += parseInt(book.assets[found].qty); 1033 } 1034 1035 // No break, we want to get ALL bookings in the basket. 1036 } 1037 } 1038 } 1039 1040 // Check to make sure that the available number of assets is not exceeded by all bookings + incoming 1041 // If it is then it was incoming that broke it. 1042 if (cartQty > available) { 1043 offset = cartQty - available; 1044 return { ok: false, message: 'There are only ' + available + ' of ' + assetData.name + ' remaining. You must remove ' + offset + ' from the current booking to continue.' }; 1045 } 1046 } 1047 } 1048 1049 return { ok: true }; 1050 }, 1051 _checkout: function () { 1052 var self = this; 1053 var roomBooker = $('<div/>').appendTo(this.element); 1054 var currentBookingClass = false; 1055 1056 if (!self.settings.embeddedMode) { 1057 self.navigation.unstick(); 1058 self.key.unstick(); 1059 } 1060 1061 if (self.groupTypeSelect) { 1062 if (self.groupTypeSelect.is(':visible')) { 1063 currentBookingClass = self.groupTypeSelect.val(); 1064 } 1065 } 1066 1067 if (self.responsiveGroupTypeSelect) { 1068 if (self.responsiveGroupTypeSelect.is(':visible')) { 1069 currentBookingClass = self.responsiveGroupTypeSelect.val(); 1070 } 1071 } 1072 self.holder.hide(); 1073 self._scheduleHeightPost(); 1074 1075 var payLaterEnabled = self.settings.mode === 1 1076 ? self.clientSettings.rooms_enable_pay_later_staff 1077 : self.clientSettings.rooms_enable_pay_later; 1078 1079 var earliestAutoCancelTime = false; 1080 var now = moment().tz(self.settings.timezone); 1081 if (self.clientSettings.rooms_auto_cancel_unpaid_pay_later && self.bookings.length > 0) { 1082 for (var b = 0; b < self.bookings.length; b++) { 1083 var booking = self.bookings[b]; 1084 if (booking.room.restrictions.pay_later_auto_cancel_enabled == 1 && booking.room.restrictions.pay_later_auto_cancel_min !== null) { 1085 // Auto-cancellation is enabled for this booking class 1086 var autoCancelMin = parseInt(booking.room.restrictions.pay_later_auto_cancel_min); 1087 var autoCancelTime = moment.tz(booking.start, self.settings.timezone).subtract(autoCancelMin, 'minutes'); 1088 if (!earliestAutoCancelTime || autoCancelTime.isBefore(earliestAutoCancelTime)) { 1089 earliestAutoCancelTime = autoCancelTime; 1090 } 1091 } 1092 } 1093 }
1094 // Do not allow patrons to book with pay later after the auto-cancellation time 1095 if (earliestAutoCancelTime && earliestAutoCancelTime.isBefore(now) && self.settings.mode === 0) { 1096 payLaterEnabled = false; 1097 } 1098 1099 // Display payment deadline before booking 1100 var autoCancelNoticeTemplate = self.settings.mode === 0 ? self.clientSettings.rooms_auto_cancel_notice : 'The patron will have until {{time}} on {{date}} to complete their payment.'; 1101 var autoCancelNotice = payLaterEnabled && earliestAutoCancelTime && !earliestAutoCancelTime.isBefore(now) ? autoCancelNoticeTemplate.replace('{{date}}', earliestAutoCancelTime.format('MMMM DD, YYYY')).replace('{{time}}', earliestAutoCancelTime.format('h:mma')) : ""; 1102 1103 roomBooker.amReserveBooker({ 1104 client: self.settings.client, 1105 apiServer: self.settings.apiServer, 1106 singleBranch: self.locations.length === 1, 1107 enabledCart: self.clientSettings.rooms_cart_enabled, 1108 enablePatronNotes: self.clientSettings.rooms_patron_notes_enabled, 1109 enableStaffNotes: self.settings.mode === 1 && self.settings.enableStaffNotes, 1110 enableExpectedAttendees: self.clientSettings.rooms_expected_attendees_enabled, 1111 expectedAttendeesRequired: self.clientSettings.rooms_expected_attendees_required, 1112 useSetupBreakdown: self.clientSettings.rooms_use_setup, 1113 adjustSetupBreakdown: self.clientSettings.rooms_adjust_setup, 1114 pinlessAuth: self.clientSettings.client_pinless_auth, 1115 roomsPatronDetailsReadonly: self.clientSettings.rooms_patron_details_readonly, 1116 ecommerceEnabled: self.clientSettings.rooms_ecommerce_enabled, 1117 chargingEnabled: self.clientSettings.rooms_charging_enabled, 1118 offlinePaymentsEnabled: self.settings.mode === 1 ? self.clientSettings.rooms_staff_offline_payments_enabled : self.clientSettings.rooms_patron_offline_payments_enabled, 1119 cardPaymentEnabled: self.clientSettings.rooms_enable_card, 1120 offlinePaymentsMessage: self.clientSettings.rooms_offline_payments_message, 1121 laterPaymentsMessage: self.clientSettings.rooms_later_payments_message, 1122 autoCancelNotice, 1123 loginRequired: self.clientSettings.rooms_require_login, 1124 layoutRequired: self.clientSettings.rooms_layout_required, 1125 emailRequired: self.clientSettings.rooms_require_email, 1126 phoneRequired: self.clientSettings.rooms_require_phone, 1127 staffRespectPatronLimit: self.clientSettings.rooms_staff_respect_patron_limit, 1128 enforceMaxAttendees: self.clientSettings.rooms_enforce_max_attendees, 1129 groupBillingEnabled: self.clientSettings.rooms_group_billing_enabled, 1130 bookingClass: self.clientSettings.rooms_group_billing_enabled ? currentBookingClass : false, 1131 bookings: self.bookings, 1132 mode: self.settings.mode, 1133 offlineButtonText: self.clientSettings.rooms_offline_button_text, 1134 rooms_mediated_booking_message: self.clientSettings.rooms_mediated_booking_message, 1135 showSMSPrompt: self.clientSettings.notifications_allow_sms_optout, 1136 SMSOptMode: self.clientSettings.notifications_sms_opt_mode, 1137 SMSPromptMessage: self.clientSettings.notifications_sms_optout_message, 1138 sessionId: this.settings.sessionId, 1139 chargingMode: this.settings.chargingMode, 1140 testMode: this.settings.testMode, 1141 ezproxy: this.settings.ezproxy, 1142 rooms_remotelock_show_on_confirmationscreen_mediated: self.clientSettings.rooms_remotelock_show_on_confirmationscreen_mediated, 1143 payment_currency: this.settings.payment_currency, 1144 payment_currency_symbol: this.settings.payment_currency_symbol, 1145 phone_number_regex: this.clientSettings.phone_number_regex, 1146 payLaterEnabled, 1147 payLaterButtonText: self.clientSettings.rooms_pay_later_button_text, 1148 }).on('booker:reset', function (event, status) { 1149 self.storage.removeItem('communicoReserveCart'); 1150 self.bookings = []; 1151 self._updateCart(); 1152 }).on('booker:closed', function (event, status) { 1153 if (status || !self.clientSettings.rooms_cart_enabled) { 1154 self.bookings = []; 1155 self._updateCart(); 1156 } 1157 roomBooker.remove(); 1158 self.holder.show(); 1159 if (!self.settings.embeddedMode) { 1160 if (self.settings.mode === 1) { 1161 self.navigation.sticky({ topSpacing: 80, wrapperClassName: 'sticky-nav' }); 1162 self.key.sticky({ topSpacing: 140, bottomSpacing: 50, wrapperClassName: 'sticky-key' }); 1163 } else { 1164 self.navigation.sticky({ wrapperClassName: 'sticky-nav' }); 1165 self.key.sticky({ topSpacing: 60, bottomSpacing: ($('#footer').outerHeight() || 300) + 50, wrapperClassName: 'sticky-key' }); 1166 } 1167 } 1168 self._update(); 1169 self._scheduleHeightPost(); 1170 }); 1171 var bookerHolder = roomBooker.find('.amnp-booker-holder').first(); 1172 if (bookerHolder.length) { 1173 bookerHolder.attr('tabindex', '-1').focus(); 1174 } 1175 self._announce('Reservation details ready'); 1176 self._scheduleHeightPost(); 1177 }, 1178 _updateCart: function () { 1179 var self = this;
1180 this.bookings.forEach(function (booking) { 1181 var room = null; 1182 //console.log(booking.booking_class, booking.room.id); 1183 //console.log(self.roomByClassById); 1184 if (parseInt(booking.booking_class) > 0) { 1185 room = self.roomByClassById[booking.booking_class][booking.room.id]; 1186 } else { 1187 room = self.roomById[booking.room.id]; 1188 } 1189 1190 booking.room = room; 1191 }); 1192 1193 if (this.storage) { 1194 this.storage.setItem('communicoReserveCart', JSON.stringify(this.bookings)); 1195 } 1196 1197 if (this.cartCount) { 1198 this.cartCount.text(this.bookings.length); 1199 } 1200 if (this.cartButton) { 1201 var countLabel = this.bookings.length === 1 ? '1 item' : this.bookings.length + ' items'; 1202 this.cartButton.attr('aria-label', 'Room basket, ' + countLabel + '. Press enter to view basket'); 1203 } 1204 if (this.cartList) { 1205 this.cartList.empty(); 1206 if (this.bookings.length === 0) { 1207 this.cartList.text('Your cart is empty'); 1208 if (this.cartFooter) this.cartFooter.hide(); 1209 this._closeCart(false); 1210 } else { 1211 if (this.cartFooter) this.cartFooter.show(); 1212 var self = this; 1213 this.bookings.forEach(function (booking) { 1214 self._getBookingSummeryDetails(booking).appendTo(self.cartList); 1215 }); 1216 } 1217 } else { 1218 if (this.bookings.length === 0 && this.cartFooter) this.cartFooter.hide(); 1219 } 1220 if (this.settings.mode !== 1 && this.clientSettings.rooms_charging_enabled) { 1221 if (this.cartTotal) { 1222 this.cartTotal.html(this.settings.payment_currency_symbol + '' + this._formatCurrency(this._calculateTotalCharge())); 1223 } 1224 } else if (this.cartTotal) { 1225 this.cartTotal.empty(); 1226 } 1227 if (this.cartPanel) { 1228 this.cartPanel.attr('aria-hidden', this._cartOpen ? 'false' : 'true'); 1229 } 1230 this._scheduleHeightPost(); 1231 }, 1232 _getBookingSummeryDetails: function (booking) { 1233 var $bookingSummary = $('<div class="amnp-booking-summary"></div>'); 1234 var $bookingSummaryDetails = $('<div class="amnp-booking-summary-details"></div>').appendTo($bookingSummary); 1235 var startTime = moment(booking.start); 1236 var endTime = moment(booking.end); 1237 1238 if (booking.room.image && booking.room.image.length > 0) { 1239 $('<img/>').attr('src', '//' + this.settings.client + '.libnet.info/images/roombooking/' + this.settings.client + '/' + booking.room.image).attr('alt', booking.room.title).appendTo($bookingSummaryDetails); 1240 } 1241 1242 var title = $('<div class="amnp-booking-summary-title">' + booking.room.title + '</div>').appendTo($bookingSummaryDetails); 1243 if (!this.settings.singleBranch) { 1244 title.append('<span>, ' + booking.location.name + '</span>'); 1245 } 1246 $('<div class="amnp-booking-summary-time">' + startTime.format('h:mma') + ' - ' + endTime.format('h:mma') + '</b>, ' + endTime.format('MMMM DD, YYYY') + '</div>').appendTo($bookingSummaryDetails); 1247 1248 var $bookingSummaryCost = $('<div class="amnp-booking-summary-total"></div>').appendTo($bookingSummary); 1249 var minutes = moment.duration(endTime.diff(startTime)).asMinutes(); 1250 var realmin = minutes % 60 1251 var hours = Math.floor(minutes / 60); 1252 var displayText = ''; 1253 if (hours > 0) { 1254 displayText = hours + (hours === 1 ? ' hour' : ' hours'); 1255 } 1256 if (realmin > 0) { 1257 displayText += ' ' + realmin + (realmin === 1 ? ' minute' : ' minutes'); 1258 } 1259 $('<div class="amnp-booking-summary-booking-name"></div>').text(displayText).appendTo($bookingSummaryCost); 1260 var costHolder = $('<div class="amnp-booking-summary-booking-cost"></div>').appendTo($bookingSummaryCost); 1261 if (this.settings.mode !== 1 && this.clientSettings.rooms_charging_enabled) { 1262 costHolder.html(this.settings.payment_currency_symbol + '' + this._formatCurrency(this._calculateBookingCharge(booking, false))); 1263 } 1264 1265 if (booking.assets.length > 0) { 1266 var $bookingSummaryAssets = $('<div class="amnp-booking-summary-assets"></div>').appendTo($bookingSummary); 1267 var self = this; 1268 booking.assets.forEach(function (asset) { 1269 var a = $('<div class="amnp-booking-summary-asset"></div>').appendTo($bookingSummaryAssets); 1270 $('<div class="amnp-booking-summary-booking-name"></div>').text(asset.name).appendTo(a); 1271 $('<div class="amnp-booking-summary-asset-qty"></div>').text('qty ' + asset.qty).appendTo(a);
1272 var costHolder = $('<div class="amnp-booking-summary-booking-cost"></div>').appendTo(a); 1273 if (self.clientSettings.rooms_charging_enabled) { 1274 costHolder.html(self.settings.payment_currency_symbol + '' + self._formatCurrency(parseFloat(asset.cost) * asset.qty)); 1275 } 1276 $('<div class="amnp-booking-summary-booking-remove link">remove</div>').attr({ tabindex: 0, role: 'button', 'aria-label': 'Remove ' + asset.name + ' from basket' }).data('asset', asset).appendTo(a); 1277 }); 1278 } 1279 var $bookingSummaryTotal = $('<div class="amnp-booking-summary-total"></div>').appendTo($bookingSummary); 1280 var $subTotal = $('<div class="amnp-booking-summary-booking-subtotal"></div>').appendTo($bookingSummaryTotal); 1281 var costHolder = $('<div class="amnp-booking-summary-booking-cost"></div>').appendTo($bookingSummaryTotal); 1282 var removeLabel = 'Remove booking from basket'; 1283 if (booking.room && booking.room.title) { 1284 removeLabel = 'Remove ' + booking.room.title + ' booking starting ' + startTime.format('MMMM Do YYYY, h:mma'); 1285 } 1286 $('<div class="amnp-booking-summary-booking-remove link">remove</div>').attr({ tabindex: 0, role: 'button', 'aria-label': removeLabel }).data('booking', booking).appendTo($bookingSummaryTotal); 1287 if (this.settings.mode !== 1 && this.clientSettings.rooms_charging_enabled) { 1288 $subTotal.text('sub total'); 1289 costHolder.html(this.settings.payment_currency_symbol + '' + this._formatCurrency(this._calculateBookingCharge(booking, true))); 1290 } 1291 1292 return $bookingSummary; 1293 }, 1294 _formatCurrency: function (amount) { 1295 return amount.toFixed(2).replace(/(\d)(?=(\d{3})+\.)/g, '$1,'); 1296 }, 1297 _calculateBookingCharge: function (booking, incAssets) { 1298 var startTime = moment(booking.start); 1299 var endTime = moment(booking.end); 1300 1301 var roomTotal = parseFloat(booking.room.restrictions.charge_amount); 1302 if (parseInt(booking.room.restrictions.charge_period) === 0) { 1303 var blockSize = parseInt(booking.room.restrictions.segment_size) * parseInt(booking.room.restrictions.block_count); 1304 var rawDuration = moment.duration(endTime.diff(startTime)).asMinutes(); 1305 roomTotal = (rawDuration / blockSize) * roomTotal; 1306 } 1307 1308 if (booking.layout && booking.layout.cost && parseFloat(booking.layout.cost) > 0) { 1309 roomTotal += parseFloat(booking.layout.cost); 1310 } 1311 1312 if (incAssets) { 1313 var assetCharge = booking.assets.reduce(function (total, asset) { 1314 return total + parseFloat(asset.cost) * asset.qty; 1315 }, 0); 1316 } else { 1317 var assetCharge = 0; 1318 } 1319 return parseFloat(roomTotal + assetCharge); 1320 }, 1321 _calculateTotalCharge: function () { 1322 var self = this; 1323 var cartTotal = this.bookings.reduce(function (total, booking) { 1324 return total + self._calculateBookingCharge(booking, true); 1325 }, 0); 1326 return cartTotal; 1327 }, 1328 _sortStage: function () { 1329 var self = this; 1330 var sections = $('.amnp-location-section', self.stage); 1331 sections.sort(function (a, b) { 1332 var an = a.getAttribute('data-location-name'); 1333 var bn = b.getAttribute('data-location-name'); 1334 1335 if (an > bn) { 1336 return 1; 1337 } 1338 if (an < bn) { 1339 return -1; 1340 } 1341 return 0; 1342 }); 1343 sections.detach().appendTo(self.stage); 1344 }, 1345 _update: function (force) { 1346 var self = this; 1347 1348 // Date selection management 1349 var curDate = moment(self.datePicker.datepicker('getUTCDate')).utc(); 1350 $('.amnp-navigation-title', self.navigation).text(curDate.format('LL')); 1351 self.CurrentDateString = curDate.format('YYYY-MM-DD'); 1352 1353 if (self._updating) return true; 1354 self._updating = true; 1355 1356 // START - Location selection management 1357 self.selectedLocations = []; 1358 if (self.locationSelect.is(":visible")) { 1359 self.locationSelect.find('input[data-location]:checked').each(function (el) { 1360 self.selectedLocations.push($(this).attr('data-location')); 1361 }); 1362 } 1363 1364 if ($(".multiselect", self.holder).is(":visible")) { 1365 self.responsiveLocationSelect.find('option[data-location]:selected').each(function (el) { 1366 self.selectedLocations.push($(this).attr('data-location')); 1367 }); 1368 } 1369 if (parseInt(self.settings.selectedLocation) > 0 && self.selectedLocations.length === 0) { 1370 self.selectedLocations.push(self.settings.selectedLocation); 1371 } 1372 1373 if (self.selectedLocations.length == 0) { 1374 self._updating = false;
1375 if (!force) { 1376 self.locationCount = 0; 1377 if (self.locations.length == 0) { 1378 self.stage.empty().append('<h3>No rooms found. Please change your search criteria</h3>'); 1379 } else { 1380 self.stage.empty().append('<h3>Please choose at least one branch</h3>'); 1381 } 1382 self._scheduleHeightPost(); 1383 return; 1384 } 1385 if (self.locationSelect.is(":visible")) { 1386 self.locationSelect.find('.checkall').prop('checked', true); 1387 } 1388 1389 if (self.responsiveLocationSelect.is(":visible")) { 1390 self.responsiveLocationSelect.multiselect('selectAll', true); 1391 } 1392 // Our previous search had no matching branches, so fall back to previously 1393 // selected branches for the current search - or to every branch when there 1394 // is no readable saved selection. Stays an array: locationCount below counts 1395 // branches, not characters. 1396 self.selectedLocations = self._readSavedBranches() || self.locations.map(function (location) { 1397 return String(location.id); 1398 }); 1399 } 1400 self._createCookie('reserveBranches', JSON.stringify(self.selectedLocations), 365); 1401 self.locationCount = self.selectedLocations.length; 1402 // END - Location selection management 1403 1404 self._showCover(true); 1405 1406 // START - Set parameters for API calls 1407 var roomOptions = { 1408 date: self.CurrentDateString, 1409 class_id: [] 1410 }; 1411 var locationOptions = { 1412 only_with_external_rooms: 1 1413 }; 1414 // Add locations for existing bookings to the roomclass query 1415 var locs = self.selectedLocations; 1416 locs = locs.concat(this.bookings 1417 .map(function (x) { return x.location.id; }) 1418 .filter(function (x) { return !locs.includes(x) })); 1419 1420 if (self.clientSettings.rooms_group_billing_enabled) { 1421 if (self.settings.mode === 0) { 1422 roomOptions.external_only = 1; 1423 } 1424 if (self.groupTypeSelect.is(':visible')) { 1425 roomOptions.class_id.push(self.groupTypeSelect.val()); 1426 } 1427 if (self.responsiveGroupTypeSelect.is(':visible')) { 1428 roomOptions.class_id.push(self.responsiveGroupTypeSelect.val()); 1429 } 1430 1431 locationOptions.class_id = roomOptions.class_id[0]; 1432 1433 if (self.bookings.length > 0) { 1434 for (var b = 0; b < self.bookings.length; b++) { 1435 var booking = self.bookings[b]; 1436 if (roomOptions.class_id.indexOf(booking.booking_class) === -1) { 1437 roomOptions.class_id.push(booking.booking_class); 1438 } 1439 } 1440 } 1441 } else { 1442 var classtype = 0; 1443 if (self.settings.mode === 0) { 1444 locationOptions.only_with_patron_rooms = 1; 1445 roomOptions.external_only = 1; 1446 classtype = 1; 1447 } 1448 var defaultClass = self.userclasses.find(function (c) { 1449 return c.type == classtype 1450 }); 1451 roomOptions.class_id = [defaultClass.id]; 1452 1453 } 1454 1455 // CH# 1678 1456 // Had to move this out of 'mode' check, otherwise it's never hit on patron side. 1457 if (self.settings.unmediatedOnly) { 1458 roomOptions.unmediated_only = 1; 1459 locationOptions.only_with_unmediated = 1; 1460 } 1461 1462 if (self.settings.mediatedOnly) { 1463 roomOptions.mediated_only = 1; 1464 } 1465 locationOptions.mode = self.settings.mode; 1466 // END - Set parameters for API calls 1467 1468 Promise.all([ 1469 self._getJSON(self.settings.apiServer + '/v2/' + self.settings.client + '/roomsbyclass/' + locs, roomOptions), 1470 self._getJSON(self.settings.apiServer + '/v1/' + self.settings.client + '/opening-hours/' + self.CurrentDateString + '/1'), 1471 self._getJSON(self.settings.apiServer + '/v1/' + self.settings.client + '/locations', locationOptions) 1472 ]).then(function (result) { 1473 var roomsByClasses = result[0]; 1474 self.openingHours = result[1]; 1475 self.locations = result[2]; 1476 self.rooms = result[0][roomOptions.class_id[0]]; 1477 self.locationsById = {}; 1478 1479 if (self.locations.length === 0) { 1480 self._updating = false; 1481 self.settings.selectedLocation = 0; 1482 self.locationSelect.find('input[data-location]:checked').prop('checked', false); 1483 self.responsiveLocationSelect.find('option[data-location]:selected').prop('selected', false); 1484 self._render(); 1485 return; 1486 } 1487 1488 self.locations.forEach(function (location) { 1489 self.locationsById[location.id] = location; 1490 }); 1491 1492 self.roomById = {}; 1493 self.roomByClassById = {}; 1494 if (self.groupTypeSelect) { 1495 var selectedClassId = parseInt(self.groupTypeSelect.val(), 10); 1496 var availableClassIds = Object.keys(roomsByClasses).map(function (classId) { 1497 return parseInt(classId, 10); 1498 }); 1499 1500 if (!availableClassIds.includes(selectedClassId)) { self._updating = false; self._update(true) } 1501 } 1502 1503 for (var roomClassKey in roomsByClasses) { 1504 var roomClass = roomsByClasses[roomClassKey]; 1505 roomClass.forEach(function (room) { 1506 if (typeof self.roomByClassById[room.restrictions.class_id] === "undefined") self.roomByClassById[room.restrictions.class_id] = {}; 1507 self.roomByClassById[room.restrictions.class_id][room.id] = room; 1508 }); 1509 }; 1510 1511 //loop each room and copy its bookings to other effected rooms 1512
1513 self.rooms.forEach(function (room) { 1514 //if(typeof self.roomByClassById[room.restrictions.class_id] === "undefined") self.roomByClassById[room.restrictions.class_id] = {}; 1515 //self.roomByClassById[room.restrictions.class_id][room.id] = room; 1516 self.roomById[room.id] = room; 1517 if (room.bookings.length > 0) { 1518 var otherRooms = room.childRooms.concat(room.parentRooms); 1519 self.rooms.forEach(function (subRoom) { 1520 if (otherRooms.indexOf(subRoom.id) !== -1) { 1521 subRoom.bookings = subRoom.bookings.concat(room.bookings.filter(function (booking) { 1522 return parseInt(booking.room_id, 10) == parseInt(room.id, 10); 1523 })); 1524 } 1525 }); 1526 } 1527 }); 1528 1529 self.bookings.forEach(function (details) { 1530 if (!moment(details.start).isSame(curDate, 'd')) return; 1531 var toUpdate = [details.room.id]; 1532 var room = null; 1533 1534 if (parseInt(details.booking_class) > 0) { 1535 room = self.roomByClassById[details.booking_class][details.room.id]; 1536 } else { 1537 room = self.roomById[details.room.id]; 1538 } 1539 1540 if (details.layout && details.layout.id) { 1541 var layoutId = parseInt(details.layout.id); 1542 details.layout = room.layouts.find(function (layout) { 1543 return parseInt(layout.id) === layoutId; 1544 }); 1545 } 1546 1547 details.room = room; 1548 toUpdate = toUpdate.concat(room.parentRooms, room.childRooms); 1549 self._addCartBookingToRooms(details, toUpdate); 1550 }); 1551 1552 self._updating = false; 1553 self._hideCover(); 1554 self._render(); 1555 }).catch(function (err) { 1556 self._updating = false; 1557 self._scheduleHeightPost(); 1558 console.log(err); 1559 }); 1560 } 1561 }); 1562 $.fn[pluginName] = function (options) { 1563 return this.each(function () { 1564 if (!$.data(this, "plugin_" + pluginName)) { 1565 $.data(this, "plugin_" + pluginName, new Plugin(this, options)); 1566 } 1567 }); 1568 }; 1569})(jQuery, window, document);
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.