PageSourceSearch

https://www.faylib.org/js/am-reserve-booker.v3.js?v=1480

js faylib.org collected 2026-09-24 09:57:07 UTC 63,903 bytes, 1,409 lines download raw bytes

1;
2(function ($, window, document, undefined) {
3	"use strict";
4	var pluginName = "amReserveBooker",
5		defaults = {
6			apiServer: false,
7			client: false,
8			mode: 0,
9			showSMSPrompt: false,
10			SMSOptMode: 0,
11			SMSPromptMessage: '',
12			offlineButtonText: 'Offline',
13			sessionId: '',
14			chargingMode: 0,
15			mediated: 0,
16			testMode: 0,
17			payment_currency: "USD",
18			payment_currency_symbol: '$',
19			enableStaffNotes: false
20		};
21	// The actual plugin constructor
22	function Plugin(element, options) {
23		this.element = element;
24		this.$element = $(element);
25		this.settings = $.extend(true, {}, defaults, options);
26		this._defaults = defaults;
27		this._name = pluginName;
28		this.bookings = [];
29		this.currentBooking = false;
30		this.bookingPaymentType = 'card';
31		this.cardForm = null;
32		this.paypalAmount = 0.00;
33		this.PayPalPayFlow = null;
34		this.availablePaymentTypes = [];
35
36		this.init();
37	}
38	$.extend(true, Plugin.prototype, $.fn['amShared']);
39	$.extend(Plugin.prototype, {
40		init: function () {
41			var self = this;
42			var defaultPaymentType =
43				self.settings.ecommerceEnabled && self.settings.cardPaymentEnabled && self.settings.mode !== 1 ? 'card' : 'offline';
44			self.bookingPaymentType = defaultPaymentType;
45			this.settings.bookings.forEach(function (booking) {
46				self.bookings.push({
47					data: {
48						start_time: booking.start,
49						end_time: booking.end,
50						contact: { first_name: "", last_name: "", phone: "", email: "", librarycard: "", booking_title: "", group_name: "" },
51						assets: booking.assets,
52						room_id: booking.room.id,
53						expected_attendees: 0,
54						staff_notes: '',
55						customQuestions: {},
56						layout_id: booking.layout ? booking.layout.id : false,
57						paymentType: defaultPaymentType,
58						bookingClass: booking.booking_class
59					},
60					layout: booking.layout,
61					room: booking.room,
62					location: booking.location,
63					startTime: moment(booking.start),
64					endTime: moment(booking.end)
65				});
66
67				if (booking.room.restrictions.require_login) {
68					if (parseInt(booking.room.restrictions.require_login) === 1 && !self.settings.loginRequired) {
69						self.settings.loginRequired = true;
70					}
71				}
72			});
73
74			this._buildInterface();
75			this._bindEvents();
76
77			window.scroll({
78				top: 0,
79				left: 0
80			});
81
82			if (this.settings.mode === 1) {
83				this.cardlookup.show();
84			} else {
85				if (this.settings.loginRequired) {
86					this.loginForm.show();
87					this.detailsForm.hide();
88				}
89			}
90
91		},
92		_buildInterface: function () {
93			var self = this;
94
95			self.holder = $('<div class="amnp-booker-holder"></div>').appendTo(self.element);
96			self.reserveScreen = $('<div class="amnp-reserve-screen amnp-reserve-screen-basket"></div>').appendTo(self.holder);
97			self.reserveScreenShared = $('<div class="amnp-reserve-screen-shared"></div>').appendTo(self.reserveScreen);
98			self.confirmScreen = $('<div class="amnp-confirm-screen"></div>').appendTo(self.holder).hide();
99			self.screenThankyou = $('<div class="amnp-thanks-screen"></div>').appendTo(self.holder).hide();
100
101			if (self.bookings.length == 1) {
102				self._getBookingHeader(self.bookings[0]).appendTo(self.reserveScreenShared);
103			}
104
105			if (self.settings.mode === 1) {
106				var bookingType = self._createField('booking_type', 'select', 'Booking type', false).appendTo(self.reserveScreenShared);
107				var bookingTypeSelect = $('select', bookingType);
108				self._getJSON('/ajax/fetch/get_user_classes').then(function (json) {
109					json.forEach(function (cls) {
110						$('<option value="' + cls.id + '">' + cls.name + '</option>').data('details', cls).appendTo(bookingTypeSelect);
111						if (!self.settings.bookingClass && parseInt(cls.type, 10) === 1) {
112							$('#booking_type', self.reserveScreenShared).val(cls.id).trigger('change');
113						}
114					});
115					if (self.settings.bookingClass) $('#booking_type', self.reserveScreenShared).val(self.settings.bookingClass).trigger('change');
116				}).catch(function (err) {
117					console.log(err);
118				});
119				self.cardlookup = self._createField('login_card', 'text', 'Library card number', false, '<span data-loading-text="<i class=\'fa fa-circle-o-notch fa-spin\'></i>" class="btn btn-edit events2-lookup input-group-addon" tabindex="0" role="button" aria-label="Find details">Find details</span>').addClass('events2-card').appendTo(self.reserveScreenShared).hide();
120			} else {
121				/*start login form*/
122				self.loginForm = $('<div class="amnp-login-form"></div>').appendTo(self.reserveScreenShared);
123				if (self.settings.pinlessAuth) {
124					if (self.settings.loginRequired) {
125						self._createField('login_card', 'text', 'Library card number', false, '<span data-loading-text="<i class=\'fa fa-circle-o-notch fa-spin\'></i>" class="btn btn-edit amnp-lookup input-group-addon" tabindex="0" role="button" aria-label="Login">Login</span>', 'A library card is required to book a room.')
126							.appendTo(self.loginForm);
127					}
128				} else {
129					if(self.settings.ezproxy.showLoginButton == 1) {
130						$('<a href="' + self.settings.ezproxy.loginButtonLink + '" class="btn btn-info">' + self.settings.ezproxy.loginButtonText + '</a>').appendTo(self.loginForm);
131					} else {
132						self._createField('login_card', 'text', 'Library card number', false).addClass('amnp-card').appendTo(self.loginForm);
133						if (self.settings.loginRequired) {
134							var pin = self._createField('login_pin', 'password', 'PIN / Password', false, '<span data-loading-text="<i class=\'fa fa-circle-o-notch fa-spin\'></i>" class="btn btn-edit amnp-lookup input-group-addon" tabindex="0" role="button" aria-label="Login">Login</span>', 'A library card is required to book a room.')
135								.appendTo(self.loginForm);
136						} else {
137							var pin = self._createField('login_pin', 'password', 'PIN / Password', false, '<span data-loading-text="<i class=\'fa fa-circle-o-notch fa-spin\'></i>" class="btn btn-edit amnp-lookup input-group-addon" tabindex="0" role="button" aria-label="Find details">Find details</span>', 'Enter your Library card number and PIN and we\'ll look up your details to fill in the next fields.').appendTo(self.loginForm);
138						}
139					}
140				}
141			}
142
143			self.detailsForm = $('<div></div>').appendTo(self.reserveScreenShared);
144			if (self.settings.mode !== 1 && self.settings.loginRequired) {
145				self.reserveScreen.addClass('amnp-login-required');
146				self.detailsForm.hide();
147			}
148
149			self._createField('contact_first_name', 'text', 'First name', true).appendTo(self.detailsForm);
150			self._createField('contact_last_name', 'text', 'Last name', true).appendTo(self.detailsForm);
151			self._createField('contact_email', 'text', 'Email', self.settings.emailRequired, false, 'We\'ll use the email address to confirm the booking and to notify if the room becomes unavailable.').appendTo(self.detailsForm);
152			self._createField('contact_tel', 'text', 'Phone', self.settings.phoneRequired, false).appendTo(self.detailsForm);
153
154			$('<input type="hidden" id="librarycard" value="">').appendTo(self.detailsForm);
155
156			if (self.settings.showSMSPrompt) {
157				var checked = self.settings.SMSOptMode ? 'checked="checked"' : '';
158				var cbx = $('<div class="row amf-row">' +
159					'	<label for="phone" class="col-sm-4 control-label">&nbsp;</label>' +
160					' <div class="col-sm-8 field">' +
161					' 	<div class="checkbox"><label style="font-size:12px;text-align:left;"><input style="width:auto !important;" type="checkbox" id="contact_allow_sms"' + checked + '/>' + self.settings.SMSPromptMessage + '</label></div>' +
162					' </div></div>');
163				cbx.appendTo(self.detailsForm);
164			}
165
166			this._setBooking(0);
167			$('<div class="amnp-booking-final-summary-heading link">Reservation details</div>').appendTo(self.confirmScreen);
168			self.bookingSummary = $('<div class="amnp-booking-summary"></div>').appendTo(self.confirmScreen);
169			$('<div class="amnp-booking-final-summary-heading link">Booking details</div>').appendTo(self.confirmScreen);
170			self.bookingDetails = $('<div class="amnp-booking-options"></div>').appendTo(self.confirmScreen);
171
172			self.bookingPayment = $('<div></div>').appendTo(self.confirmScreen).hide();
173			if (self.settings.chargingEnabled && self.settings.ecommerceEnabled) $('<
173div class="amnp-booking-final-summary-heading link">Payment details</div>').appendTo(self.bookingPayment);
174
175			var buttons = $('<div class="amnp-buttons">' + '<span class="amnp-required-label"><span class="required-field"></span> field required</span>' + '<button class="btn btn-edit amnp-back-to-reserve">Cancel</button>' + '<button class="btn btn-success amnp-submit-booking">Reserve</button>' + '</div>').appendTo(self.confirmScreen);
176			if(self.settings.payLaterEnabled && this.settings.mode == 1) {
177				// Always build the staff payment type field; its visibility follows chargeability,
178				// which can change when the booking type is switched inside the booker.
179				self.staffPaymentTypeField = self._createField('payment_type', 'select', 'Payment type', false).appendTo(self.reserveScreenShared);
180				var $paymentTypeSelect = $('select', self.staffPaymentTypeField);
181				$paymentTypeSelect.append('<option value="offline">Offline</option>');
182				$paymentTypeSelect.append('<option value="pay_later">Pay Later</option>');
183				self.staffPaymentTypeField.toggle(this._isChargable());
184			}
185
186			var paymentTypes = [
187				{
188					string: 'card',
189					cond: self.settings.ecommerceEnabled && self.settings.cardPaymentEnabled,
190					buttonText: 'Card'
191				},
192				{
193					string: 'pay_later',
194					cond: self.settings.ecommerceEnabled && self.settings.payLaterEnabled,
195					buttonText: self.settings.payLaterButtonText || 'Pay Later'
196				},
197				{ // CH#4337 - Offline text button customization
198					string: 'offline',
199					cond: self.settings.offlinePaymentsEnabled,
200					buttonText: self.settings.offlineButtonText || 'Offline'
201				}
202			];
203
204			// Determine which payment types are enabled and only show those buttons
205			var buttonHtml = '';
206			paymentTypes.forEach(function (paymentType) {
207				if (paymentType['cond']) {
208					self.availablePaymentTypes.push(paymentType['string']);
209					var isActiveString = '';
210					var isCheckedString = '';
211					// Pre-select button for default payment type
212					if (self.bookingPaymentType === paymentType['string']) {
213						isActiveString = 'active';
214						isCheckedString = 'checked';
215					}
216					buttonHtml +=
217					'<label class="btn btn-default ' + isActiveString + '">'
218						+ '<input type="radio" name="options" value="' + paymentType['string'] + '" autocomplete="off" ' + isCheckedString + '>'
219						+ paymentType['buttonText'] +
220					'</label>';
221				}
222			})
223
224			self.paymentTypeForm = $('<div class="row amf-row amnp-payment-toggle">'
225				+ '<label  class="col-sm-4 control-label">Payment type'
226				+ '</span>'
227				+ '</label>'
228				+ '<div class="col-sm-8 field">'
229				+ '<div class="btn-group" data-toggle="buttons" style="display:flex"}}>'
230				+ buttonHtml
231				+ '</div>'
232				+ '</div>'
233				+ '<div class="col-sm-4">'
234				+ '</div>'
235				+ '<div class="col-sm-8 am-form-desc">'
236				+ '</div>'
237				+ '</div>').appendTo(self.bookingPayment).hide();
238
239			self.offlineForm = $('<div class="amnp-offline-form"></div>').html(self.settings.offlinePaymentsMessage).appendTo(self.bookingPayment).hide();
240			var payLaterMessage = self.settings.laterPaymentsMessage + " " + self.settings.autoCancelNotice;
241			self.laterForm = $('<div class="amnp-paylater-form"></div>').html(payLaterMessage).appendTo(self.bookingPayment).hide();
242
243
244			if (self.settings.ecommerceEnabled) {
245				self.PayPalPayFlow = new PayPalPayFlow('S', self.settings.testMode, self.settings.client);
246				self.PayPalPayFlow.buildForm().then(function (form) {
247					self.cardForm = form;
248					self.cardForm.appendTo(self.bookingPayment).hide();
249					self._finishInterface();
250				});
251			} else {
252				self._finishInterface();
253			}
254		},
255		_finishInterface: function () {
256			var self = this;
257			$('<div class="amnp-spacer"><div class="amnp-error-message"></div></div>').appendTo(self.holder);
258
259			var $stage = $('.stage');
260			if($stage.length > 0) {
261				$stage.scrollTop(0);
262			}
263
264			self._bindEvents();
265
266			if (self.settings.mode === 1) {
267				self.cardlookup.show();
268			} else {
269				if (self.settings.loginRequired) {
270					self.loginForm.show();
271					self.detailsForm.hide();
272				}
273			}
274		},
275		_back: function () {
276			if (!this.currentBooking) {
277				this.$element.trigger('booker:closed', false);
278			}
279			var pos = this.bookings.indexOf(this.currentBooking);
280			if (pos > 0) {
281				this._setBooking(pos - 1);
282			} else {
283				this.$element.trigger('booker:closed', false);
284			}
285		},
286		_next: function () {
287			var pos = this.bookings.indexOf(this.currentBooking) + 1;
288			if (pos < this.bookings.length) {
289				this._setBooking(pos);
290			} else {
291				this._showSummaryScreen();
292			}
293		},
294		_setBooking: function (pos) {
295			this.currentBooking = this.bookings[pos];
296			this.reserveScreen.find('.amnp-booker-fields').remove();
297			this._getBookingFields().appendTo(this.reserveScreen);
298			if (this.bookings.length > 1 || this.settings.chargingEnabled) {
299				if (pos > 0) {
300					this.reserveScreenShared.hide();
301				} else {
302					this.reserveScreenShared.show();
303				}
304				$('.amnp-booking-summary', this.reserveScreen).remove();
305				$('.amnp-booking-final-summary-heading', this.reserveScreen).remove();
306				this._getBookingSummeryDetails(this.currentBooking).prependTo(this.reserveScreen);
307				$('<div class="amnp-booking-final-summary-heading link">Reservation details (' + (pos + 1) + ' of ' + this.bookings.length + ')</div>').prependTo(this.reserveScreen);
308			}
309		},
310		_showSummaryScreen: function () {
311			this.reserveScreen.hide();
312
313			var self = this;
314			self.bookingSummary.empty();
315			this.bookings.forEach(function (booking) {
316				self._getBookingSummeryDetails(booking, self.settings.chargingEnabled).appendTo(self.bookingSummary);
317			});
318			self.bookingDetails.empty();
319			self._createField('contact_first_name', 'text', 'First name', false).appendTo(self.bookingDetails).find('input').val(self.currentBooking.data.contact.first_name).prop('readonly', true);
320			self._createField('contact_last_name', 'text', 'Last name', false).appendTo(self.bookingDetails).find('input').val(self.currentBooking.data.contact.last_name).prop('readonly', true);
321			self._createField('contact_tel', 'text', 'Phone', false, false).appendTo(self.bookingDetails).find('input').val(self.currentBooking.data.contact.phone).prop('readonly', true);
322			self._createField('contact_email', 'text', 'Email', false, false, 'We\'ll use the email address to confirm the booking and to notify if the room becomes unavailable.').appendTo(self.bookingDetails).find('input').val(self.currentBooking.data.contact.email).prop('readonly', true);
323
324			if (this._isChargable() && this.settings.chargingEnabled && this.settings.mode !== 1) {
325				var cost = this._calculateTotalCharge();
326
327				if (this.availablePaymentTypes.length > 0) {
328					this.bookingPayment.show();
329					if (this.availablePaymentTypes.length > 1) {
330						this.paymentTypeForm.show();
331					}
332					if (this.availablePaymentTypes.includes(this.bookingPaymentType)) {
333						if (this.bookingPaymentType === 'card') {
334							this.cardForm.show();
335						} else if (this.bookingPaymentType === 'offline') {
336							this.offlineForm.show();
337						} else if (this.bookingPaymentType === 'pay_later') {
338							this.laterForm.show();
339						}
340					}
341				}
342			} else {
343				if (this.paymentTypeForm) this.paymentTypeForm.hide();
344				if (this.cardForm) this.cardForm.hide();
345				if (this.offlineForm) this.offlineForm.hide();
346				if (this.laterForm) this.laterForm.hide();
347				if (this.bookingPayment) this.bookingPayment.hide();
348			}
349
350			this.confirmScreen.show();
351		},
352		_getBookings: function () {
353			var self = this;
354			return self.bookings.reduce(function (ret, b) {
355				var booking = JSON.parse(JSON.stringify(b.data));
356				var chargeamt = self._calculateBookingCharge(b, true);
357
358				booking.setup_time = 0;
359				booking.breakdown_time = 0;
360
361				booking.contact.group_name = booking.contact.group_name || booking.contact.first_name + ' ' + booking.contact.last_name.slice(0, 1);
362				booking.contact.booking_title = booking.contact.booking_title || booking.contact.first_name + ' ' + booking.contact.last_name.slice(0, 1);
363				booking.paymentType = self.bookingPaymentType;
364
365				if(self.settings.useSetupBreakdown) {
366					if(b.layout) {
367						//use layout setup/breakdown times
368						booking.setup_time =  b.layout.setup_time;
369						booking.breakdown_time = b.layout.breakdown_time;
370					} else {
371						//use room setup/breakdown times
372						booking.setup_time = b.room.setup_time;
373						booking.breakdown_time = b.room.breakdown_time;
374					}
375				}
376
377				if (self._isChargable() && self.settings.mode !== 1 && self.settings.ecommerceEnabled && self.settings.chargingEnabled && chargeamt > 0 && booking.paymentType === 'card') {
378
379					if (!self._isFormClean(self.cardForm)) {
380						throw "Please enter information for all required fields";
381					}
382
383					booking.paymentDetails = {
384						first_name: $('input#card_first_name', self.cardForm).val(),
385						last_name: $('input#card_last_name', self.cardForm).val(),
386						card_number: $('input#card_number', self.cardForm).val(),
387						expiration: $('input#expiration', self.cardForm).payment('cardExpiryVal'),
388						cvc: $('input#CVV2', self.cardForm).val(),
389						amt: chargeamt // Have to get the charge from the 'b' object not booking
390					};
391
392					self.paypalAmount += parseFloat(booking.paymentDetails.amt);
393
394					if (booking.paymentType == 'card' && !$.payment.validateCardNumber(booking.paymentDetails.card_number)) {
395						throw "Invalid card number";
396					}
397					if (booking.paymentType == 'card' && !$.payment.validateCardExpiry(booking.paymentDetails.expiration.month, booking.paymentDetails.expiration.year)) {
398						throw "Invalid expiration date";
399					}
400					if (booking.paymentType == 'card' && !$.payment.validateCardCVC(booking.paymentDetails.cvc)) {
401						throw "Invalid cvc";
402					}
403					if (booking.paymentType == 'card' && !$.payment.validateCardExpiry(booking.paymentDetails.expiration.month, booking.paymentDetails.expiration.year)) {
404						throw "Invalid expiration date";
405					}
406					if (booking.paymentType == 'card' && !$.payment.validateCardCVC(booking.paymentDetails.cvc)) {
407						throw "Invalid cvc";
408					}
409				}
410
411				booking.customQuestions = JSON.stringify(booking.customQuestions);
412				ret.push(booking);
413				return ret;
414			}, []);
415		},
416		_resetEvents: function () {
417			this.paymentTypeForm.off('change', ':input');
418			this.holder.off('click', '.amnp-submit-booking');
419			this.holder.off('click', '.amnp-back-to-picker');
420			this.holder.off('click', '.amnp-back-to-reserve');
421			this.holder.off('click', '.amnp-lookup-history');
422			this.holder.off('click', '.amnp-to-next-screen');
423			this.holder.off('change', '.agreed');
424
425			if (this.settings.mode === 1) {
426				this.reserveScreen.off('change', '#booking_type');
427				this.reserveScreen.off('change', '#payment_type');
428				this.reserveScreen.off('click', '.events2-lookup');
429				this.reserveScreen.off('keydown', '.events2-lookup');
430			} else {
431				this.loginForm.off('click', '.amnp-lookup');
432				this.loginForm.off('keydown', '.amnp-lookup');
433			}
434		},
435		_bindEvents: function () {
436			var self = this;
437			self._resetEvents();
438
439			self.paymentTypeForm.on('change', ':input', function () {
440				self.bookingPaymentType = $(this).val();
441				var bookingForms = {'card': self.cardForm, 'offline': self.offlineForm, 'pay_later': self.laterForm};
442				for (var paymentType in bookingForms) {
443					if (paymentType === self.bookingPaymentType) {
444						bookingForms[paymentType]?.show();
445					} else {
446						bookingForms[paymentType]?.hide();
447					}
448				}
449			});
450
451			if (this.settings.mode == 1) {
452				this.reserveScreen.on('change', '#payment_type', function () {
453					self.bookingPaymentType = $(this).val();
454					if (self.bookingPaymentType === 'pay_later') {
455						if (self.settings.autoCancelNotice && $('#auto-cancel-notice').length == 0) {
456							$('.am-input#payment_type').parent().append('<div id="auto-cancel-notice" class="am-form-desc"><div class="col-sm-4"></div><div class="col-sm-8">' + self.settings.autoCancelNotice + '</div></div>');
457						}
458					} else {
459						$('#auto-cancel-notice').remove();
460					}
461				});
462			}
463
464			self.holder.on('click', '.amnp-submit-booking', function (event) {
465				event.preventDefault();
466				var b = $(this).button('loading');
467				var cache = [];
468
469				var runCharges = self.settings.chargingEnabled && self.settings.ecommerceEnabled && self.settings.cardPaymentEnabled && self.bookingPaymentType === 'card';
470
471				try {
472					var finalBookings = self._getBookings();
473				} catch (exception) {
474					$('.amnp-error-message', self.holder).text(exception).fadeIn().delay(6000).fadeOut();
475					b.button('reset');
476					return;
477				}
478
479				if (self.settings.mode === 1) {
480					// Control Panel
481					// No Payments
482					var toDo = finalBookings.length;
483					$.each(finalBookings, function (index, booking) {
484						var bookingData = self.bookings[index];
485						var data = {
486							location_id: bookingData.location.id,
487							status: 0,
488							class_id: bookingData.class_id,
489							booking_type: bookingData.type,
490							contact_first_name: booking.contact.first_name,
491							contact_last_name: booking.contact.last_name,
492							contact_email: booking.contact.email,
493							contact_tel: booking.contact.phone,
494							contact_allow_sms: booking.contact.allow_sms ? 1 : 0,
495							group_name: booking.contact.group_name,
496							expected_attendees: booking.expected_attendees,
497							patron_notes: booking.patron_notes,
498							staff_notes: booking.staff_notes,
499							display_name: booking.contact.booking_title,
500							librarycard: booking.contact.librarycard,
501							start_time: booking.start_time,
502							end_time: booking.end_time,
503							setup_time: booking.setup_time,
504							breakdown_time: booking.breakdown_time,
505							assets: booking.assets,
506							room_id: booking.room_id,
507							custom_questions: booking.customQuestions,
508							layout_id: booking.layout_id,
509							paymentType: booking.paymentType,
510							booking_class: bookingData.class_id
511						};
512						if (booking.paymentDetails) {
513							data.paymentDetails = booking.paymentDetails;
514						}
515						$.ajax({
516							type: 'POST',
517							data: data,
518							dataType: 'json',
519							url: '/ajax/fetch/save_room_booking',
520							success: function (data) {
521								toDo--;
522								if (toDo <= 0) {
523									confirmChanges();
524									showAlert(data);
525									b.button('reset');
526									if (data.type === 'success') {
527										self.$element.trigger('booker:reset', true);
528										self.$element.trigger('booker:closed', true);
529										window.location.hash = '#rooms';
530									}
531								}
532							}
533						});
534					});
535				} else {
536					// Website
537					// PayPal PayFlow
538					var blocks = '';
539					var body = '';
540					var head = '';
541					var stage = $('<div class="events2-reg-thanks-stage"></div>');
542
543					var toDo = finalBookings.length;
544					var cache = [];
545					var bookings = [];
546					var bookingTransactions = [];
547
548					self.paymentTypeForm.hide();
549					self.reserveScreen.hide();
550					self.confirmScreen.hide();
551					self.screenThankyou.show().empty();
552
553					// Setup a new stage, as we don't want the message.
554					if (runCharges) {
555						self.PayPalPayFlow.showProcessingAnimation($('.amnp-booker-holder'));
556					} else {
557						// We only want to show these once
558						stage.append('<div class="events2-reg-thanks-title">Your room booking information</div>');
559					}
560
561					var foot = $('<div class="events2-reg-thanks-online">' +
562						'	<div class="events2-reg-thanks-online-title">View online</div>' +
563						'	<div>You can manage your room bookings at:</div>' +
564						'	<div><a href="/myreservations">' + window.location.hostname + '/myreservations</a></div>' +
565						'</div>');
566
567					var sendBooking = function(booking, index) {
568						return $.ajax({
569							url: '/reserve?action=submit',
570							data: booking,
571							dataType: 'json',
572							type: 'POST',
573						}).then(function (data) {
574							if (runCharges && booking.paymentDetails && booking.paymentDetails.amt > 0) {
575								data.amt = booking.paymentDetails.amt;
576								data.paymentType = self.bookingPaymentType; // booking.paymentType;
577								bookingTransactions.push({ json: data, booking: self.bookings[index] });
578							}
579
580							// Cache should include all bookings, not just charged ones.
581							// merge the data with bookings data - regardless if new or not, we don't really care either way
582							for (var key in data) {
583								self.bookings[index].data[key] = data[key];
584							}
585							var cacheData = self.bookings[index].data;
586							cacheData.loc = JSON.parse(JSON.stringify(self.bookings[index].location.name));
587							cache.push(cacheData);
588
589							// Bookings needs to include all as well.
590							bookings.push({ json: data, booking: self.bookings[index] });
591							return cache;
592						});
593					};
594
595					// Send bookings sequentially
596					finalBookings.reduce(function(acc, nextBooking, index) {
597						return acc.then(function() {
598							return sendBooking(nextBooking, index);
599						});
600					}, Promise.resolve(cache)).then(function () {
601						stage.empty();
602
603						for (var b = 0; b < bookings.length; b++) {
604							var book = bookings[b];
605							// Get a formatted block per registration, this allows us to make a nicely designed 'stage'
606							blocks = self._getBookingMessageBlock(book.booking, book.json);
607							body = $('<div class="events2-reg-thanks-body"></div>');
608							head = $('<div class="events2-reg-thanks-head"></div>');
609							body.append(blocks.body);
610							head.append(blocks.head);
611							stage.append(head).append(body);
612						}
613
614						if (runCharges && bookingTransactions && bookingTransactions.length > 0) {
615							if (parseInt(self.settings.chargingMode, 10) === 1) {
616								$.post('/reserve', { action: 'set_booking_cache', bookingCache: cache }, function (cacheResponse) {
617									if (typeof cacheResponse.id !== 'undefined') {
618										new Promise(function (resolve, reject) {
619											if (typeof self.PayPalPayFlow !== 'undefined') {
620												if (bookingTransactions.length > 0) {
621													var paymentDetails = {
622														first_name: $('input#card_first_name', self.cardForm).val(),
623														last_name: $('input#card_last_name', self.cardForm).val(),
624														card_number: $('input#card_number', self.cardForm).val(),
625														expiration: $('input#expiration', self.cardForm).payment('cardExpiryVal'),
626														cvc: $('input#CVV2', self.cardForm).val(),
627														email: $('input#contact_email').val()
628													};
629
630													paymentDetails.expiration = ("0" + paymentDetails.expiration.month.toString() + paymentDetails.expiration.year.toString().substr(2)).slice(-4);
631													paymentDetails.card_number = paymentDetails.card_number.replace(/\s+/g, '');
632
633													var transactionData = {
634														'AMT': 0.00,
635														'CVV2': paymentDetails.cvc,
636														'EXPDATE': paymentDetails.expiration,
637														'ACCT': paymentDetails.card_number,
638														'BILLTOFIRSTNAME': paymentDetails.first_name,
639														'BILLTOLASTNAME': paymentDetails.last_name,
640														'EMAIL': paymentDetails.email,
641														'CURRENCY': self.settings.payment_currency ? self.settings.payment_currency : 'USD',
642														'CLIENT': self.settings.client
643													};
644
645													self.PayPalPayFlow.sendReserveTransaction(transactionData, bookingTransactions, cacheResponse.id).then(function (response) {
646														// If we are back here, then we haven't browsed away, something is wrong with the bookings.
647														if (!response) {
648															reject(false);
649														} else {
650															resolve();
651														}
652													});
653												} else {
654													reject('Bookings array is empty.');
655												}
656											} else {
657												reject('Form is broken.');
658											}
659										})
660										.then(function () {
661											// do nothing on success
662											return;
663										})
664										.catch(function (response) {
665											console.error(response);
666											// If we are are here, then just draw the page as normal with whatever errors appear
667											if (self.PayPalPayFlow) self.PayPalPayFlow.hideProcessingAnimation();
668
669											self.screenThankyou.empty();
670											self.$element.trigger('booker:reset', true);
671											self.screenThankyou.append(stage).append(foot);
672										});
673									}
674								}, 'json');
675							} else {
676								// We've finished all the main drawing, time to append THANKS!
677								if (self.PayPalPayFlow) self.PayPalPayFlow.hideProcessingAnimation();
678
679								self.screenThankyou.empty();
680								self.$element.trigger('booker:reset', true);
681								self.screenThankyou.append(stage).append(foot);
682							}
683						} else {
684							// We've finished all the main drawing, time to append THANKS!
685							if (self.PayPalPayFlow) self.PayPalPayFlow.hideProcessingAnimation();
686
687							self.screenThankyou.empty();
688							self.$element.trigger('booker:reset', true);
689							self.screenThankyou.append(stage).append(foot);
690						}
691					}).catch(function(reason){
692						console.error(reason);
693					});
694				}
695			});
696
697			self.holder.on('click', '.amnp-back-to-picker', function () {
698				self.$element.trigger('booker:closed', true);
699			}).on('change', '.agreed', function (event) {
700				event.preventDefault();
701				if ($(this).is(':checked')) {
702					$('.amnp-to-next-screen', self.reserveScreen).removeProp('disabled');
703					self.currentBooking.agreed = true;
704				} else {
705					self.currentBooking.agreed = false;
706					$('.amnp-to-next-screen', self.reserveScreen).prop('disabled', 'true');
707				}
708			}).on('click', '.amnp-back-to-reserve', function (event) {
709				event.preventDefault();
710				self._back();
711			}).on('click', '.amnp-lookup-history', function (event) {
712				var search = $('#contact_first_name', self.holder).val();
713				search += ' ' + $('#contact_last_name', self.holder).val();
714				openDialog('/ajax/fetch/patron_finder?embedded=1&include_past=1&term=' + search + '&type=roombooking', 'Look up', function () {
715					addButton('.gldbuttons', '<i class="fa fa-times"></i>', 'btn-edit', function () { closeDialog(false); });
716				}, function (status) {
717				});
718
719			}).on('click', '.amnp-to-next-screen', function (event) {
720				event.preventDefault();
721
722				var reserveWindow = document.getElementsByClassName('amnp-booker-holder');
723				if(reserveWindow[0] !== undefined) reserveWindow[0].scrollIntoView({behavior: "smooth"});
724
725				if (!self.currentBooking) self.currentBooking = self.bookings[0];
726				if (self.settings.mode === 1) {
727					var bookingClass = $('#booking_type', self.holder).find(':selected').data('details');
728					self.currentBooking.type = parseInt(bookingClass.type, 10) === 0 ? 1 : 2;
729					var paymentType = $('#payment_type', self.holder).val();
730					self.currentBooking.data.paymentType = paymentType;
731					self.currentBooking.class_type = bookingClass.type;
732					self.currentBooking.class_id = bookingClass.id;
733				}
734				self.currentBooking.data.contact.first_name = $('#contact_first_name', self.holder).val();
735				self.currentBooking.data.contact.last_name = $('#contact_last_name', self.holder).val();
736				self.currentBooking.data.contact.phone = $('#contact_tel', self.holder).val();
737				self.currentBooking.data.contact.allow_sms = $('#contact_allow_sms', self.holder).length > 0 ? $('#contact_allow_sms', self.holder).is(":checked") : 0;
738				self.currentBooking.data.contact.email = $('#contact_email', self.holder).val();
739				self.currentBooking.data.contact.librarycard = $('#librarycard', self.holder).val();
740				self.currentBooking.data.contact.group_name = $('#group_name', self.holder).val();
741				self.currentBooking.data.contact.booking_title = $('#booking_title', self.holder).val();
742
743				var patron = $(self.holder).data('patron');
744				if (patron && typeof patron.mediationRequired !== 'undefined' && patron.mediationRequired !== null) {
745					self.currentBooking.data.mediationRequired = patron.mediationRequired ? 1 : 0;
746				}
747
748				$('input#card_first_name', self.cardForm).val(self.currentBooking.data.contact.first_name);
749				$('input#card_last_name', self.cardForm).val(self.currentBooking.data.contact.last_name);
750
751				self.currentBooking.data.expected_attendees = parseInt($('#expected_attendees', self.holder).val() || 0, 10); //TODO: we still need this
752				self.currentBooking.data.patron_notes = $('#patron_notes', self.holder).val();
753				if (self.settings.mode === 1 && self.settings.enableStaffNotes) {
754					self.currentBooking.data.staff_notes = $('#staff_notes', self.holder).val() || '';
755				}
756				self.currentBooking.data.customQuestions = {};
757				var reemail = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
758				var rephone = new RegExp(self.settings.phone_number_regex, "i");
759				var roomMax = Math.max(parseInt(self.currentBooking.room.capacity_standing, 10), parseInt(self.currentBooking.room.capacity_chairs, 10), parseInt(self.currentBooking.room.capacity_tables, 10));
760				if (self.currentBooking.data.layout_id) {
761					$.each(self.currentBooking.room.layouts, function (idx, layout) {
762						if (layout.id === self.currentBooking.data.layout_id) {
763							roomMax = parseInt(layout.capacity, 10);
764						}
765					});
766				}
767				if (self.settings.mode !== 1) {
768					if (!self._isFormClean(self.reserveScreen)) {
769						$('.amnp-error-message', self.holder).text('Please enter information for all required fields').fadeIn().delay(6000).fadeOut();
770						return;
771					}
772					// CH#1957 - CK - Fix for when max is 0 to ignore the message.
773					if (self.settings.enforceMaxAttendees && (self.currentBooking.data.expected_attendees > roomMax) && roomMax > 0) {
774						$('.amnp-error-message', self.holder).text('Expected attendees exceeds capacity').fadeIn().delay(6000).fadeOut();
775						$('#expected_attendees', self.holder).addClass("hightlight-field").delay(6000).queue(function () {
776							$(this).removeClass("hightlight-field").dequeue();
777						});
778						return;
779					} else if (self.settings.enableExpectedAttendees && self.settings.expectedAttendeesRequired && self.currentBooking.data.expected_attendees <= 0) {
780						$('.amnp-error-message', self.holder).text('Expected attendees cannot be less than one').fadeIn().delay(6000).fadeOut();
781						$('#expected_attendees', self.holder).addClass("hightlight-field").delay(6000).queue(function () {
782							$(this).removeClass("hightlight-field").dequeue();
783						});
784						return;
785					}
786					if (!$('#contact_email', self.holder).prop('disabled') && self.currentBooking.data.contact.email.length > 0 && !reemail.test(self.currentBooking.data.contact.email)) {
787						$('.amnp-error-message', self.holder).text('Please enter a valid email address').fadeIn().delay(6000).fadeOut();
788						$('#contact_email', self.holder).addClass("hightlight-field").delay(6000).queue(function () {
789							$(this).removeClass("hightlight-field").dequeue();
790						});
791						return;
792					}
793					if (!$('#contact_tel', self.holder).prop('disabled') && self.currentBooking.data.contact.phone.length > 0 && !rephone.test(self.currentBooking.data.contact.phone)) {
794						$('.amnp-error-message', self.holder).text('Please enter a valid phone number').fadeIn().delay(6000).fadeOut();
795						$('#contact_tel', self.holder).addClass("hightlight-field").delay(6000).queue(function () {
796							$(this).removeClass("hightlight-field").dequeue();
797						});
798
799						return;
800					}
801
802					if (self.currentBooking.room.custom_fields && self.currentBooking.room.custom_fields.fields && self.currentBooking.room.custom_fields.fields.length > 0) {
803						for (var i = self.currentBooking.room.custom_fields.fields.length - 1; i >= 0; i--) {
804							var field = self.currentBooking.room.custom_fields.fields[i];
805							var val = false;
806							switch (field.type) {
807								case 'checkbox':
808									val = $('#' + field.name, self.customQuestions).is(':checked');
809									break;
810								default:
811									val = $('#' + field.name, self.customQuestions).val();
812									break;
813							}
814							switch (field.validate) {
815								case 'custom':
816									var flags = field.regex.replace(/.*\/([gimy]*)$/, '$1');
817									var pattern = field.regex.replace(new RegExp('^/(.*?)/' + flags + '$'), '$1');
818									var regex = new RegExp(pattern, flags);
819									if (val.length > 0 && !regex.test(val)) {
820										$('.amnp-error-message', self.holder).text('The information entered is not valid').fadeIn().delay(6000).fadeOut();
821										$('#' + field.name, self.holder).addClass("hightlight-field").delay(6000).queue(function () {
822											$(this).removeClass("hightlight-field").dequeue();
823										});
824										return;
825									}
826									break;
827								case 'email':
828									if (val.length > 0 && !reemail.test(val)) {
829										$('.amnp-error-message', self.holder).text('Please enter a valid email address').fadeIn().delay(6000).fadeOut();
830										$('#' + field.name, self.holder).addClass("hightlight-field").delay(6000).queue(function () {
831											$(this).removeClass("hightlight-field").dequeue();
832										});
833										return;
834									}
835									break;
836								case 'phone':
837									if (val.length > 0 && !rephone.test(val)) {
838										$('.amnp-error-message', self.holder).text('Please enter a valid phone number').fadeIn().delay(6000).fadeOut();
839										$('#' + field.name, self.holder).addClass("hightlight-field").delay(6000).queue(function () {
840											$(this).removeClass("hightlight-field").dequeue();
841										});
842										return;
843									}
844									break;
845							}
846							// An unticked checkbox is a real answer, so record it rather than dropping it,
847							// otherwise 'said no' and 'was never asked' look identical in the stored answers.
848							if (field.type === 'checkbox' || val) self.currentBooking.data.customQuestions[field.name] = val;
849						}
850					}
851
852					self._next();
853				} else {
854					if (self.currentBooking.room.custom_fields && self.currentBooking.room.custom_fields.fields && self.currentBooking.room.custom_fields.fields.length > 0) {
855						for (var i = self.currentBooking.room.custom_fields.fields.length - 1; i >= 0; i--) {
856							var field = self.currentBooking.room.custom_fields.fields[i];
857							var val = false;
858							switch (field.type) {
859								case 'checkbox':
860									val = $('#' + field.name, self.customQuestions).is(':checked');
861									break;
862								default:
863									val = $('#' + field.name, self.customQuestions).val();
864									break;
865							}
866							// An unticked checkbox is a real answer, so record it rather than dropping it,
867							// otherwise 'said no' and 'was never asked' look identical in the stored answers.
868							if (field.type === 'checkbox' || val) self.currentBooking.data.customQuestions[field.name] = val;
869						}
870					}
871					if (self.settings.enforceMaxAttendees && (self.currentBooking.data.expected_attendees > roomMax && roomMax > 0)) {
872						showAlert({
873							title: 'Warning',
874							message: 'expected attendees exceeds capacity',
875							type: 'warning'
876						});
877					}
878
879					if (parseInt(self.currentBooking.class_type, 10) > 0 && (self.currentBooking.data.contact.librarycard && self.currentBooking.data.contact.librarycard.length > 0 || self.currentBooking.data.contact.email && self.currentBooking.data.contact.email.length > 0)) {
880						$.getJSON('/ajax/fetch/check_room_booking', { room_id: self.currentBooking.data.room_id, start_time: self.currentBooking.data.start_time, end_time: self.currentBooking.data.end_time, librarycard: self.currentBooking.data.contact.librarycard, email: self.currentBooking.data.contact.email, class_id: self.currentBooking.class_id }, function (json) {
881							if (!json.ok) {
882								if (self.settings.staffRespectPatronLimit) {
883									showAlert({
884										title: 'This will exceed the patrons booking limit',
885										message: json.message,
886										type: 'error'
887									});
888									return;
889								} else {
890									if (json.far) {
891										var far = parseInt(self.currentBooking.room.restrictions.furthest_booking_days, 10);
892										showAlert({
893											title: 'This will exceed the furthest booking day for the room (' + far + ' days)',
894											message: json.message,
895											type: 'warning'
896										});
897									} else {
898										showAlert({
899											title: 'This will exceed the patrons booking limit',
900											message: json.message,
901											type: 'warning'
902										});
903									}
904								}
905							}
906							self._next();
907						});
908					} else {
909						self._next();
910					}
911				}
912			});
913			if (self.settings.mode === 1) {
914				self.reserveScreen.on('change', '#booking_type', function (event) {
915					event.preventDefault();
916					var details = $(this).find(':selected').data('details');
917					var locRooms = self.bookings.reduce(function (ret, booking) {
918						if (!ret[booking.room.location_id]) ret[booking.room.location_id] = [];
919						ret[booking.room.location_id].push(booking.room.id);
920						return ret;
921					}, {});
922					for (var loc in locRooms) {
923
924						self._getJSON(self.settings.apiServer + '/v2/' + self.settings.client + '/rooms/' + loc, { room_id: locRooms[loc].join(','), class_id: $('#booking_type', self.reserveScreen).val() }).then(function (json) {
925							json.forEach(function (roomData) {
926								self.bookings.forEach(function (booking) {
927									if (booking.room.id == roomData.id) {
928										booking.room = roomData;
929									}
930								});
931
932								// CH14585 - Fix for issues
933								self._updateCustomQuestions(null, null);
934							});
935							if (self.staffPaymentTypeField) self.staffPaymentTypeField.toggle(self._isChargable());
936							if (parseInt(details.type, 10) === 0) {
937								self.cardlookup.hide();
938								self._getJSON('/ajax/fetch/logged_in_data').then(function (json) {
939									self.userData = json;
940									$('#contact_first_name', self.reserveScreen).val(self.userData.first_name);
941									$('#contact_last_name', self.reserveScreen).val(self.userData.last_name);
942									$('#contact_email', self.reserveScreen).val(self.userData.username);
943								});
944							} else {
945								$('#contact_first_name', self.reserveScreen).val('');
946								$('#contact_last_name', self.reserveScreen).val('');
947								$('#contact_email', self.reserveScreen).val('');
948								self.cardlookup.show();
949							}
950							$('.amnp-booking-summary', self.reserveScreen).replaceWith(self._getBookingSummeryDetails(self.currentBooking));
951						}).catch(function (err) {
952							console.log(err);
953						});
954					}
955				}).on('click', '.events2-lookup', function (event) {
956					event.preventDefault();
957					var b = $(this).button('loading');
958					var card = $('#login_card', self.holder).val();
959					self._getJSON('/ajax/fetch/patron_info', {
960						barcode: card,
961						type: 'reserve'
962					}).then(function (json, textStatus) {
963						$('#contact_first_name', self.holder).val('');
964						$('#contact_last_name', self.holder).val('');
965						$('#contact_tel', self.holder).val('');
966						$('#contact_allow_sms', self.holder).val('');
967						$('#contact_email', self.holder).val('');
968						$('#librarycard', self.holder).val('');
969						if (json.result === 'ok') {
970							if (json.data.names.length > 0) {
971								$('#contact_first_name', self.holder).val(json.data.names[0].first_name);
972								$('#contact_last_name', self.holder).val(json.data.names[0].last_name);
973							}
974							if (json.data.phones.length > 0) {
975								$('#contact_tel', self.holder).val(json.data.phones[0].replace(/\W/g, ''));
976							}
977							if (json.data.emails.length > 0) {
978								$('#contact_email', self.holder).val(json.data.emails[0]);
979							}
980							if (json.data.barcodes.length > 0) {
981								$('#librarycard', self.holder).val(json.data.barcodes[0]);
982							}
983							if (json.prefixBlocked) {
984								showAlert({
985									title: 'Warning',
986									message: 'This card is not allowed to reserve rooms',
987									type: 'warning'
988								});
989							}
990						} else {
991							showAlert({
992								title: 'Failed',
993								message: 'Sorry, pin or card number not found',
994								type: 'warning'
995							});
996						}
997						b.button('reset');
998					}).catch(function (err) {
999						showAlert({
1000							title: 'Error',
1001							message: 'Sorry, there was a problem making the request',
1002							type: 'error'
1003						});
1004						b.button('reset');
1005					});
1006				}).on('keydown', '.events2-lookup', function (event) {
1007					var key = event.key;
1008					var keyCode = event.which || event.keyCode;
1009					if (key === 'Enter' || key === ' ' || key === 'Spacebar' || keyCode === 13 || keyCode === 32) {
1010						event.preventDefault();
1011						$(this).trigger('click');
1012					}
1013				});
1014			} else {
1015				self.loginForm.on('click', '.amnp-lookup', function (event) {
1016					event.preventDefault();
1017					var b = $(this).button('loading');
1018					// CH#1942 - Trim the card/pin fields to prevent blanks.
1019					var card = $.trim($('#login_card', self.holder).val());
1020					var loginData = { u: card, 'type': 'reserve' };
1021					if (!self.settings.pinlessAuth) {
1022						loginData.p = $.trim($('#login_pin', self.holder).val());
1023					}
1024					var locRooms = self.bookings.reduce(function (ret, booking) {
1025						ret.push(booking.room.id);
1026						return ret;
1027					}, []);
1028					if(locRooms.length > 0) {
1029						loginData.rooms = locRooms.join(',');
1030					}
1031					self._getJSON(self.settings.apiServer + '/v1/' + self.settings.client + '/patron', loginData).then(function (json, textStatus) {
1032						$(self.holder).removeData('patron');
1033						$('#contact_first_name', self.holder).val('');
1034						$('#contact_last_name', self.holder).val('');
1035						$('#contact_tel', self.holder).val('');
1036						$('#contact_allow_sms', self.holder).val('');
1037						$('#contact_email', self.holder).val('');
1038						$('#librarycard', self.holder).val('');
1039						if (json.result == 'ok') {
1040							$(self.holder).data('patron', json.data);
1041							if (!self.settings.pinlessAuth) {
1042								if (json.data.names.length > 0) {
1043									$('#contact_first_name', self.holder).val(json.data.names[0].first_name);
1044									$('#contact_last_name', self.holder).val(json.data.names[0].last_name);
1045									if (self.settings.roomsPatronDetailsReadonly) {
1046										$('#contact_first_name', self.holder).prop('disabled', true);
1047										$('#contact_last_name', self.holder).prop('disabled', true);
1048									}
1049								}
1050								if (json.data.phones.length > 0) {
1051									$('#contact_tel', self.holder).val(json.data.phones[0].replace(/\W/g, ''));
1052									if (self.settings.roomsPatronDetailsReadonly) {
1053										$('#contact_tel', self.holder).prop('disabled', true);
1054									}
1055								}
1056								if (json.data.emails.length > 0) {
1057									$('#contact_email', self.holder).val(json.data.emails[0]);
1058									if (self.settings.roomsPatronDetailsReadonly) {
1059										$('#contact_email', self.holder).prop('disabled', true);
1060									}
1061								}
1062							}
1063							if (json.data.barcodes.length > 0) {
1064								$('#librarycard', self.holder).val(json.data.barcodes[0]);
1065							}
1066						if (self.settings.loginRequired) {
1067							self.reserveScreen.removeClass('amnp-login-required');
1068							self.loginForm.hide();
1069							self.detailsForm.show();
1070						}
1071						} else {
1072							var msg = self.settings.pinlessAuth ? 'Sorry, card number not found' : 'Sorry, pin or card number not found';
1073							if (json.message && json.message.length > 0) {
1074								msg = json.message;
1075							}
1076							$('.amnp-error-message', self.holder).text(json.message || msg).fadeIn().delay(6000).fadeOut();
1077						}
1078						b.button('reset');
1079					}).catch(function (err) {
1080						$('.amnp-error-message', self.holder).text('Sorry, there was a problem making the request').fadeIn().delay(6000).fadeOut();
1081						b.button('reset');
1082					});
1083				});
1084			}
1085		},
1086		// Formatting block for CH#327
1087		_getBookingMessageBlock: function (booking, json) {
1088			// Booking - Room booking?
1089			// JSON - Custom object { amt(payment figure float), paymentType('card') }
1090			var self = this;
1091
1092			var message = '<i class="fa fa-check-circle"></i>&nbsp;Reservation For: ';
1093			var ref = (self.settings.mode !== 1) ? ' <div class="events2-reg-thanks-ref">Ref:<span class="events2-reg-thanks-ref-num">' + json.reference + '</span></div>' : '';
1094
1095			var costMessage = '';
1096			var mediatedMessage = '';
1097			if (!json.ok) {
1098				message = '<span style="color:#ED0202;"><i class="fa fa-times-circle"></i>&nbsp;' + json.message + '</span>';
1099				ref = '';
1100			}
1101			var lock = '';
1102
1103			if (booking.room.unmediated === false || booking.data.mediationRequired === 1) {
1104				// Mediated
1105				mediatedMessage = '<div class="amrp-status-inreview"><i class="fa fa-info-circle"></i>' + '&nbsp' + self.settings.rooms_mediated_booking_message + '</div>';
1106				if (json.lock_code && json.lock_code.length > 0 && self.settings.rooms_remotelock_show_on_confirmationscreen_mediated) {
1107					lock = '<div class="events2-reg-thanks-pin"><span><i class="am-unlock"></i>Door code: </span>' + json.lock_code + '</div>' +
1108						'<div class="events2-reg-thanks-pin-message"><i class="fa fa-info-circle"></i>' +
1109						'<div class="events2-reg-thanks-pin-message-title">This room has a door lock.</div>' +
1110						'<div class="events2-reg-thanks-pin-message-body">You will need to enter the ' + json.lock_code.length + ' digit door code to unlock the room. Note: The code will not work before your room booking start time.</div>' +
1111						'</div>';
1112				}
1113			} else {
1114				// Unmediated
1115				if (json.lock_code && json.lock_code.length > 0) {
1116					lock = '<div class="events2-reg-thanks-pin"><span><i class="am-unlock"></i>Door code: </span>' + json.lock_code + '</div>' +
1117						'<div class="events2-reg-thanks-pin-message"><i class="fa fa-info-circle"></i>' +
1118						'<div class="events2-reg-thanks-pin-message-title">This room has a door lock.</div>' +
1119						'<div class="events2-reg-thanks-pin-message-body">You will need to enter the ' + json.lock_code.length + ' digit door code to unlock the room. Note: The code will not work before your room booking start time.</div>' +
1120						'</div>';
1121				}
1122			}
1123
1124			if (json.ok && this._isChargable()) {
1125				var cost = this._calculateBookingCharge(booking, true);
1126				if (cost > 0) {
1127					if (this.bookingPaymentType === 'card') {
1128						costMessage = '<div class="events2-cost-msg">The total cost of this booking was ' + this.settings.payment_currency_symbol + '' + this._formatCurrency(cost);
1129						costMessage += ((booking.room.unmediated === false || booking.data.mediationRequired === 1) ? ', payment will be collected once the booking is confirmed.' : '');
1130						costMessage += '</div>';
1131					} else if (this.bookingPaymentType === 'offline') {
1132						costMessage = '<div class="events2-cost-msg">The total cost of this booking was ' + this.settings.payment_currency_symbol + '' + this._formatCurrency(cost) + '.</div>';
1133					}
1134				}
1135			}
1136
1137			var thanksHead = $('<div class="events2-reg-thanks-evtitle">' + booking.room.title + '</div>' +
1138				'<div class="events2-reg-thanks-evlocation"><i class="fa fa-map-marker"></i>&nbsp;' + booking.location.name + '</div>');
1139
1140
1141			var thanksBody = $('<div class="events2-reg-thanks-event">' +
1142				'	<div class="events2-reg-thanks-sub-title">' + message + '</div>' +
1143				(json.ok ? mediatedMessage : "") +
1144				'	<div class="events2-reg-thanks-evdate">' + booking.startTime.format('MMMM DD, YYYY') + '</div>' +
1145				'	<div class="events2-reg-thanks-evtime">' + booking.startTime.format('h:mma') + ' - ' + booking.endTime.format('h:mma') + '</div>' +
1146				ref +
1147				lock +
1148				costMessage +
1149				'</div>');
1150
1151			if(self.settings.mode === 1) {
1152				var closeButton = $('<button style="margin-top: 30px;" class="btn btn-info">Close</button>').appendTo(thanksBody);
1153
1154				closeButton.on('click', function (event) {
1155					event.preventDefault();
1156					event.stopPropagation();
1157
1158					self.$element.trigger('booker:reset', true);
1159					self.$element.trigger('booker:closed', true);
1160				});
1161			}
1162
1163			return { head: thanksHead, body: thanksBody };
1164		},
1165		_getBookingSummeryDetails: function (booking, forcePrice) {
1166			var $bookingSummary = $('<div class="amnp-booking-summary"></div>');
1167			var $bookingSummaryDetails = $('<div class="amnp-booking-summary-details"></div>').appendTo($bookingSummary);
1168			var startTime = booking.startTime;
1169			var endTime = booking.endTime;
1170
1171			if (booking.room.image && booking.room.image.length > 0) {
1172				$('<img/>').attr('src', '//' + this.settings.client + '.libnet.info/images/roombooking/' + this.settings.client + '/' + booking.room.image).attr('alt', booking.room.title).appendTo($bookingSummaryDetails);
1173			}
1174
1175			var title = $('<div class="amnp-booking-summary-title">' + booking.room.title + '</div>').appendTo($bookingSummaryDetails);
1176			if (!this.settings.singleBranch) {
1177				title.append('<span>, ' + booking.location.name + '</span>');
1178			}
1179			$('<div class="amnp-booking-summary-time">' + startTime.format('h:mma') + ' - ' + endTime.format('h:mma') + '</b>, ' + endTime.format('MMMM DD, YYYY') + '</div>').appendTo($bookingSummaryDetails);
1180
1181			var $bookingSummaryCost = $('<div class="amnp-booking-summary-total"></div>').appendTo($bookingSummary);
1182			var minutes = moment.duration(endTime.diff(startTime)).asMinutes();
1183			var realmin = minutes % 60
1184			var hours = Math.floor(minutes / 60);
1185			var displayText = '';
1186			if (hours > 0) {
1187				displayText = hours + (hours === 1 ? ' hour' : ' hours');
1188			}
1189			if (realmin > 0) {
1190				displayText += ' ' + realmin + (realmin === 1 ? ' minute' : ' minutes');
1191			}
1192			$('<div class="amnp-booking-summary-booking-name"></div>').text(displayText).appendTo($bookingSummaryCost);
1193			var costHolder = $('<div class="amnp-booking-summary-booking-cost"></div>').appendTo($bookingSummaryCost);
1194			if (this.settings.chargingEnabled || forcePrice) {
1195				costHolder.html(this.settings.payment_currency_symbol + '' + this._formatCurrency(this._calculateBookingCharge(booking, false)));
1196			}
1197			if (booking.data.assets.length > 0) {
1198				var $bookingSummaryAssets = $('<div class="amnp-booking-summary-assets"></div>').appendTo($bookingSummary);
1199				var self = this;
1200				booking.data.assets.forEach(function (asset) {
1201					var a = $('<div class="amnp-booking-summary-asset"></div>').appendTo($bookingSummaryAssets);
1202					$('<div class="amnp-booking-summary-booking-name"></div>').text(asset.name).appendTo(a);
1203					$('<div class="amnp-booking-summary-asset-qty"></div>').text('qty ' + asset.qty).appendTo(a);
1204					var costHolder = $('<div class="amnp-booking-summary-booking-cost"></div>').appendTo(a);
1205					if ((self.settings.mode !== 1 && self.settings.chargingEnabled) || forcePrice) {
1206						costHolder.html(self.settings.payment_currency_symbol + '' + self._formatCurrency(parseFloat(asset.cost) * asset.qty));
1207					}
1208					$('<div class="amnp-booking-summary-booking-remove link">remove</div>').data('asset', asset).appendTo(a);
1209				});
1210			}
1211
1212			var $bookingSummaryTotal = $('<div class="amnp-booking-summary-total"></div>').appendTo($bookingSummary);
1213			var $subTotal = $('<div class="amnp-booking-summary-booking-subtotal"></div>').appendTo($bookingSummaryTotal);
1214			var costHolder = $('<div class="amnp-booking-summary-booking-cost"></div>').appendTo($bookingSummaryTotal);
1215			$('<div class="amnp-booking-summary-booking-remove link">remove</div>').data('booking', booking).appendTo($bookingSummaryTotal);
1216			if (this.settings.chargingEnabled) {
1217				$subTotal.text('sub total');
1218				costHolder.html(this.settings.payment_currency_symbol + '' + this._formatCurrency(this._calculateBookingCharge(booking, true)))
1219			}
1220			return $bookingSummary;
1221		},
1222		_getBookingHeader: function (booking) {
1223			var $bookingHeader = $('<div></div>');
1224
1225			var title = $('<div class="amnp-booker-summary"><div class="amnp-booker-title"><i class="am-locations"><b>' + booking.room.title + '</b></div>' +
1226				'<b>' + booking.startTime.format('h:mma') + ' - ' + booking.endTime.format('h:mma') + '</b>, ' + booking.endTime.format('MMMM DD, YYYY') +
1227				'<div class="amnp-booker-time"/></div>').appendTo($bookingHeader);
1228
1229			if (!this.settings.singleBranch) {
1230				$('.amnp-booker-title', title).append('<span>, ' + booking.location.name + '</span>');
1231			}
1232			return $bookingHeader;
1233		},
1234		_updateCustomQuestions: function ($form, last) {
1235			var customQuestions = null;
1236			var self = this;
1237			var pos = self.bookings.indexOf(self.currentBooking);
1238			var lastBooking;
1239			var $bookingForm = null;
1240
1241			if (pos > 0 && last === null) {
1242				lastBooking = self.bookings[pos - 1];
1243			}
1244
1245			if ($form === null) {
1246				$bookingForm = $('<div class="amnp-booker-fields"></div>').data(self.currentBooking);
1247			} else {
1248				$bookingForm = $form;
1249			}
1250
1251			if ($('.amnp-custom-questions', $bookingForm).length > 0) {
1252				customQuestions = $('.amnp-custom-questions', $bookingForm);
1253				customQuestions.empty();
1254			} else {
1255				customQuestions = $('<div class="amnp-custom-questions"></div>').appendTo($bookingForm);
1256			}
1257
1258			if (this.currentBooking.room.custom_fields.fields) {
1259				$.each(this.currentBooking.room.custom_fields.fields, function (index, val) {
1260					var field = self._createFieldWithObject(val).data('fieldSettings', val).addClass('amnp-custom-field').appendTo(customQuestions);
1261					if (val.type === 'select') {
1262						var select = $('select', field);
1263						$('<option value="">Please choose...</option>').appendTo(select);
1264						var options = val.options.trim().split(/\r?\n/);
1265						for (var i = 0; i < options.length; i++) {
1266							$('<option>' + options[i] + '</option>').appendTo(select);
1267						}
1268					}
1269					switch (val.type) {
1270						case 'checkbox':
1271							break;
1272						default:
1273							if (self.currentBooking.data.customQuestions && self.currentBooking.data.customQuestions[val.name]) {
1274								$('#' + val.name, $bookingForm).val(self.currentBooking.data.customQuestions[val.name]);
1275							} else if (lastBooking && lastBooking.data.customQuestions && lastBooking.data.customQuestions[val.name]) {
1276								$('#' + val.name, $bookingForm).val(lastBooking.data.customQuestions[val.name]);
1277							}
1278							break;
1279						}
1280					});
1281			}
1282			$('.amnp-lookup', $bookingForm)
1283				.attr({ tabindex: 0, role: 'button' })
1284				.off('keydown.lookup')
1285				.on('keydown.lookup', function (event) {
1286					var key = event.key;
1287					var keyCode = event.which || event.keyCode;
1288					if (key === 'Enter' || key === ' ' || key === 'Spacebar' || keyCode === 13 || keyCode === 32) {
1289						event.preventDefault();
1290						$(this).trigger('click');
1291					}
1292				});
1293		}
1294		,
1295		_getBookingFields: function () {
1296			var pos = this.bookings.indexOf(this.currentBooking);
1297			var lastBooking;
1298			if (pos > 0) {
1299				lastBooking = this.bookings[pos - 1];
1300			}
1301
1302			var $bookingForm = $('<div class="amnp-booker-fields"></div>').data(this.currentBooking);
1303
1304			if(parseInt(this.currentBooking.room.group_name_enabled, 10) === 1){
1305				this._createField('group_name', 'text', 'Group name', true, false, '(This is <b>not displayed</b> to the public)').appendTo($bookingForm);
1306				var group_name = this.currentBooking.data.contact.group_name;
1307				if (group_name === "" && lastBooking) {
1308					group_name = lastBooking.data.contact.group_name
1309				}
1310				$('#group_name', $bookingForm).val(group_name);
1311			}
1312
1313			if(parseInt(this.currentBooking.room.booking_title_enabled, 10) === 1){
1314				this._createField('booking_title', 'text', 'Booking title', true, false, '(This <b>will be displayed</b> to the public)').appendTo($bookingForm);
1315				var booking_title = this.currentBooking.data.contact.booking_title;
1316				if (booking_title === "" && lastBooking) {
1317					booking_title = lastBooking.data.contact.booking_title
1318				}
1319				$('#booking_title', $bookingForm).val(booking_title);
1320			}
1321
1322			if (this.settings.enableExpectedAttendees) {
1323				this._createField('expected_attendees', 'number', 'Attendees', this.settings.expectedAttendeesRequired, false, 'Number of attendees expected.', 0).appendTo($bookingForm);
1324				var expected_attendees = this.currentBooking.data.contact.expected_attendees;
1325				if (expected_attendees === "" && lastBooking) {
1326					expected_attendees = lastBooking.data.contact.expected_attendees
1327				}
1328				$('#expected_attendees', $bookingForm).val(expected_attendees);
1329			}
1330
1331			if (this.settings.enablePatronNotes && parseInt(this.currentBooking.room.patron_notes_enabled, 10) === 1) {
1332				this._createField('patron_notes', 'textarea', 'Notes', false, false, false).appendTo($bookingForm);
1333				$('#patron_notes', $bookingForm).val(this.currentBooking.data.patron_notes);
1334			}
1335
1336			if (this.settings.mode === 1 && this.settings.enableStaffNotes) {
1337				this._createField('staff_notes', 'textarea', 'Staff notes', false, false, false).appendTo($bookingForm);
1338				$('#staff_notes', $bookingForm).val(this.currentBooking.data.staff_notes);
1339			}
1340
1341			if (this.currentBooking.room.custom_fields && this.currentBooking.room.custom_fields.fields && this.currentBooking.room.custom_fields.fields.length > 0) {
1342				this._updateCustomQuestions($bookingForm, lastBooking);
1343			}
1344			var backlabel = this.bookings.indexOf(this.currentBooking) === 0 ? 'Cancel' : 'Back';
1345			if (this.settings.mode === 1) {
1346				$('<div class="amnp-buttons"><button class="btn btn-edit amnp-lookup-history pull-left"><i class="am-search"/> Look up</button><button class="btn btn-edit amnp-back-to-reserve">' + backlabel + '</button><button class="btn btn-success amnp-to-next-screen">Next</button></div>').appendTo($bookingForm);
1347			} else {
1348				if (parseInt(this.currentBooking.room.tc_hidden, 10) === 0) {
1349					if (this.currentBooking.room.tc_link && this.currentBooking.room.tc_link.length > 0) {
1350						$('<label><input class="agreed" type="checkbox"> <span class="required-field"></span> I have read and agree to the room booking <a target="_blank" href="' + this.currentBooking.room.tc_link + '">terms and conditions</a></label>').appendTo($bookingForm);
1351					} else {
1352						$('<label><input class="agreed" type="checkbox"> <span class="required-field"></span> I have read and agree to the room booking terms and conditions</label>').appendTo($bookingForm);
1353					}
1354				} else {
1355					this.currentBooking.agreed = true;
1356				}
1357				$('<div class="amnp-buttons"><button class="btn btn-edit amnp-back-to-reserve">' + backlabel + '</button><button class="btn btn-success amnp-to-next-screen" disabled="true">Next</button></div>').appendTo($bookingForm);
1358
1359				if (this.currentBooking.agreed) {
1360					$('.agreed', $bookingForm).prop('checked', true);
1361					$('.amnp-to-next-screen', $bookingForm).removeProp('disabled');
1362				}
1363			}
1364
1365			return $bookingForm;
1366		},
1367		_isChargable: function () {
1368			return this._calculateTotalCharge() > 0;
1369		},
1370		_formatCurrency: function (amount) {
1371			return amount.toFixed(2).replace(/(\d)(?=(\d{3})+\.)/g, '$1,');
1372		},
1373		_calculateBookingCharge: function (booking, incAssets) {
1374			var roomTotal = parseFloat(booking.room.restrictions.charge_amount);
1375
1376			if (parseInt(booking.room.restrictions.charge_period) === 0) {
1377				var blockSize = parseInt(booking.room.restrictions.segment_size) * parseInt(booking.room.restrictions.block_count);
1378				var rawDuration = moment.duration(booking.endTime.diff(booking.startTime)).asMinutes();
1379				roomTotal = (rawDuration / blockSize) * roomTotal;
1380			}
1381
1382			if (booking.layout && booking.layout.cost && parseFloat(booking.layout.cost) > 0) {
1383				roomTotal += parseFloat(booking.layout.cost);
1384			}
1385			if (incAssets) {
1386				var assetCharge = booking.data.assets.reduce(function (total, asset) {
1387					return total + parseFloat(asset.cost) * asset.qty;
1388				}, 0);
1389			} else {
1390				var assetCharge = 0;
1391			}
1392			return parseFloat(roomTotal + assetCharge);
1393		},
1394		_calculateTotalCharge: function () {
1395			var self = this;
1396			var cartTotal = this.bookings.reduce(function (total, booking) {
1397				return total + self._calculateBookingCharge(booking, true);
1398			}, 0);
1399			return cartTotal;
1400		}
1401	});
1402	$.fn[pluginName] = function (options) {
1403		return this.each(function () {
1404			if (!$.data(this, "plugin_" + pluginName)) {
1405				$.data(this, "plugin_" + pluginName, new Plugin(this, options));
1406			}
1407		});
1408	};
1409})(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.