PageSourceSearch

https://www.andresoliveira.com.br/wp-content/plugins/motopress-appointment/assets/js/public.min.js

js andresoliveira.com.br collected 2026-10-06 13:13:42 UTC 227,442 bytes, 9,685 lines download raw bytes

1(function () {
2	'use strict';
3
4	let namespace = '/motopress/appointment/v1';
5
6	/**
7	 * @param {String} route
8	 * @param {Object} args Optional.
9	 * @param {String} type Optional. 'GET' by default.
10	 * @return {Promise}
11	 *
12	 * @since 1.0
13	 */
14	function mpa_rest_request(route, args = {}, type = 'GET') {
15	  return new Promise((resolve, reject) => {
16		wp.apiRequest({
17		  path: namespace + route,
18		  type: type,
19		  data: args
20		})
21		// Convert jqXHR object into Promise
22		.done(responseData => resolve(responseData)).fail((request, statusText) => {
23		  let message = 'parsererror'; // Default response for PHP error
24
25		  // Get error message
26		  if (request.responseJSON && request.responseJSON.message) {
27			message = request.responseJSON.message;
28		  } else {
29			message = `Status: ${statusText}`;
30		  }
31		  if (message == 'parsererror') {
32			message = 'REST request failed. Maybe PHP error on the server side. Check PHP logs.';
33		  }
34		  reject(new Error(message));
35		});
36	  });
37	}
38
39	/**
40	 * @param {String} route
41	 * @param {Object} args Optional.
42	 * @return {Promise}
43	 *
44	 * @since 1.0
45	 */
46	function mpa_rest_get(route, args = {}) {
47	  return mpa_rest_request(route, args, 'GET');
48	}
49
50	/**
51	 * @param {String} route
52	 * @param {Object} args
53	 * @return {Promise}
54	 *
55	 * @since 1.0
56	 */
57	function mpa_rest_post(route, args) {
58	  return mpa_rest_request(route, args, 'POST');
59	}
60
61	/**
62	 * @since 1.0
63	 */
64	class Settings {
65	  /**
66	   * @since 1.0
67	   */
68	  constructor() {
69		this.settings = this.getDefaults();
70		this.loadingPromise = this.load();
71	  }
72
73	  /**
74	   * @since 1.0
75	   *
76	   * @access protected
77	   *
78	   * @return {Object}
79	   */
80	  getDefaults() {
81		return {
82		  plugin_name: 'Appointment Booking',
83		  // No translation required
84		  today: '2030-01-01',
85		  business_name: '',
86		  default_time_step: 30,
87		  default_booking_status: 'confirmed',
88		  confirmation_mode: 'auto',
89		  terms_page_id_for_acceptance: 0,
90		  allow_multibooking: false,
91		  allow_coupons: false,
92		  allow_customer_account_creation: false,
93		  country: '',
94		  currency: 'EUR',
95		  currency_symbol: '€',
96		  currency_position: 'before',
97		  decimal_separator: '.',
98		  thousand_separator: ',',
99		  number_of_decimals: 2,
100		  date_format: 'F j, Y',
101		  time_format: 'H:i',
102		  week_starts_on: 0,
103		  thumbnail_size: {
104			width: 150,
105			height: 150
106		  },
107		  flatpickr_locale: 'en',
108		  enable_payments: false,
109		  active_gateways: [],
110		  payment_received_page_url: '',
111		  failed_transaction_page_url: '',
112		  default_payment_gateway: ''
113		};
114	  }
115
116	  /**
117	   * @return {Promise}
118	   *
119	   * @access protected
120	   *
121	   * @since 1.0
122	   */
123	  load() {
124		return new Promise((resolve, reject) => {
125		  mpa_rest_get('/settings?nocache').then(responseData => this.settings = responseData, error => console.error('Unable to load public settings.', error)).finally(() => resolve(this.settings));
126		});
127	  }
128
129	  /**
130	   * @return {Promise}
131	   *
132	   * @since 1.0
133	   */
134	  ready() {
135		return this.loadingPromise;
136	  }
137
138	  /**
139	   * @since 1.5.0
140	   *
141	   * @return {String}
142	   */
143	  getPluginName() {
144		return this.settings.plugin_name;
145	  }
146
147	  /**
148	   * @since 1.11.0
149	   *
150	   * @return {String} 'Y-m-d'
151	   */
152	  getBusinessDate() {
153		return this.settings.today;
154	  }
155
156	  /**
157	   * @return {String}
158	   */
159	  getBusinessName() {
160		return this.settings.business_name;
161	  }
162
163	  /**
164	   * @return {Number} Step for time slots (in minutes).
165	   *
166	   * @since 1.0
167	   */
168	  getTimeStep() {
169		return this.settings.default_time_step;
170	  }
171
172	  /**
173	   * @return {String} 'pending' or 'confirmed'.
174	   *
175	   * @since 1.1.0
176	   */
177	  getDefaultBookingStatus() {
178		return this.settings.default_booking_status;
179	  }
180
181	  /**
182	   * @since 1.5.0
183	   * @return {String} auto|manual|payment
184	   */
185	  getConfirmationMode() {
186		return this.settings.confirmation_mode;
187	  }
188
189	  /**
190	   * @since 1.10.2
191	   * @return {Integer} 0 | page id
192	   */
193	  getTermsPageIdForAcceptance() {
194		return this.settings.terms_page_id_for_acceptance;
195	  }
196
197	  /**
198	   * @since 1.4.0
199	   *
200	   * @return {Boolean}
201	   */
202	  isMultibookingEnabled() {
203		return this.settings.allow_multibooking;
204	  }
205
206	  /**
207	   * @since 1.11.0
208	   *
209	   * @return {Boolean}
210	   */
211	  isCouponsEnabled() {
212		return this.settings.allow_coupons;
213	  }
214
215	  /**
216	   * @since 1.18.0
217	   *
218	   * @return {Boolean}
219	   */
220	  isAllowCustomerAccountCreation() {
221		return this.settings.allow_customer_account_creation;
222	  }
223
224	  /**
225	   * @since 1.5.0
226	   *
227	   * @return {String}
228	   */
229	  getCountry() {
230		return this.settings.country;
231	  }
232
233	  /**
234	   * @return {String} Currency code, like "EUR".
235	   *
236	   * @since 1.0
237	   */
238	  getCurrency() {
239		return this.settings.currency;
240	  }
241
242	  /**
243	   * @return {String}
244	   *
245	   * @since 1.0
246	   */
247	  getCurrencySymbol() {
248		return this.settings.currency_symbol;
249	  }
250
251	  /**
252	   * @return {String}
253	   *
254	   * @since 1.0
255	   */
256	  getCurrencyPosition() {
257		return this.settings.currency_position;
258	  }
259
260	  /**
261	   * @return {String}
262	   *
263	   * @since 1.0
264	   */
265	  getDecimalSeparator() {
266		return this.settings.decimal_separator;
267	  }
268
269	  /**
270	   * @return {String}
271	   *
272	   * @since 1.0
273	   */
274	  getThousandSeparator() {
275		return this.settings.thousand_separator;
276	  }
277
278	  /**
279	   * @return {Number}
280	   *
281	   * @since 1.0
282	   */
283	  getDecimalsCount() {
284		return this.settings.number_of_decimals;
285	  }
286
287	  /**
288	   * @return {String}
289	   *
290	   * @since 1.0
291	   */
292	  getDateFormat() {
293		return this.settings.date_format;
294	  }
295
296	  /**
297	   * @return {String}
298	   *
299	   * @since 1.0
300	   */
301	  getTimeFormat() {
302		return this.settings.time_format;
303	  }
304
305	  /**
306	   * @return {Number}
307	   *
308	   * @since 1.0
309	   */
310	  getFirstDayOfWeek() {
311		return this.settings.week_starts_on;
312	  }
313
314	  /**
315	   * @since 1.4.0
316	   *
317	   * @return {Object} {width, height}
318	   */
319	  getThumbnailSize() {
320		return this.settings.thumbnail_size;
321	  }
322
323	  /**
324	   * @return {String}
325	   *
326	   * @since 1.2.1
327	   */
328	  getFlatpickrLocale() {
329		return this.settings.flatpickr_locale;
330	  }
331
332	  /**
333	   * @since 1.5.0
334	   *
335	   * @return {Boolean}
336	   */
337	  isPaymentsEnabled() {
338		return this.settings.enable_payments;
339	  }
340
341	  /**
342	   * @since 1.5.0
343	   *
344	   * @return {String[]}
345	   */
346	  getActiveGateways() {
347		return this.settings.active_gateways;
348	  }
349
350	  /**
351	   * @since 1.5.0
352	   *
353	   * @return {String}
354	   */
355	  getPaymentReceivedPageUrl() {
356		return this.settings.payment_received_page_url;
357	  }
358
359	  /**
360	   * @since 1.5.0
361	   *
362	   * @return {String}
363	   */
364	  getFailedTransactionPageUrl() {
365		return this.settings.failed_transaction_page_url;
366	  }
367
368	  /**
369	   * @since 1.6.2
370	   *
371	   * @return {String}
372	   */
373	  getDefaultPaymentGateway() {
374		return this.settings.default_payment_gateway;
375	  }
376	}
377
378	/**
379	 * @since 1.0
380	 */
381	class Plugin {
382	  /**
383	   * @since 1.0
384	   */
385	  constructor() {
386		this.settingsCtrl = new Settings();
387		this.loadingPromise = this.load();
388	  }
389
390	  /**
391	   * @return {Promise}
392	   *
393	   * @since 1.0
394	   */
395	  load() {
396		return Promise.all([this.settingsCtrl.ready()]).then(() => this);
397	  }
398
399	  /**
400	   * @return {Promise}
401	   *
402	   * @since 1.0
403	   */
404	  ready() {
405		return this.loadingPromise;
406	  }
407
408	  /**
409	   * @since 1.0
410	   */
411	  settings() {
412		return this.settingsCtrl;
413	  }
414
415	  /**
416	   * @since 1.0
417	   */
418	  static getInstance() {
419		if (Plugin.instance == undefined) {
420		  Plugin.instance = new Plugin();
421		}
422		return Plugin.instance;
423	  }
424	}
425
426	/**
427	 * @return {Plugin}
428	 *
429	 * @since 1.0
430	 */
431	function mpapp() {
432	  return Plugin.getInstance();
433	}
434
435	const localTranslate = (text, domain = '') => text;
436	const localTranslateWithContext = (text, context, domain = '') => text;
437	const localSprintf = (format, ...args) => {
438	  let argIndex = 0;
439	  return format.replace(/%([sdf])/g, (match, specifier) => {
440		if (argIndex >= args.length) return match;
441		let value = args[argIndex++];
442		switch (specifier) {
443		  case 's':
444			return String(value);
445		  case 'd':
446			return parseInt(value, 10);
447		  case 'f':
448			return parseFloat(value);
449		  default:
450			return match;
451		}
452	  });
453	};
454	const __ = typeof wp !== 'undefined' && wp.i18n && wp.i18n.__ ? wp.i18n.__ : localTranslate;
455	const _x = typeof wp !== 'undefined' && wp.i18n && wp.i18n._x ? wp.i18n._x : localTranslateWithContext;
456	typeof wp !== 'undefined' && wp.i18n && wp.i18n.sprintf ? wp.i18n.sprintf : localSprintf;
457
458	const locale = {
459	  weekdays: {
460		shorthand: [__('Sun', 'motopress-appointment'), __('Mon', 'motopress-appointment'), __('Tue', 'motopress-appointment'), __('Wed', 'motopress-appointment'), __('Thu', 'motopress-appointment'), __('Fri', 'motopress-appointment'), __('Sat', 'motopress-appointment')],
461		longhand: [__('Sunday', 'motopress-appointment'), __('Monday', 'motopress-appointment'), __('Tuesday', 'motopress-appointment'), __('Wednesday', 'motopress-appointment'), __('Thursday', 'motopress-appointment'), __('Friday', 'motopress-appointment'), __('Saturday', 'motopress-appointment')]
462	  },
463	  months: {
464		shorthand: [__('Jan', 'motopress-appointment'), __('Feb', 'motopress-appointment'), __('Mar', 'motopress-appointment'), __('Apr', 'motopress-appointment'),
465		// Translators: Month name (short variant, like "Apr").
466		_x('May', 'Month (short)', 'motopress-appointment'), __('Jun', 'motopress-appointment'), __('Jul', 'motopress-appointment'), __('Aug', 'motopress-appointment'), __('Sep', 'motopress-appointment'), __('Oct', 'motopress-appointment'), __('Nov', 'motopress-appointment'), __('Dec', 'motopress-appointment')],
467		longhand: [__('January', 'motopress-appointment'), __('February', 'motopress-appointment'), __('March', 'motopress-appointment'), __('April', 'motopress-appointment'), _x('May', 'Month', 'motopress-appointment'), __('June', 'motopress-appointment'), __('July', 'motopress-appointment'), __('August', 'motopress-appointment'), __('September', 'motopress-appointment'), __('October', 'motopress-appointment'), __('November', 'motopress-appointment'), __('December', 'motopress-appointment')]
468	  },
469	  amPM: ['AM', 'PM'],
470	  firstDayOfWeek: mpapp().settings().getFirstDayOfWeek()
471	};
472
473	/**
474	 * @return {String}
475	 *
476	 * @since 1.0
477	 */
478	function mpa_date_format() {
479	  return mpapp().settings().getDateFormat();
480	}
481
482	/**
483	 * Based on Flatpickr's formatDate().
484	 *
485	 * @see Flatpickr.formatDate(): https://github.com/flatpickr/flatpickr/blob/master/src/utils/formatting.ts
486	 * @see PHP date formatting tokens: https://www.php.net/manual/en/datetime.format.php
487	 *
488	 * @param {Date|String} date
489	 * @param {String} Optional. 'public', 'internal' ('Y-m-d') or custom date
490	 *     format. 'public' by default.
491	 * @return {String}
492	 *
493	 * @since 1.0
494	 */
495	function mpa_format_date(date, format = 'public') {
496	  if (typeof date == 'string') {
497		return date; // Already stringified
498	  }
499
500	  if (format == 'internal') {
501		return mpa_format_date(date, 'Y-m-d');
502	  } else if (format == 'public') {
503		return mpa_format_date(date, mpa_date_format());
504	  }
505
506	  // Handle custom date format
507	  let pad = (number, length = 2) => {
508		// Use at least two zeros for a padding to support milliseconds
509		return ('00' + number).slice(-length);
510	  };
511
512	  // Go through the each symbol
513	  let isSlashed = false;
514	  let dateString = format.split('').map(char => {
515		// Handle slashes '\'
516		if (isSlashed) {
517		  isSlashed = false;
518		  return char;
519		}
520
521		// Handle any other symbols
522		switch (char) {
523		  // Handle slashes '\'
524		  case '\\':
525			isSlashed = true;
526			return '';
527		  // Day
528		  case 'j':
529			// 1 to 31
530			return date.getDate();
531		  case 'd':
532			// 01 to 31
533			return pad(date.getDate());
534		  case 'D':
535			// Mon through Sun
536			return locale.weekdays.shorthand[date.getDay()];
537		  case 'l':
538			// Monday through Sunday
539			return locale.weekdays.longhand[date.getDay()];
540		  case 'N':
541			// 1 (Monday) to 7 (Sunday)
542			return date.getDay() || 7;
543		  case 'w':
544			// 0 (Sunday) to 6 (Saturday)
545			return date.getDay();
546		  case 'z':
547			// 0 to 365
548			// Thanks to Alex Turpin https://stackoverflow.com/a/8619946
549			let yearStart = new Date(date.getFullYear(), 0, 1);
550
551			// Compensate daylight savings time
552			let timezoneOffset = yearStart.getTimezoneOffset() - date.getTimezoneOffset();
553			let diffMilliseconds = date - yearStart + timezoneOffset * 60 * 1000;
554			let millisecondsInDay = 1000 * 60 * 60 * 24;
555			return Math.floor(diffMilliseconds / millisecondsInDay);
556
557		  // Week
558		  case 'W':
559			// 1 to 52
560			// Thanks to RobG https://stackoverflow.com/a/6117889
561			// Don't modify the original date
562			let cloneDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
563			// Use 7 as a day number for Sunday
564			let currentDay = cloneDate.getUTCDay() || 7;
565			// Set to nearest Thursday: %current date% + 4 - %current day%
566			cloneDate.setUTCDate(cloneDate.getUTCDate() + 4 - currentDay);
567			// Get the first day of the year
568			let utcYear = new Date(Date.UTC(cloneDate.getUTCFullYear(), 0, 1));
569			// Calculate full weeks to nearest Thursday
570			let oneDayMs = 1000 * 60 * 60 * 24;
571			let weekNo = Math.ceil(((cloneDate - utcYear) / oneDayMs + 1) / 7);
572			return weekNo;
573
574		  // Month
575		  case 'F':
576			// January through December
577			return locale.months.longhand[date.getMonth()];
578		  case 'M':
579			// Jan through Dec
580			return locale.months.shorthand[date.getMonth()];
581		  case 'm':
582			// 01 to 12
583			return pad(date.getMonth() + 1);
584		  case 'n':
585			// 1 to 12
586			return date.getMonth() + 1;
587		  case 't':
588			// 28 to 31
589			// Here is a trick. The days are 1-based. So when we pass 0 as a day number
590			// then JS Date object goes a day before (the last day of the previous month).
591			// So that's how it works: we select the 0-day of the next month and then go
592			// to the last day of the previous (required) month
593			let lastDate = new Date(date.getFullYear(), date.getMonth() + 1, 0);
594			return lastDate.getDate();
595
596		  // Year
597		  case 'Y':
598			// Examples: 1999 or 2003
599			return date.getFullYear();
600		  case 'y':
601			// Examples: 99 or 03
602			return String(date.getFullYear()).substring(2);
603		  case 'L':
604			// 1 (it's a leap year) or 0 (otherwise)
605			return date.getFullYear() % 4 == 0 ? 1 : 0;
606
607		  // Time
608		  case 'A':
609			// AM or PM
610			return locale.amPM[date.getHours() > 11 ? 1 : 0];
611		  case 'a':
612			// am or pm
613			return locale.amPM[date.getHours() > 11 ? 1 : 0].toLowerCase();
614		  case 'H':
615			// Hours, 00 to 23
616			return pad(date.getHours());
617		  case 'h':
618			// Hours, 01 to 12
619			return pad(date.getHours() % 12 || 12);
620		  case 'G':
621			// Hours, 0 to 23
622			return date.getHours();
623		  case 'g':
624			// Hours, 1 to 12
625			return date.getHours() % 12 || 12;
626		  case 'i':
627			// Minutes, 00 to 59
628			return pad(date.getMinutes());
629		  case 's':
630			// Seconds, 00 to 59
631			return pad(date.getSeconds());
632		  case 'v':
633			// Milliseconds, 000 to 999
634			return pad(date.getMilliseconds(), 3);
635		  case 'u':
636			// Microseconds, 000000 to 999999
637			return pad(date.getMilliseconds(), 3) + '000';
638		  // Partial support
639
640		  // Timezone
641		  case 'O': // +0200
642		  case 'P':
643			// +02:00
644			let offset = -date.getTimezoneOffset();
645			let sign = offset >= 0 ? '+' : '-';
646			let hours = Math.floor(Math.abs(offset) / 60);
647			let minutes = Math.abs(offset) % 60;
648			let separator = char == 'O' ? '' : ':';
649			return sign + pad(hours) + separator + pad(minutes);
650		  case 'Z':
651			// Offset in seconds, -43200 to 50400
652			return date.getTimezoneOffset() * 60;
653
654		  // Full date/time
655		  case 'U':
656			// Seconds since the Unix Epoch (January 1 1970 00:00:00 GMT)
657			return Math.floor(date.getTime() / 1000);
658		  case 'c':
vendor: 13,539 bytes, lines 659-1154
659			// ISO 8601 date, like: 2004-02-12T15:19:21+00:00
660
661			// date.toISOString() will return the string with the suffix
662			// '.065Z' (milliseconds) instead of '+02:00'
663			return mpa_format_date(date, 'Y-m-d\\TH:i:sP');
664		  case 'r':
665			// RFC 2822 date, like: Thu, 21 Dec 2000 16:01:07 +0200
666
667			// date.toUTCString() will return the string with the timezone
668			// identifier suffix (GMT) instead of '+0200'
669			return mpa_format_date(date, 'D, d M Y H:i:s O');
670
671		  // Not supported
672		  case 'S': // Ordinal suffix, like: st, nd, rd or th
673		  case 'o': // ISO-8601 week-numbering year (examples: 1999 or 2003)
674		  case 'B': // Swatch Internet time
675		  case 'e': // Timezone identifier, like: UTC, GMT or Atlantic/Azores
676		  case 'T': // Timezone abbreviation, like: EST, MDT, etc.
677		  case 'I':
678			// Whether or not the date is in daylight saving time
679			return '';
680
681		  // Undefined
682		  default:
683			return char;
684		} // switch (char)
685	  }) // Map each symbol
686	  .join('');
687	  return dateString;
688	}
689
690	/**
691	 * @param {String} dateString Only the internal format is acceptable - 'Y-m-d'.
692	 * @return {Date|null}
693	 *
694	 * @since 1.0
695	 * @since 1.11.0 returns null if the date string is invalid.
696	 */
697	function mpa_parse_date(dateString) {
698	  let date = dateString.match(mpa_validate_date_pattern());
699	  if (date != null) {
700		let year = parseInt(date[1]);
701		let month = parseInt(date[2]);
702		let day = parseInt(date[3]);
703		return new Date(year, month - 1, day);
704	  } else {
705		return null;
706	  }
707	}
708
709	/**
710	 * @return {RegExp}
711	 *
712	 * @since 1.0
713	 */
714	function mpa_validate_date_pattern() {
715	  return /(\d{4})-(\d{2})-(\d{2})/;
716	}
717
718	/**
719	 * @return {Date}
720	 *
721	 * @since 1.0
722	 */
723	function mpa_today() {
724	  let today = new Date();
725	  today.setHours(0, 0, 0, 0);
726	  return today;
727	}
728
729	function getDefaultExportFromCjs (x) {
730		return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
731	}
732
733	var md5$1 = {exports: {}};
734
735	var crypt = {exports: {}};
736
737	(function() {
738	  var base64map
739		  = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
740
741	  crypt$1 = {
742		// Bit-wise rotation left
743		rotl: function(n, b) {
744		  return (n << b) | (n >>> (32 - b));
745		},
746
747		// Bit-wise rotation right
748		rotr: function(n, b) {
749		  return (n << (32 - b)) | (n >>> b);
750		},
751
752		// Swap big-endian to little-endian and vice versa
753		endian: function(n) {
754		  // If number given, swap endian
755		  if (n.constructor == Number) {
756			return crypt$1.rotl(n, 8) & 0x00FF00FF | crypt$1.rotl(n, 24) & 0xFF00FF00;
757		  }
758
759		  // Else, assume array and swap all items
760		  for (var i = 0; i < n.length; i++)
761			n[i] = crypt$1.endian(n[i]);
762		  return n;
763		},
764
765		// Generate an array of any length of random bytes
766		randomBytes: function(n) {
767		  for (var bytes = []; n > 0; n--)
768			bytes.push(Math.floor(Math.random() * 256));
769		  return bytes;
770		},
771
772		// Convert a byte array to big-endian 32-bit words
773		bytesToWords: function(bytes) {
774		  for (var words = [], i = 0, b = 0; i < bytes.length; i++, b += 8)
775			words[b >>> 5] |= bytes[i] << (24 - b % 32);
776		  return words;
777		},
778
779		// Convert big-endian 32-bit words to a byte array
780		wordsToBytes: function(words) {
781		  for (var bytes = [], b = 0; b < words.length * 32; b += 8)
782			bytes.push((words[b >>> 5] >>> (24 - b % 32)) & 0xFF);
783		  return bytes;
784		},
785
786		// Convert a byte array to a hex string
787		bytesToHex: function(bytes) {
788		  for (var hex = [], i = 0; i < bytes.length; i++) {
789			hex.push((bytes[i] >>> 4).toString(16));
790			hex.push((bytes[i] & 0xF).toString(16));
791		  }
792		  return hex.join('');
793		},
794
795		// Convert a hex string to a byte array
796		hexToBytes: function(hex) {
797		  for (var bytes = [], c = 0; c < hex.length; c += 2)
798			bytes.push(parseInt(hex.substr(c, 2), 16));
799		  return bytes;
800		},
801
802		// Convert a byte array to a base-64 string
803		bytesToBase64: function(bytes) {
804		  for (var base64 = [], i = 0; i < bytes.length; i += 3) {
805			var triplet = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
806			for (var j = 0; j < 4; j++)
807			  if (i * 8 + j * 6 <= bytes.length * 8)
808				base64.push(base64map.charAt((triplet >>> 6 * (3 - j)) & 0x3F));
809			  else
810				base64.push('=');
811		  }
812		  return base64.join('');
813		},
814
815		// Convert a base-64 string to a byte array
816		base64ToBytes: function(base64) {
817		  // Remove non-base-64 characters
818		  base64 = base64.replace(/[^A-Z0-9+\/]/ig, '');
819
820		  for (var bytes = [], i = 0, imod4 = 0; i < base64.length;
821			  imod4 = ++i % 4) {
822			if (imod4 == 0) continue;
823			bytes.push(((base64map.indexOf(base64.charAt(i - 1))
824				& (Math.pow(2, -2 * imod4 + 8) - 1)) << (imod4 * 2))
825				| (base64map.indexOf(base64.charAt(i)) >>> (6 - imod4 * 2)));
826		  }
827		  return bytes;
828		}
829	  };
830
831	  crypt.exports = crypt$1;
832	})();
833
834	var cryptExports = crypt.exports;
835
836	var charenc = {
837	  // UTF-8 encoding
838	  utf8: {
839		// Convert a string to a byte array
840		stringToBytes: function(str) {
841		  return charenc.bin.stringToBytes(unescape(encodeURIComponent(str)));
842		},
843
844		// Convert a byte array to a string
845		bytesToString: function(bytes) {
846		  return decodeURIComponent(escape(charenc.bin.bytesToString(bytes)));
847		}
848	  },
849
850	  // Binary encoding
851	  bin: {
852		// Convert a string to a byte array
853		stringToBytes: function(str) {
854		  for (var bytes = [], i = 0; i < str.length; i++)
855			bytes.push(str.charCodeAt(i) & 0xFF);
856		  return bytes;
857		},
858
859		// Convert a byte array to a string
860		bytesToString: function(bytes) {
861		  for (var str = [], i = 0; i < bytes.length; i++)
862			str.push(String.fromCharCode(bytes[i]));
863		  return str.join('');
864		}
865	  }
866	};
867
868	var charenc_1 = charenc;
869
870	/*!
871	 * Determine if an object is a Buffer
872	 *
873	 * @author   Feross Aboukhadijeh <https://feross.org>
874	 * @license  MIT
875	 */
876
877	// The _isBuffer check is for Safari 5-7 support, because it's missing
878	// Object.prototype.constructor. Remove this eventually
879	var isBuffer_1 = function (obj) {
880	  return obj != null && (isBuffer(obj) || isSlowBuffer(obj) || !!obj._isBuffer)
881	};
882
883	function isBuffer (obj) {
884	  return !!obj.constructor && typeof obj.constructor.isBuffer === 'function' && obj.constructor.isBuffer(obj)
885	}
886
887	// For Node v0.10 support. Remove this eventually.
888	function isSlowBuffer (obj) {
889	  return typeof obj.readFloatLE === 'function' && typeof obj.slice === 'function' && isBuffer(obj.slice(0, 0))
890	}
891
892	(function(){
893	  var crypt = cryptExports,
894		  utf8 = charenc_1.utf8,
895		  isBuffer = isBuffer_1,
896		  bin = charenc_1.bin,
897
898	  // The core
899	  md5 = function (message, options) {
900		// Convert to byte array
901		if (message.constructor == String)
902		  if (options && options.encoding === 'binary')
903			message = bin.stringToBytes(message);
904		  else
905			message = utf8.stringToBytes(message);
906		else if (isBuffer(message))
907		  message = Array.prototype.slice.call(message, 0);
908		else if (!Array.isArray(message) && message.constructor !== Uint8Array)
909		  message = message.toString();
910		// else, assume byte array already
911
912		var m = crypt.bytesToWords(message),
913			l = message.length * 8,
914			a =  1732584193,
915			b = -271733879,
916			c = -1732584194,
917			d =  271733878;
918
919		// Swap endian
920		for (var i = 0; i < m.length; i++) {
921		  m[i] = ((m[i] <<  8) | (m[i] >>> 24)) & 0x00FF00FF |
922				 ((m[i] << 24) | (m[i] >>>  8)) & 0xFF00FF00;
923		}
924
925		// Padding
926		m[l >>> 5] |= 0x80 << (l % 32);
927		m[(((l + 64) >>> 9) << 4) + 14] = l;
928
929		// Method shortcuts
930		var FF = md5._ff,
931			GG = md5._gg,
932			HH = md5._hh,
933			II = md5._ii;
934
935		for (var i = 0; i < m.length; i += 16) {
936
937		  var aa = a,
938			  bb = b,
939			  cc = c,
940			  dd = d;
941
942		  a = FF(a, b, c, d, m[i+ 0],  7, -680876936);
943		  d = FF(d, a, b, c, m[i+ 1], 12, -389564586);
944		  c = FF(c, d, a, b, m[i+ 2], 17,  606105819);
945		  b = FF(b, c, d, a, m[i+ 3], 22, -1044525330);
946		  a = FF(a, b, c, d, m[i+ 4],  7, -176418897);
947		  d = FF(d, a, b, c, m[i+ 5], 12,  1200080426);
948		  c = FF(c, d, a, b, m[i+ 6], 17, -1473231341);
949		  b = FF(b, c, d, a, m[i+ 7], 22, -45705983);
950		  a = FF(a, b, c, d, m[i+ 8],  7,  1770035416);
951		  d = FF(d, a, b, c, m[i+ 9], 12, -1958414417);
952		  c = FF(c, d, a, b, m[i+10], 17, -42063);
953		  b = FF(b, c, d, a, m[i+11], 22, -1990404162);
954		  a = FF(a, b, c, d, m[i+12],  7,  1804603682);
955		  d = FF(d, a, b, c, m[i+13], 12, -40341101);
956		  c = FF(c, d, a, b, m[i+14], 17, -1502002290);
957		  b = FF(b, c, d, a, m[i+15], 22,  1236535329);
958
959		  a = GG(a, b, c, d, m[i+ 1],  5, -165796510);
960		  d = GG(d, a, b, c, m[i+ 6],  9, -1069501632);
961		  c = GG(c, d, a, b, m[i+11], 14,  643717713);
962		  b = GG(b, c, d, a, m[i+ 0], 20, -373897302);
963		  a = GG(a, b, c, d, m[i+ 5],  5, -701558691);
964		  d = GG(d, a, b, c, m[i+10],  9,  38016083);
965		  c = GG(c, d, a, b, m[i+15], 14, -660478335);
966		  b = GG(b, c, d, a, m[i+ 4], 20, -405537848);
967		  a = GG(a, b, c, d, m[i+ 9],  5,  568446438);
968		  d = GG(d, a, b, c, m[i+14],  9, -1019803690);
969		  c = GG(c, d, a, b, m[i+ 3], 14, -187363961);
970		  b = GG(b, c, d, a, m[i+ 8], 20,  1163531501);
971		  a = GG(a, b, c, d, m[i+13],  5, -1444681467);
972		  d = GG(d, a, b, c, m[i+ 2],  9, -51403784);
973		  c = GG(c, d, a, b, m[i+ 7], 14,  1735328473);
974		  b = GG(b, c, d, a, m[i+12], 20, -1926607734);
975
976		  a = HH(a, b, c, d, m[i+ 5],  4, -378558);
977		  d = HH(d, a, b, c, m[i+ 8], 11, -2022574463);
978		  c = HH(c, d, a, b, m[i+11], 16,  1839030562);
979		  b = HH(b, c, d, a, m[i+14], 23, -35309556);
980		  a = HH(a, b, c, d, m[i+ 1],  4, -1530992060);
981		  d = HH(d, a, b, c, m[i+ 4], 11,  1272893353);
982		  c = HH(c, d, a, b, m[i+ 7], 16, -155497632);
983		  b = HH(b, c, d, a, m[i+10], 23, -1094730640);
984		  a = HH(a, b, c, d, m[i+13],  4,  681279174);
985		  d = HH(d, a, b, c, m[i+ 0], 11, -358537222);
986		  c = HH(c, d, a, b, m[i+ 3], 16, -722521979);
987		  b = HH(b, c, d, a, m[i+ 6], 23,  76029189);
988		  a = HH(a, b, c, d, m[i+ 9],  4, -640364487);
989		  d = HH(d, a, b, c, m[i+12], 11, -421815835);
990		  c = HH(c, d, a, b, m[i+15], 16,  530742520);
991		  b = HH(b, c, d, a, m[i+ 2], 23, -995338651);
992
993		  a = II(a, b, c, d, m[i+ 0],  6, -198630844);
994		  d = II(d, a, b, c, m[i+ 7], 10,  1126891415);
995		  c = II(c, d, a, b, m[i+14], 15, -1416354905);
996		  b = II(b, c, d, a, m[i+ 5], 21, -57434055);
997		  a = II(a, b, c, d, m[i+12],  6,  1700485571);
998		  d = II(d, a, b, c, m[i+ 3], 10, -1894986606);
999		  c = II(c, d, a, b, m[i+10], 15, -1051523);
1000		  b = II(b, c, d, a, m[i+ 1], 21, -2054922799);
1001		  a = II(a, b, c, d, m[i+ 8],  6,  1873313359);
1002		  d = II(d, a, b, c, m[i+15], 10, -30611744);
1003		  c = II(c, d, a, b, m[i+ 6], 15, -1560198380);
1004		  b = II(b, c, d, a, m[i+13], 21,  1309151649);
1005		  a = II(a, b, c, d, m[i+ 4],  6, -145523070);
1006		  d = II(d, a, b, c, m[i+11], 10, -1120210379);
1007		  c = II(c, d, a, b, m[i+ 2], 15,  718787259);
1008		  b = II(b, c, d, a, m[i+ 9], 21, -343485551);
1009
1010		  a = (a + aa) >>> 0;
1011		  b = (b + bb) >>> 0;
1012		  c = (c + cc) >>> 0;
1013		  d = (d + dd) >>> 0;
1014		}
1015
1016		return crypt.endian([a, b, c, d]);
1017	  };
1018
1019	  // Auxiliary functions
1020	  md5._ff  = function (a, b, c, d, x, s, t) {
1021		var n = a + (b & c | ~b & d) + (x >>> 0) + t;
1022		return ((n << s) | (n >>> (32 - s))) + b;
1023	  };
1024	  md5._gg  = function (a, b, c, d, x, s, t) {
1025		var n = a + (b & d | c & ~d) + (x >>> 0) + t;
1026		return ((n << s) | (n >>> (32 - s))) + b;
1027	  };
1028	  md5._hh  = function (a, b, c, d, x, s, t) {
1029		var n = a + (b ^ c ^ d) + (x >>> 0) + t;
1030		return ((n << s) | (n >>> (32 - s))) + b;
1031	  };
1032	  md5._ii  = function (a, b, c, d, x, s, t) {
1033		var n = a + (c ^ (b | ~d)) + (x >>> 0) + t;
1034		return ((n << s) | (n >>> (32 - s))) + b;
1035	  };
1036
1037	  // Package private blocksize
1038	  md5._blocksize = 16;
1039	  md5._digestsize = 16;
1040
1041	  md5$1.exports = function (message, options) {
1042		if (message === undefined || message === null)
1043		  throw new Error('Illegal argument ' + message);
1044
1045		var digestbytes = crypt.wordsToBytes(md5(message, options));
1046		return options && options.asBytes ? digestbytes :
1047			options && options.asString ? bin.bytesToString(digestbytes) :
1048			crypt.bytesToHex(digestbytes);
1049	  };
1050
1051	})();
1052
1053	var md5Exports = md5$1.exports;
1054	var md5 = /*@__PURE__*/getDefaultExportFromCjs(md5Exports);
1055
1056	/**
1057	 * Notice: all nullable properties must be set till the checkout.
1058	 *
1059	 * @since 1.0
1060	 */
1061	class CartItem {
1062	  /**
1063	   * @since 1.4.0
1064	   * @access protected
1065	   */
1066	  setupProperties() {
1067		/**
1068		 * @since 1.4.0
1069		 * @var {String}
1070		 * @access protected
1071		 */
1072		this.itemId = '';
1073
1074		/**
1075		 * @since 1.4.0
1076		 * @var {Service|Null}
1077		 */
1078		this.service = null;
1079		this.serviceCategories = {};
1080
1081		/**
1082		 * @since 1.4.0
1083		 * @var {Employee|Null}
1084		 */
1085		this.employee = null;
1086
1087		/**
1088		 * @since 1.4.0
1089		 * @var {Location|Null}
1090		 */
1091		this.location = null;
1092
1093		/**
1094		 * @since 1.0
1095		 * @var {Date|Null}
1096		 */
1097		this.date = null;
1098
1099		/**
1100		 * @since 1.0
1101		 * @var {TimePeriod|Null}
1102		 */
1103		this.time = null;
1104
1105		/**
1106		 * @since 1.0
1107		 * @var {Number}
1108		 */
1109		this.capacity = 1;
1110
1111		/**
1112		 * An array of all available employees. For example, when no exact
1113		 * employee was selected on the previous steps ("- Any -").
1114		 *
1115		 * @since 1.4.0
1116		 * @var {Employee[]}
1117		 */
1118		this.availableEmployees = [];
1119
1120		/**
1121		 * An array of all available locations. For example, when no exact
1122		 * location was selected on the previous steps ("- Any -").
1123		 *
1124		 * @since 1.4.0
1125		 * @var {Location[]}
1126		 */
1127		this.availableLocations = [];
1128	  }
1129
1130	  /**
1131	   * @since 1.0
1132	   *
1133	   * @param {String} itemId
1134	   */
1135	  constructor(itemId) {
1136		this.setupProperties();
1137		this.itemId = itemId;
1138	  }
1139
1140	  /**
1141	   * @since 1.4.0
1142	   *
1143	   * @return {String}
1144	   */
1145	  getItemId() {
1146		return this.itemId;
1147	  }
1148
1149	  /**
1150	   * @since 1.4.0
1151	   *
1152	   * @return {Number[]}
1153	   */
1154	  getAvailableEmployeeIds() {
1155		return this.availableEmployees.map(entity => entity.id);
1156	  }
1157
1158	  /**
1159	   * @since 1.4.0
1160	   *
1161	   * @return {Number[]}
1162	   */
1163	  getAvailableLocationIds() {
1164		return this.availableLocations.map(entity => entity.id);
1165	  }
1166
1167	  /**
1168	   * @since 1.4.0
1169	   *
1170	   * @return {Object}
1171	   */
1172	  getAvailableIds() {
1173		return {
1174		  // service and employee can impact to time slots availability
1175		  service_id: this.service !== null ? this.service.id : 0,
1176		  employee_id: this.employee !== null ? this.employee.id : 0,
1177		  employee_ids: this.getAvailableEmployeeIds(),
1178		  location_ids: this.getAvailableLocationIds()
1179		};
1180	  }
1181
1182	  /**
1183	   * @since 1.0
1184	   *
1185	   * @return {Object}
1186	   */
1187	  getIds() {
1188		return {
1189		  service_id: this.service !== null ? this.service.id : 0,
1190		  employee_id: this.employee !== null ? this.employee.id : 0,
1191		  location_id: this.location !== null ? this.location.id : 0
1192		};
1193	  }
1194
1195	  /**
1196	   * @since 1.0
1197	   * @since 1.4.0 added the <code>fields</code> argument.
1198	   *
1199	   * @param {String} fields Optional. all|ids|period|availability. 'all' by default.
1200	   * @return {Object}
1201	   */
1202	  toArray(fields = 'all') {
1203		if (fields === 'ids') {
1204		  return this.getIds();
1205		} else if (fields === 'availability') {
1206		  return this.getAvailableIds();
1207		} else if (fields === 'period') {
1208		  return {
1209			date: this.date !== null ? mpa_format_date(this.date, 'internal') : '',
1210			time: this.time !== null ? this.time.toString('internal') : ''
1211		  };
1212		} else {
1213		  return jQuery.extend(this.getIds(), {
1214			date: this.date !== null ? mpa_format_date(this.date, 'internal') : '',
1215			time: this.time !== null ? this.time.toString('internal') : '',
1216			capacity: this.capacity
1217		  });
1218		}
1219	  }
1220
1221	  /**
1222	   * @since 1.4.0
1223	   *
1224	   * @param {String} fields Optional. all|ids|period. 'all' by default.
1225	   * @return {Boolean}
1226	   */
1227	  isSet(fields = 'all') {
1228		let isSet = true;
1229		if (fields === 'all' || fields === 'ids') {
1230		  isSet = isSet && this.service !== null && this.employee !== null && this.location !== null;
1231		}
1232		if (fields === 'all' || fields === 'period') {
1233		  isSet = isSet && this.date !== null && this.time !== null;
1234		}
1235		return isSet;
1236	  }
1237
1238	  /**
1239	   * @since 1.11.0
1240	   *
1241	   * @return {Number}
1242	   */
1243	  getPrice() {
1244		if (!this.service) {
1245		  return 0;
1246		}
1247		let employeeId = this.employee ? this.employee.id : 0;
1248		return this.service.getPrice(employeeId, this.capacity);
1249	  }
1250
1251	  /**
1252	   * @since 1.14.0
1253	   *
1254	   * @return {Number}
1255	   * @param {number} priceAfterPossibleDiscount
1256	   */
1257	  getDeposit(priceAfterPossibleDiscount) {
1258		let deposit = 0;
1259		switch (this.service.depositType) {
1260		  case 'disabled':
1261			deposit = priceAfterPossibleDiscount;
1262			break;
1263		  case 'fixed':
1264			deposit = this.service.depositAmount;
1265			break;
1266		  case 'percentage':
1267			deposit = priceAfterPossibleDiscount * this.service.depositAmount / 100;
1268			break;
1269		  default:
1270			deposit = priceAfterPossibleDiscount;
1271		}
1272		if (deposit > priceAfterPossibleDiscount) {
1273		  return priceAfterPossibleDiscount;
1274		}
1275		return deposit;
1276	  }
1277
1278	  /**
1279	   * @since 1.0
1280	   *
1281	   * @param {String} fields Optional. all|ids|availability. 'all' by default.
1282	   * @return {String} Snapshot MD5 hash.
1283	   */
1284	  getHash(fields = 'all') {
1285		return md5(JSON.stringify(this.toArray(fields)));
1286	  }
1287
1288	  /**
1289	   * @since 1.0
1290	   *
1291	   * @param {String} hash Last saved snapshot.
1292	   * @param {String} fields Optional. all|ids|availability. 'all' by default.
1293	   * @return {Boolean}
1294	   */
1295	  didChange(hash, fields = 'all') {
1296		return hash !== this.getHash(fields);
1297	  }
1298
1299	  /**
1300	   * @since 1.4.0
1301	   *
1302	   * @param {Number} employeeId
1303	   * @return {Employee|Null}
1304	   */
1305	  getEmployee(employeeId) {
1306		if (this.employee !== null && this.employee.id == employeeId) {
1307		  return this.employee;
1308		} else {
1309		  for (let employee of this.availableEmployees) {
1310			if (employee.id == employeeId) {
1311			  return employee;
1312			}
1313		  }
1314		}
1315		return null;
1316	  }
1317
1318	  /**
1319	   * @since 1.4.0
1320	   *
1321	   * @param {Number} locationId
1322	   * @return {Location|Null}
1323	   */
1324	  getLocation(locationId) {
1325		if (this.location !== null && this.location.id == locationId) {
1326		  return this.location;
1327		} else {
1328		  for (let location of this.availableLocations) {
1329			if (location.id == locationId) {
1330			  return location;
1331			}
1332		  }
1333		}
1334		return null;
1335	  }
1336
1337	  /**
1338	   * @since 1.4.0
1339	   *
1340	   * @return {Boolean}
1341	   */
1342	  hasMultipleAvailableEmployees() {
1343		return this.availableEmployees.length > 1;
1344	  }
1345
1346	  /**
1347	   * @since 1.4.0
1348	   *
1349	   * @return {Boolean}
1350	   */
1351	  hasMultipleAvailableLocations() {
1352		return this.availableLocations.length > 1;
1353	  }
1354
1355	  /**
1356	   * @since 1.4.0
1357	   *
1358	   * @return {Boolean}
1359	   */
1360	  hasMultipleAvailableVariants() {
1361		return this.hasMultipleAvailableEmployees() || this.hasMultipleAvailableLocations();
1362	  }
1363
1364	  /**
1365	   * @since 1.4.0
1366	   *
1367	   * @param {Service} service
1368	   */
1369	  setService(service) {
1370		this.service = service;
1371	  }
1372
1373	  /**
1374	   * @param {category slug: category name, ...} serviceCategories 
1375	   */
1376	  setServiceCategories(serviceCategories) {
1377		this.serviceCategories = serviceCategories;
1378	  }
1379
1380	  /**
1381	   * @since 1.4.0
1382	   *
1383	   * @param {Employee|Number} employee Employee object or ID.
1384	   * @param {Boolean} resetAvailable Optional. True by default.
1385	   */
1386	  setEmployee(employee, resetAvailable = true) {
1387		if (typeof employee === 'number') {
1388		  employee = this.getEmployee(employee);
1389		}
1390		this.employee = employee;
1391		if (resetAvailable === true) {
1392		  this.availableEmployees = [employee];
1393		}
1394	  }
1395
1396	  /**
1397	   * @since 1.4.0
1398	   *
1399	   * @param {Employee[]} availableEmployees
1400	   * @param {Boolean} resetEmployee Optional. True by default.
1401	   */
1402	  setAvailableEmployees(availableEmployees, resetEmployee = true) {
1403		this.availableEmployees = availableEmployees;
1404		if (resetEmployee === true) {
1405		  this.employee = null;
1406		}
1407	  }
1408
1409	  /**
1410	   * @since 1.4.0
1411	   *
1412	   * @param {Location|Number} location Location object or ID.
1413	   * @param {Boolean} resetAvailable Optional. True by default.
1414	   */
1415	  setLocation(location, resetAvailable = true) {
1416		if (typeof location === 'number') {
1417		  location = this.getLocation(location);
1418		}
1419		this.location = location;
1420		if (resetAvailable === true) {
1421		  this.availableLocations = [location];
1422		}
1423	  }
1424
1425	  /**
1426	   * @since 1.4.0
1427	   *
1428	   * @param {Location[]} availableLocations
1429	   * @param {Boolean} resetLocation Optional. True by default.
1430	   */
1431	  setAvailableLocations(availableLocations, resetLocation = true) {
1432		this.availableLocations = availableLocations;
1433		if (resetLocation === true) {
1434		  this.location = null;
1435		}
1436	  }
1437	}
1438
1439	/**
1440	 * @param {Array} array
1441	 * @return {Array}
1442	 *
1443	 * @since 1.0
1444	 */
1445	function mpa_array_unique(array) {
1446	  let unique = array.filter((value, index, self) => {
1447		return self.indexOf(value) === index;
1448	  });
1449	  return unique;
1450	}
1451
1452	/**
1453	 * @param {Array} array1
1454	 * @param {Array} array2
1455	 * @return {Array}
1456	 *
1457	 * @since 1.2.1
1458	 */
1459	function mpa_array_intersect(array1, array2) {
1460	  return array1.filter(value => array2.indexOf(value) != -1);
1461	}
1462
1463	/**
1464	 * @since 1.4.0
1465	 *
1466	 * @param {Array} keys
1467	 * @param {Array} values
1468	 * @return {Object}
1469	 */
1470	function mpa_array_combine(keys, values) {
1471	  let size = Math.min(keys.length, values.length);
1472	  let object = {};
1473	  for (let i = 0; i < size; i++) {
1474		object[keys[i]] = values[i];
1475	  }
1476	  return object;
1477	}
1478
1479	/**
1480	 * @since 1.4.0
1481	 *
1482	 * @param {Number} start
1483	 * @param {Number} end
1484	 * @param {Number} step Optional. 1 by default.
1485	 * @return {Array} An array of numbers from <code>start</code> to <code>end</code>
1486	 *		with step <code>step</code>.
1487	 */
1488	function mpa_array_range(start, end, step = 1) {
1489	  let validStep = step || 1; // No 0
1490	  let length = Math.abs(Math.floor((end - start) / validStep)) + 1;
1491	  let range = [...Array(length).keys()].map(i => i * step + start);
1492	  return range;
1493	}
1494
1495	/**
1496	 * @since 1.0
1497	 */
1498	class Map {
1499	  /**
1500	   * @param {Object|Null} values Optional. Starting values. Null by default.
1501	   *
1502	   * @since 1.0
1503	   */
1504	  constructor(values = null) {
1505		this.setupProperties();
1506		if (values != null) {
1507		  this.merge(values);
1508		}
1509	  }
1510
1511	  /**
1512	   * @access protected
1513	   *
1514	   * @since 1.0
1515	   */
1516	  setupProperties() {
1517		this.keys = [];
1518		this.values = {};
1519		this.length = 0;
1520	  }
1521
1522	  /**
1523	   * @param {Object} values
1524	   *
1525	   * @since 1.0
1526	   */
1527	  merge(values) {
1528		for (let key in values) {
1529		  this.push(key, values[key]);
1530		}
1531	  }
1532
1533	  /**
1534	   * @param {String|Number|Symbol} key
1535	   * @param {*} value
1536	   * @return {Boolean} Is this a new item (key) or a duplicate.
1537	   *
1538	   * @since 1.0
1539	   */
1540	  push(key, value) {
1541		let isNew = !this.includesKey(key);
1542
1543		// Add value
1544		this.values[key] = value;
1545
1546		// Add key
1547		if (isNew) {
1548		  this.keys.push(key);
1549		  this.length++;
1550		}
1551		return isNew;
1552	  }
1553
1554	  /**
1555	   * @param {String|Number|Symbol} key
1556	   * @param {*} defaultValue Optional. Null by default.
1557	   * @return {*} Item value or default value.
1558	   *
1559	   * @since 1.0
1560	   */
1561	  find(key, defaultValue = null) {
1562		if (this.includesKey(key)) {
1563		  return this.values[key];
1564		} else {
1565		  return defaultValue;
1566		}
1567	  }
1568
1569	  /**
1570	   * @param {String|Number|Symbol} key
1571	   * @param {*} defaultValue Optional. Null by default.
1572	   * @return {*} Next item value or default value.
1573	   *
1574	   * @since 1.9.0
1575	   */
1576	  findNext(key, defaultValue = null) {
1577		let nextKey = this.findNextKey(key);
1578		if (nextKey !== '') {
1579		  return this.values[nextKey];
1580		} else {
1581		  return defaultValue;
1582		}
1583	  }
1584
1585	  /**
1586	   * @param {String|Number|Symbol} key
1587	   * @return {String|Number|Symbol} Next or last key.
1588	   *
1589	   * @since 1.9.0
1590	   */
1591	  findNextKey(key) {
1592		let index = this.keys.indexOf(key);
1593		if (index === -1) {
1594		  return '';
1595		}
1596		let nextIndex = index + 1;
1597		let nextKey = nextIndex < this.length ? this.keys[nextIndex] : this.keys[index];
1598		return nextKey;
1599	  }
1600
1601	  /**
1602	   * @param {String|Number|Symbol} key
1603	   * @param {*} defaultValue Optional. Null by default.
1604	   * @return {*} Previous item value or default value.
1605	   *
1606	   * @since 1.9.0
1607	   */
1608	  findPrevious(key, defaultValue = null) {
1609		let previousKey = this.findPreviousKey(key);
1610		if (previousKey !== '') {
1611		  return this.values[previousKey];
1612		} else {
1613		  return defaultValue;
1614		}
1615	  }
1616
1617	  /**
1618	   * @param {String|Number|Symbol} key
1619	   * @return {String|Number|Symbol} Previous or first key.
1620	   *
1621	   * @since 1.9.0
1622	   */
1623	  findPreviousKey(key) {
1624		let index = this.keys.indexOf(key);
1625		if (index === -1) {
1626		  return '';
1627		}
1628		let previousIndex = index - 1;
1629		let previousKey = previousIndex >= 0 ? this.keys[previousIndex] : this.keys[index];
1630		return previousKey;
1631	  }
1632
1633	  /**
1634	   * Currently it's an alias of push() and has no difference.
1635	   *
1636	   * @param {String|Number|Symbol} key
1637	   * @param {*} value
1638	   * @return {Boolean}
1638 Is this a new item (key) or a duplicate.
1639	   *
1640	   * @since 1.0
1641	   */
1642	  update(key, value) {
1643		return this.push(key, value);
1644	  }
1645
1646	  /**
1647	   * @param {String|Number|Symbol} key
1648	   * @return {*} The removed value or null if there was no item with a such key.
1649	   *
1650	   * @since 1.0
1651	   */
1652	  remove(key) {
1653		if (!this.includesKey(key)) {
1654		  return null;
1655		}
1656		let value = this.values[key];
1657
1658		// Remove value
1659		delete this.values[key];
1660
1661		// Remove key
1662		let index = this.keys.indexOf(key);
1663		this.keys.splice(index, 1);
1664
1665		// Update length
1666		this.length--;
1667		return value;
1668	  }
1669
1670	  /**
1671	   * @return {Map}
1672	   *
1673	   * @since 1.0
1674	   */
1675	  empty() {
1676		this.keys = [];
1677		this.values = {};
1678		this.length = 0;
1679		return this;
1680	  }
1681
1682	  /**
1683	   * @return {Boolean}
1684	   *
1685	   * @since 1.0
1686	   */
1687	  isEmpty() {
1688		return this.length == 0;
1689	  }
1690
1691	  /**
1692	   * @param {String|Number|Symbol} key
1693	   * @return {Boolean}
1694	   *
1695	   * @since 1.0
1696	   */
1697	  includesKey(key) {
1698		return key in this.values;
1699	  }
1700
1701	  /**
1702	   * @since 1.6.2
1703	   *
1704	   * @return {String|null}
1705	   */
1706	  firstKey() {
1707		if (this.keys.length > 0) {
1708		  return this.keys[0];
1709		} else {
1710		  return null;
1711		}
1712	  }
1713
1714	  /**
1715	   * @since 1.6.2
1716	   *
1717	   * @return {*|null}
1718	   */
1719	  firstValue() {
1720		let firstKey = this.firstKey();
1721		if (firstKey !== null) {
1722		  return this.values[firstKey];
1723		} else {
1724		  return null;
1725		}
1726	  }
1727
1728	  /**
1729	   * @return {*}
1730	   *
1731	   * @since 1.0
1732	   */
1733	  lastValue() {
1734		let lastKey = this.lastKey();
1735		if (lastKey != null) {
1736		  return this.values[lastKey];
1737		} else {
1738		  return null;
1739		}
1740	  }
1741
1742	  /**
1743	   * @return {String|Number|Symbol|Null}
1744	   *
1745	   * @since 1.0
1746	   */
1747	  lastKey() {
1748		if (!this.isEmpty()) {
1749		  return this.keys[this.length - 1];
1750		} else {
1751		  return null;
1752		}
1753	  }
1754
1755	  /**
1756	   * @return {Array}
1757	   *
1758	   * @since 1.0
1759	   */
1760	  cloneKeys() {
1761		return [...this.keys];
1762	  }
1763
1764	  /**
1765	   * @param {String|Number|Symbol} column
1766	   * @return {Array} Column values (only unique ones).
1767	   *
1768	   * @since 1.0
1769	   */
1770	  getColumn(column) {
1771		let values = [];
1772		for (let key of this.keys) {
1773		  let value = this.values[key][column];
1774		  if (value != undefined) {
1775			if (Array.isArray(value)) {
1776			  // Merge arrays
1777			  values = values.concat(value);
1778			} else {
1779			  // Add value to array
1780			  values.push(value);
1781			}
1782		  }
1783		}
1784		return mpa_array_unique(values);
1785	  }
1786
1787	  /**
1788	   * @param {Function} callback
1789	   *
1790	   * @since 1.0
1791	   */
1792	  forEach(callback) {
1793		let i = 0;
1794		for (let key of this.keys) {
1795		  let response = callback(this.values[key], i, key, this);
1796		  i++;
1797		  if (response === false) {
1798			break;
1799		  }
1800		}
1801	  }
1802
1803	  /**
1804	   * @param {Function} callback
1805	   * @return {Array} May have duplicates.
1806	   *
1807	   * @since 1.0
1808	   */
1809	  map(callback) {
1810		let values = [];
1811		let i = 0;
1812		for (let key of this.keys) {
1813		  values.push(callback(this.values[key], i, key, this));
1814		  i++;
1815		}
1816		return values;
1817	  }
1818
1819	  /**
1820	   * @return {Array}
1821	   *
1822	   * @since 1.0
1823	   */
1824	  toArray() {
1825		let values = [];
1826		for (let key of this.keys) {
1827		  values.push(this.values[key]);
1828		}
1829		return values;
1830	  }
1831	}
1832
1833	let globals = {};
1834
1835	/**
1836	 * @param {*} value
1837	 * @param {Boolean} strict Optional. Don't count non-iterable types. False by
1838	 *     default.
1839	 * @return {Number}
1840	 *
1841	 * @since 1.0
1842	 */
1843	function mpa_count(value, strict = false) {
1844	  if (typeof value == 'object') {
1845		if (Array.isArray(value)) {
1846		  return value.length;
1847		} else {
1848		  return Object.keys(value).length;
1849		}
1850	  } else {
1851		return strict ? 0 : 1;
1852	  }
1853	}
1854
1855	/**
1856	 * @param {*} value
1857	 * @param {Boolean} strict Optional. False by default.
1858	 * @return {Boolean}
1859	 *
1860	 * @since 1.0
1861	 */
1862	function mpa_empty(value, strict = false) {
1863	  if (typeof value == 'object') {
1864		return mpa_count(value) == 0;
1865	  } else {
1866		return strict ? true : !value;
1867	  }
1868	}
1869
1870	/**
1871	 * @param {String} prefix Optional. '' by default.
1872	 * @param {Boolean} moreEntropy Optional. False by default.
1873	 * @return {String}
1874	 *
1875	 * @see https://locutus.io/php/misc/uniqid/
1876	 *
1877	 * @since 1.0
1878	 */
1879	function mpa_uniqid(prefix = '', moreEntropy = false) {
1880	  let mpa_format_seed = function (seed, requiredWidth) {
1881		seed = parseInt(seed, 10).toString(16); // To hex string
1882
1883		if (requiredWidth < seed.length) {
1884		  // So long we split
1885		  return seed.slice(seed.length - requiredWidth);
1886		} else if (requiredWidth > seed.length) {
1887		  // So short we pad
1888		  return Array(1 + (requiredWidth - seed.length)).join('0') + seed;
1889		} else {
1890		  // Exact width
1891		  return seed;
1892		}
1893	  };
1894	  if (!globals.uniqid_seed) {
1895		// Init seed with big random int
1896		globals.uniqid_seed = Math.floor(Math.random() * 0x75bcd15);
1897	  }
1898	  globals.uniqid_seed++;
1899
1900	  // Start with prefix
1901	  let id = prefix;
1902	  // Add current milliseconds (hex string)
1903	  id += mpa_format_seed(parseInt(new Date().getTime() / 1000, 10), 8);
1904	  // Add seed hex string
1905	  id += mpa_format_seed(globals.uniqid_seed, 5);
1906	  if (moreEntropy) {
1907		// For more entropy we add a float lowe to 10
1908		id += (Math.random() * 10).toFixed(8).toString();
1909	  }
1910	  return id;
1911	}
1912
1913	/**
1914	 * @since 1.0
1915	 */
1916	class Cart {
1917	  /**
1918	   * @since 1.4.0
1919	   * @access protected
1920	   */
1921	  setupProperties() {
1922		/**
1923		 * @since 1.0
1924		 * @var {Map}
1925		 */
1926		this.items = new Map();
1927
1928		/**
1929		 * @since 1.4.0 (Replaced the property <code>selectedItem</code>)
1930		 * @var {CartItem|Null}
1931		 */
1932		this.activeItem = null;
1933
1934		/**
1935		 * @since 1.0
1936		 * @var {Object}
1937		 */
1938		this.customerDetails = {
1939		  name: '',
1940		  email: '',
1941		  phone: ''
1942		};
1943
1944		/**
1945		 * @since 1.5.0
1946		 * @var {Object}
1947		 */
1948		this.paymentDetails = {
1949		  booking_id: 0,
1950		  gateway_id: 'none'
1951		};
1952
1953		/**
1954		 * @since 1.11.0
1955		 * @var {Coupon|Null}
1956		 */
1957		this.coupon = null;
1958	  }
1959
1960	  /**
1961	   * @since 1.0
1962	   */
1963	  constructor() {
1964		this.setupProperties();
1965	  }
1966
1967	  /**
1968	   * @since 1.0
1969	   * @since 1.4.0 added the <code>itemId</code> argument.
1970	   * @since 1.4.0 returns <code>CartItem</code> object instead of item identifier.
1971	   *
1972	   * @param {String} itemId Optional.
1973	   * @return {CartItem} New item.
1974	   */
1975	  createItem(itemId = '') {
1976		if (!itemId) {
1977		  itemId = mpa_uniqid();
1978		}
1979		let item = new CartItem(itemId);
1980		this.items.push(itemId, item);
1981		this.activeItem = item;
1982		return item;
1983	  }
1984
1985	  /**
1986	   * @since 1.0
1987	   *
1988	   * @param {String} itemId
1989	   * @return {CartItem|Null}
1990	   */
1991	  getItem(itemId) {
1992		return this.items.find(itemId);
1993	  }
1994
1995	  /**
1996	   * @since 1.4.0
1997	   *
1998	   * @return {CartItem|Null}
1999	   */
2000	  getActiveItem() {
2001		return this.activeItem;
2002	  }
2003
2004	  /**
2005	   * @since 1.4.0
2006	   *
2007	   * @return {String}
2008	   */
2009	  getActiveItemId() {
2010		if (this.activeItem !== null) {
2011		  return this.activeItem.getItemId();
2012		} else {
2013		  return '';
2014		}
2015	  }
2016
2017	  /**
2018	   * @since 1.4.0
2019	   *
2020	   * @return {Number}
2021	   */
2022	  getItemsCount() {
2023		return this.items.length;
2024	  }
2025
2026	  /**
2027	   * @since 1.4.0
2028	   *
2029	   * @param {String|CartItem} item
2030	   */
2031	  setActiveItem(item) {
2032		if (typeof item === 'string') {
2033		  this.activeItem = this.getItem(item);
2034		} else {
2035		  this.activeItem = item;
2036		}
2037	  }
2038
2039	  /**
2040	   * @since 1.4.0
2041	   *
2042	   * @param {String|CartItem} item
2043	   */
2044	  removeItem(item) {
2045		if (typeof item === 'string') {
2046		  this.items.remove(item);
2047		} else {
2048		  this.items.remove(item.getItemId());
2049		}
2050	  }
2051
2052	  /**
2053	   * @since 1.4.0
2054	   *
2055	   * @return {Boolean}
2056	   */
2057	  isEmpty() {
2058		return this.getItemsCount() === 0;
2059	  }
2060
2061	  /**
2062	   * @since 1.4.0
2063	   *
2064	   * @return {Object} {Product name: Product price}
2065	   */
2066	  getProductPrices() {
2067		let products = [];
2068		this.items.forEach(cartItem => {
2069		  if (cartItem.service != null) {
2070			products.push({
2071			  name: cartItem.service.name,
2072			  price: cartItem.getPrice()
2073			});
2074		  }
2075		});
2076		return products;
2077	  }
2078
2079	  /**
2080	   * @since 1.4.0
2081	   *
2082	   * @param {Array|Null} products Optional. List of products to calculate the
2083	   *		subtotal price. Products of the cart by default.
2084	   * @return {Number}
2085	   */
2086	  getSubtotalPrice(products = null) {
2087		if (products === null) {
2088		  products = this.getProductPrices();
2089		}
2090		let subtotal = 0;
2091		for (let product of products) {
2092		  subtotal += product.price;
2093		}
2094		return subtotal;
2095	  }
2096
2097	  /**
2098	   * @since 1.4.0
2099	   *
2100	   * @param {Object|Null} products Optional. List of products to calculate the
2101	   *		total price. Products of the cart by default.
2102	   * @return {Number}
2103	   */
2104	  getTotalPrice(products = null) {
2105		let subtotalPrice = this.getSubtotalPrice(products);
2106		if (this.hasCoupon()) {
2107		  let discountPrice = this.coupon.calcDiscountAmount(this);
2108		  return Math.max(0, subtotalPrice - discountPrice);
2109		} else {
2110		  return subtotalPrice;
2111		}
2112	  }
2113
2114	  /**
2115	   * @since 1.14.0
2116	   * @return {Number}
2117	   */
2118	  getDeposit() {
2119		let deposit = 0;
2120		this.items.forEach(cartItem => {
2121		  // TODO: ask all prices via ajax call to avoid calculation duplication
2122		  let price = cartItem.getPrice();
2123		  if (this.hasCoupon()) {
2124			price = price - this.coupon.calcDiscountForCartItem(cartItem);
2125		  }
2126		  deposit += cartItem.getDeposit(price);
2127		});
2128		return deposit;
2129	  }
2130
2131	  /**
2132	   * @since 1.5.0
2133	   *
2134	   * @return {Object}
2135	   */
2136	  getCustomer() {
2137		return this.customerDetails;
2138	  }
2139
2140	  /**
2141	   * @since 1.4.0
2142	   *
2143	   * @return {Object}
2144	   */
2145	  getOrder() {
2146		let products = this.getProductPrices();
2147		let order = {
2148		  products: products,
2149		  subtotal: this.getSubtotalPrice(products),
2150		  total: this.getTotalPrice(products),
2151		  customer: this.getCustomer()
2152		};
2153		if (this.hasCoupon()) {
2154		  order['coupon'] = {
2155			code: this.coupon.getCode(),
2156			amount: this.coupon.calcDiscountAmount(this)
2157		  };
2158		}
2159		order['deposit'] = this.getDeposit();
2160		return order;
2161	  }
2162
2163	  /**
2164	   * @since 1.5.0
2165	   *
2166	   * @return {Object}
2167	   */
2168	  getPaymentDetails() {
2169		return this.paymentDetails;
2170	  }
2171
2172	  /**
2173	   * @since 1.0
2174	   * @since 1.4.0 added the <code>fields</code> argument.
2175	   *
2176	   * @param {String} fields Optional. all|items. 'all' by default.
2177	   * @return {Object|Array}
2178	   */
2179	  toArray(fields = 'all') {
2180		let cart = {
2181		  items: [],
2182		  customer: this.customerDetails
2183		};
2184
2185		// Add items
2186		this.items.forEach(cartItem => {
2187		  if (!cartItem.isSet()) {
2188			return;
2189		  }
2190		  cart.items.push(cartItem.toArray());
2191		});
2192
2193		// Add payment details
2194		if (mpapp().settings().isPaymentsEnabled()) {
2195		  cart.payment_details = this.paymentDetails;
2196		}
2197
2198		// Add coupon
2199		if (this.hasCoupon()) {
2200		  cart['coupon'] = this.coupon.getCode();
2201		}
2202		if (fields === 'items') {
2203		  return cart.items;
2204		} else {
2205		  return cart;
2206		}
2207	  }
2208
2209	  /**
2210	   * @since 1.5.0
2211	   *
2212	   * @param {String} fields Optional. all|order|items. 'all' by default.
2213	   * @return {String} Snapshot MD5 hash.
2214	   */
2215	  getHash(fields = 'all') {
2216		if (fields !== 'order') {
2217		  return md5(JSON.stringify(this.toArray(fields)));
2218		} else {
2219		  return md5(JSON.stringify(this.getOrder()));
2220		}
2221	  }
2222
2223	  /**
2224	   * @since 1.5.0
2225	   *
2226	   * @param {String} hash Last saved snapshot.
2227	   * @param {String} fields Optional. all|order|items. 'all' by default.
2228	   * @return {Boolean}
2229	   */
2230	  didChange(hash, fields = 'all') {
2231		return hash !== this.getHash(fields);
2232	  }
2233
2234	  /**
2235	   * @since 1.4.0 (Replaced the method <code>addCustomerDetails()</code>)
2236	   *
2237	   * @param {Object} customerDetails
2238	   */
2239	  setCustomerDetails(customerDetails) {
2240		jQuery.extend(this.customerDetails, customerDetails);
2241	  }
2242
2243	  /**
2244	   * @since 1.5.0
2245	   *
2246	   * @param {Object} paymentDetails
2247	   */
2248	  setPaymentDetails(paymentDetails) {
2249		jQuery.extend(this.paymentDetails, paymentDetails);
2250	  }
2251
2252	  /**
2253	   * @since 1.6.2
2254	   */
2255	  reset() {
2256		this.setupProperties();
2257	  }
2258
2259	  /**
2260	   * @since 1.9.0
2261	   *
2262	   * @return {Date}
2263	   */
2264	  getMinDate() {
2265		let minDate = null;
2266		this.items.forEach(cartItem => {
2267		  if (!cartItem.date) {
2268			return;
2269		  }
2270		  if (!minDate || minDate > cartItem.date) {
2271			minDate = new Date(cartItem.date.getTime());
2272		  }
2273		});
2274		return minDate || mpa_today();
2275	  }
2276
2277	  /**
2278	   * @since 1.9.0
2279	   *
2280	   * @return {Number[]}
2281	   */
2282	  getServiceIds() {
2283		let serviceIds = this.items.map(cartItem => {
2284		  return cartItem.service != null ? cartItem.service.id : 0;
2285		});
2286		serviceIds = mpa_array_unique(serviceIds);
2287		return serviceIds;
2288	  }
2289
2290	  /**
2291	   * @since 1.9.0
2292	   *
2293	   * @param {Service[]} services
2294	   */
2295	  updateServices(services) {
2296		for (let service of services) {
2297		  this.items.forEach(cartItem => {
2298			if (cartItem.service != null && cartItem.service.id === service.id) {
2299			  cartItem.service = service;
2300			}
2301		  });
2302		}
2303	  }
2304
2305	  /**
2306	   * @since 1.11.0
2307	   *
2308	   * @param {Coupon} coupon
2309	   */
2310	  setCoupon(coupon) {
2311		this.coupon = coupon;
2312	  }
2313
2314	  /**
2315	   * @since 1.11.0
2316	   */
2317	  removeCoupon() {
2318		this.coupon = null;
2319	  }
2320
2321	  /**
2322	   * @since 1.11.0
2323	   */
2324	  hasCoupon() {
2325		return this.coupon != null;
2326	  }
2327
2328	  /**
2329	   * @since 1.11.0
2330	   */
2331	  testCoupon() {
2332		if (this.hasCoupon() && !this.coupon.isApplicableForCart(this)) {
2333		  this.removeCoupon();
2334		}
2335	  }
2336	}
2337
2338	/**
2339	 * @abstract
2340	 *
2341	 * @since 1.0
2342	 */
2343	class AbstractEntity {
2344	  /**
2345	   * @param {Number} id
2346	   * @param {Object} properties Optional.
2347	   *
2348	   * @since 1.0
2349	   */
2350	  constructor(id, properties = {}) {
2351		this.id = id;
2352		this.setupProperties();
2353		this.setupValues(properties);
2354	  }
2355
2356	  /**
2357	   * @access protected
2358	   *
2359	   * @since 1.0
2360	   */
2361	  setupProperties() {}
2362
2363	  /**
2364	   * @param {Object} properties
2365	   *
2366	   * @access protected
2367	   *
2368	   * @since 1.0
2369	   */
2370	  setupValues(properties) {
2371		for (let property in properties) {
2372		  this[property] = properties[property];
2373		}
2374	  }
2375
2376	  /**
2377	   * @return {Number}
2378	   *
2379	   * @since 1.11.0
2380	   */
2381	  getId() {
2382		return this.id;
2383	  }
2384	}
2385
2386	/**
2387	 * Draft version just to present employee as an object in some code.
2388	 *
2389	 * @since 1.4.0
2390	 */
2391	class Employee extends AbstractEntity {
2392	  /**
2393	   * @since 1.4.0
2394	   * @access protected
2395	   */
2396	  setupProperties() {
2397		super.setupProperties();
2398		this.name = '';
2399	  }
2400	}
2401
2402	/**
2403	 * Draft version just to present location as an object in some code.
2404	 *
2405	 * @since 1.4.0
2406	 */
2407	class Location extends AbstractEntity {
2408	  /**
2409	   * @since 1.4.0
2410	   * @access protected
2411	   */
2412	  setupProperties() {
2413		super.setupProperties();
2414		this.name = '';
2415	  }
2416	}
2417
2418	/**
2419	 * @since 1.0
2420	 */
2421	class Service extends AbstractEntity {
2422	  /**
2423	   * @since 1.0
2424	   * @access protected
2425	   */
2426	  setupProperties() {
2427		// Declare only the required minimum for methods. Still get all fields
2428		// from REST requests
2429		super.setupProperties();
2430		this.name = '';
2431		this.price = 0;
2432
2433		/**
2434		 *  @since 1.14.0
2435		 *  string disabled|fixed|percentage
2436		 */
2437		this.depositType = 'disabled';
2438		/**
2439		 *  @since 1.14.0
2440		 */
2441		this.depositAmount = 0;
2442		this.duration = 0; // Minutes
2443
2444		this.bufferTimeBefore = 0; // Minutes
2445		this.bufferTimeAfter = 0; // Minutes
2446		this.timeBeforeBooking = 0; // Minutes
2447		this.maxAdvanceTimeBeforeReservation = 0; // Minutes
2448
2449		this.minCapacity = 1;
2450		this.maxCapacity = 1;
2451		this.multiplyPrice = false;
2452		this.variations = {}; // {Employee ID: {employee, price, duration, min_capacity, max_capacity}}
2453
2454		/** @since 1.4.0 */
2455		this.image = '';
2456
2457		/** @since 1.4.0 */
2458		this.thumbnail = '';
2459	  }
2460
2461	  /**
2462	   * @since 1.0
2463	   * @since 1.3.1 added the <code>capacity</code> argument.
2464	   *
2465	   * @param {Number} employeeId Optional. 0 by default.
2466	   * @param {Number} capacity Optional. 0 by default (minimum capacity of the
2467	   *     service).
2468	   * @return {Number}
2469	   */
2470	  getPrice(employeeId = 0, capacity = 0) {
2471		if (!capacity) {
2472		  capacity = this.minCapacity;
2473		}
2474		let price = this.getVariation('price', employeeId, this.price);
2475		if (this.multiplyPrice) {
2476		  price *= capacity;
2477		}
2478		return price;
2479	  }
2480
2481	  /**
2482	   * @since 1.0
2483	   *
2484	   * @param {Number} employeeId Optional.
2485	   * @return {Number}
2486	   */
2487	  getDuration(employeeId = 0) {
2488		return this.getVariation('duration', employeeId, this.duration);
2489	  }
2490
2491	  /**
2492	   * @since 1.3.1
2493	   *
2494	   * @param {Number} employeeId Optional.
2495	   * @return {Number}
2496	   */
2497	  getMinCapacity(employeeId = 0) {
2498		return this.getVariation('min_capacity', employeeId, this.minCapacity);
2499	  }
2500
2501	  /**
2502	   * @since 1.3.1
2503	   *
2504	   * @param {Number} employeeId Optional.
2505	   * @return {Number}
2506	   */
2507	  getMaxCapacity(employeeId = 0) {
2508		return this.getVariation('max_capacity', employeeId, this.maxCapacity);
2509	  }
2510
2511	  /**
2512	   * @since 1.9.0
2513	   *
2514	   * @param {Number} employeeId Optional.
2515	   * @return {Number[]}
2516	   */
2517	  getCapacityRange(employeeId = 0) {
2518		let minCapacity = this.getMinCapacity(employeeId);
2519		let maxCapacity = this.getMaxCapacity(employeeId);
2520		return mpa_array_range(minCapacity, maxCapacity);
2521	  }
2522
2523	  /**
2524	   * @since 1.3.1
2525	   *
2526	   * @param {Number} employeeId Optional.
2527	   * @return {Number} The maximum number of guests in range [0; ∞).
2528	   *
2529	   * @todo Not used. Remove after implementing the Events feature.
2530	   */
2531	  getMaxGuests(employeeId = 0) {
2532		let maxCapacity = this.getMaxCapacity(employeeId);
2533		let minCapacity = this.getMinCapacity(employeeId);
2534		let maxGuests = maxCapacity - minCapacity;
2535		return Math.max(0, maxGuests);
2536	  }
2537
2538	  /**
2539	   * @since 1.3.1
2540	   * @access protected
2541	   *
2542	   * @param {String} field
2543	   * @param {Number} employeeId
2544	   * @param {*} defaultValue
2545	   * @return {*}
2546	   */
2547	  getVariation(field, employeeId, defaultValue) {
2548		if (employeeId in this.variations) {
2549		  return this.variations[employeeId][field];
2550		} else {
2551		  return defaultValue;
2552		}
2553	  }
2554	}
2555
2556	/**
2557	 * @since 1.4.0
2558	 */
2559	class EntityUtils {
2560	  /**
2561	   * @since 1.4.0
2562	   *
2563	   * @param {AbstractEntity} entity
2564	   * @param {AbstractRepository} repository
2565	   * @param {Boolean} forceReload Optional. False by default.
2566	   * @return {Promise}
2567	   */
2568	  static loadInBackground(entity, repository, forceReload = false) {
2569		return repository.findById(entity.id, forceReload).then(loadedEntity => {
2570		  // If got real result (not null)...
2571		  if (loadedEntity !== null) {
2572			// ... then copy all property values to existing entity
2573			for (let property in loadedEntity) {
2574			  entity[property] = loadedEntity[property];
2575			}
2576		  }
2577		  return loadedEntity;
2578		});
2579	  }
2580	}
2581
2582	/**
2583	 * @since 1.11.0
2584	 */
2585	class Coupon extends AbstractEntity {
2586	  setupProperties() {
2587		super.setupProperties();
2588		this.status = 'new';
2589		this.code = '';
2590		this.description = '';
2591		this.type = 'fixed';
2592		this.amount = 0;
2593		this.expirationDate = null;
2594		this.serviceIds = [];
2595		this.minDate = null;
2596		this.maxDate = null;
2597		this.usageLimit = 0;
2598		this.usageCount = 0;
2599	  }
2600
2601	  /**
2602	   * @access protected
2603	   *
2604	   * @param {Object} properties
2605	   */
2606	  setupValues(properties) {
2607		// Parse dates
2608		for (let dateProperty of ['expirationDate', 'minDate', 'maxDate']) {
2609		  let dateValue = properties[dateProperty];
2610		  if (dateValue != null && dateValue !== '') {
2611			this[dateProperty] = mpa_parse_date(dateValue);
2612		  }
2613		  delete properties[dateProperty];
2614		}
2615
2616		// Set up other properties
2617		super.setupValues(properties);
2618	  }
2619
2620	  /**
2621	   * @return {String}
2622	   */
2623	  getCode() {
2624		return this.code;
2625	  }
2626
2627	  /**
2628	   * @param {Cart} cart
2629	   * @return {Boolean}
2630	   */
2631	  isApplicableForCart(cart) {
2632		let isApplicable = false;
2633		cart.items.forEach(cartItem => {
2634		  if (this.isApplicableForCartItem(cartItem)) {
2635			isApplicable = true;
2636			return false; // Stop the cycle
2637		  }
2638		});
2639
2640		return isApplicable;
2641	  }
2642
2643	  /**
2644	   * @param {CartItem} cartItem
2645	   * @return {Boolean}
2646	   */
2647	  isApplicableForCartItem(cartItem) {
2648		if (!cartItem.isSet()) {
2649		  return false;
2650		}
2651		if (this.serviceIds.length > 0 && this.serviceIds.indexOf(cartItem.service.id) == -1) {
2652		  return false;
2653		}
2654		if (this.minDate != null && cartItem.date < this.minDate) {
2655		  return false;
2656		}
2657		if (this.maxDate != null && cartItem.date > this.maxDate) {
2658		  return false;
2659		}
2660
2661		// Applicable
2662		return true;
2663	  }
2664
2665	  /**
2666	   * @param {Cart} cart
2667	   * @return {Number}
2668	   */
2669	  calcDiscountAmount(cart) {
2670		let discountPrice = this.calcDiscountForCart(cart);
2671		return Math.min(discountPrice, cart.getSubtotalPrice());
2672	  }
2673
2674	  /**
2675	   * @access protected
2676	   *
2677	   * @param {Cart} cart
2678	   * @return {Number}
2679	   */
2680	  calcDiscountForCart(cart) {
2681		let discount = 0;
2682		cart.items.forEach(cartItem => {
2683		  discount += this.calcDiscountForCartItem(cartItem);
2684		});
2685		return discount;
2686	  }
2687
2688	  /**
2689	   * @access protected
2690	   *
2691	   * @param {CartItem} cartItem
2692	   * @return {Number}
2693	   */
2694	  calcDiscountForCartItem(cartItem) {
2695		let discountPrice = 0;
2696		if (this.isApplicableForCartItem(cartItem)) {
2697		  let itemPrice = cartItem.getPrice();
2698		  switch (this.type) {
2699			case 'fixed':
2700			  discountPrice = this.amount;
2701			  break;
2702			case 'percentage':
2703			  discountPrice = itemPrice * this.amount / 100;
2704			  break;
2705		  }
2706
2707		  // Don't exceed with amount > 100%
2708		  discountPrice = Math.min(discountPrice, itemPrice);
2709		}
2710		return discountPrice;
2711	  }
2712	}
2713
2714	/**
2715	 * @param {Number} number
2716	 * @param {Number} decimals
2717	 * @param {String} decimalSeparator
2718	 * @param {String} thousandsSeparator
2719	 * @return {String}
2720	 *
2721	 * @since 1.0
2722	 */
2723	function mpa_number_format(number, decimals = 0, decimalSeparator = '.', thousandsSeparator = ',') {
2724	  // + Original by: Jonas Raoni Soares Silva (http://www.jsfromhell.com)
2725	  // + Improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
2726	  // +   Bugfix by: Michael White (http://crestidg.com)
2727	  let sign = '',
2728		i,
2729		j,
2730		kw,
2731		kd,
2732		km;
2733	  if (number < 0) {
2734		sign = '-';
2735		number *= -1;
2736	  }
2737	  i = parseInt(number = (+number || 0).toFixed(decimals)) + '';
2738	  if ((j = i.length) > 3) {
2739		j = j % 3;
2740	  } else {
2741		j = 0;
2742	  }
2743	  km = j ? i.substr(0, j) + thousandsSeparator : '';
2744	  kw = i.substr(j).replace(/(\d{3})(?=\d)/g, '$1' + thousandsSeparator);
2745	  kd = decimals ? decimalSeparator + Math.abs(number - i).toFixed(decimals).replace(/-/, 0).slice(2) : '';
2746	  return sign + km + kw + kd;
2747	}
2748
2749	/**
2750	 * @param {String} string
2751	 * @return {String}
2752	 *
2753	 * @since 1.0
2754	 */
2755	function mpa_unprefix(string) {
2756	  if (string.indexOf('mpa_') === 0) {
2757		return string.substring(4); // Remove the public prefix
2758	  } else if (string.indexOf('_mpa_') === 0) {
2759		return string.substring(5); // Remove the private prefix
2760	  } else {
2761		return string; // Already without the prefix
2762	  }
2763	}
2764
2765	/**
2766	 * Will trim '5.00' to '5', but leave '5.50' as is.
2767	 *
2768	 * @param {String} price
2769	 * @param {String} decimalSeparator Optional. Decimal separator from settings by
2770	 *     default.
2771	 * @return {String}
2772	 *
2773	 * @since 1.0
2774	 */
2775	function mpa_trim_price(price, decimalSeparator = null) {
2776	  if (decimalSeparator == null) {
2777		decimalSeparator = mpapp().settings().getDecimalSeparator();
2778	  }
2779	  let regex = new RegExp('\\' + decimalSeparator + '0+$'); // /\.0+$/
2780	  price = price.replace(regex, '');
2781	  return price;
2782	}
2783
2784	/**
2785	 * @param {*} value
2786	 * @return {Boolean}
2787	 *
2788	 * @since 1.2
2789	 */
2790	function mpa_boolval(value) {
2791	  return !!value;
2792	}
2793
2794	/**
2795	 * @param {*} value
2796	 * @return {Number}
2797	 *
2798	 * @since 1.2
2799	 */
2800	function mpa_intval(value) {
2801	  let intValue = parseInt(value);
2802	  if (!isNaN(intValue)) {
2803		return intValue;
2804	  } else {
2805		return value << 0;
2806	  }
2807	}
2808
2809	/**
2810	 * @since 1.3.1
2811	 *
2812	 * @param {Number} number
2813	 * @param {Number} min
2814	 * @param {Number} max
2815	 * @return {Number} The number in range [min; max].
2816	 */
2817	function mpa_limit(number, min, max) {
2818	  return Math.max(min, Math.min(number, max));
2819	}
2820
2821	/**
2822	 * @abstract
2823	 *
2824	 * @since 1.0
2825	 */
2826	class AbstractRepository {
2827	  /**
2828	   * @since 1.0
2829	   */
2830	  constructor(postType) {
2831		this.postType = postType;
2832		this.entityType = mpa_unprefix(postType);
2833		this.savedEntities = {}; // {Entity ID: Entity instance}
2834	  }
2835
2836	  /**
2837	   * @param {Number} id
2838	   * @param {Boolean} forceReload Optional. False by default.
2839	   * @return {Promise}
2840	   *
2841	   * @since 1.0
2842	   */
2843	  findById(id, forceReload = false) {
2844		if (!id) {
2845		  return Promise.resolve(null);
2846		} else if (!forceReload && this.haveEntity(id) && this.getEntity(id) != null) {
2847		  return Promise.resolve(this.getEntity(id));
2848		}
2849		return this.requestEntity(id).then(entityData => {
2850		  let entity = this.mapRestDataToEntity(entityData);
2851
2852		  // Save the most actual result
2853		  this.saveEntity(id, entity);
2854		  return entity;
2855		}, error => {
2856		  // Rewrite the last result, even if entity == null
2857		  this.saveEntity(id, null);
2858		  return null;
2859		});
2860	  }
2861
2862	  /**
2863	   * @param {Number[]} ids
2864	   * @param {Boolean} forceReload
2865	   * @return {AbstractEntity[])
2866	   */
2867	  findAll(ids, forceReload = false) {
2868		let loadIds = [];
2869		let entities = [];
2870		for (let id of ids) {
2871		  if (this.haveEntity(id) && !forceReload) {
2872			entities.push(this.getEntity(id));
2873		  } else {
2874			loadIds.push(id);
2875		  }
2876		}
2877		if (loadIds.length === 0) {
2878		  return Promise.resolve(entities);
2879		}
2880		return this.requestEntities(loadIds).then(rawEntities => {
2881		  for (let rawEntity of rawEntities) {
2882			let entity = this.mapRestDataToEntity(rawEntity);
2883			this.saveEntity(entity.id, entity);
2884			entities.push(entity);
2885		  }
2886		  return entities;
2887		}, error => []);
2888	  }
2889
2890	  /**
2891	   * @param {Number} id
2892	   * @return {Promise}
2893	   *
2894	   * @access protected
2895	   *
2896	   * @since 1.0
2897	   */
2898	  requestEntity(id) {
2899		return mpa_rest_get(this.getRoute(), {
2900		  id
2901		});
2902	  }
2903
2904	  /**
2905	   * @access protected
2906	   *
2907	   * @param {Number[]} ids
2908	   * @return {Promise}
2909	   */
2910	  requestEntities(ids) {
2911		return mpa_rest_get(this.getRoute(), {
2912		  id: ids
2913		});
2914	  }
2915
2916	  /**
2917	   * @access protected
2918	   *
2919	   * @param {Number} id
2920	   * @return {Boolean}
2921	   */
2922	  haveEntity(id) {
2923		return id in this.savedEntities;
2924	  }
2925
2926	  /**
2927	   * @access protected
2928	   *
2929	   * @param {Number} id
2930	   * @return {AbstractEntity|null}
2931	   */
2932	  getEntity(id) {
2933		return this.savedEntities[id] || null;
2934	  }
2935
2936	  /**
2937	   * @param {Number} id
2938	   * @param {AbstractEntity|Null} entity
2939	   *
2940	   * @access protected
2941	   *
2942	   * @since 1.0
2943	   */
2944	  saveEntity(id, entity) {
2945		this.savedEntities[id] = entity;
2946	  }
2947
2948	  /**
2949	   * @abstract
2950	   *
2951	   * @param {Object} entityData
2952	   * @return {AbstractEntity}
2953	   *
2954	   * @access protected
2955	   *
2956	   * @since 1.0
2957	   */
2958	  mapRestDataToEntity(entityData) {
2959		return null;
2960	  }
2961
2962	  /**
2963	   * @return {String}
2964	   *
2965	   * @since 1.0
2966	   */
2967	  getRoute() {
2968		return `/${this.entityType}s`; // '/services'
2969	  }
2970	}
2971
2972	/**
2973	 * @since 1.11.0
2974	 */
2975	class CouponRepository extends AbstractRepository {
2976	  /**
2977	   * @return {Promise} Coupon or null|Error.
2978	   */
2979	  findByCode(couponCode, suppressErrors = false) {
2980		return mpa_rest_get(this.getRoute(), {
2981		  code: couponCode
2982		}).then(couponData => {
2983		  let coupon = this.mapRestDataToEntity(couponData);
2984
2985		  // Save the most actual result
2986		  this.saveEntity(coupon.getId(), coupon);
2987		  return coupon;
2988		}, error => {
2989		  if (!suppressErrors) {
2990			throw error;
2991		  } else {
2992			return null;
2993		  }
2994		});
2995	  }
2996
2997	  /**
2998	   * @param {Object} entityData
2999	   * @return {Coupon}
3000	   *
3001	   * @access protected
3002	   */
3003	  mapRestDataToEntity(entityData) {
3004		return new Coupon(entityData.id, entityData);
3005	  }
3006	}
3007
3008	/**
3009	 * @param {Date|String} time
3010	 * @param {String} format Optional. 'public', 'internal' ('H:i') or custom time
3011	 *     format. 'public' by default.
3012	 * @return {String}
3013	 *
3014	 * @since 1.0
3015	 */
3016	function mpa_format_time(time, format = 'public') {
3017	  if (format == 'internal') {
3018		return mpa_format_date(time, 'H:i');
3019	  } else if (format == 'public') {
3020		return mpa_format_date(time, mpa_time_format());
3021	  } else {
3022		return mpa_format_date(time, format);
3023	  }
3024	}
3025
3026	/**
3027	 * @param {String} timeString Only the internal format is acceptable - 'H:i'
3028	 *     (the function will not check if the format is OK).
3029	 * @return {Date}
3030	 *
3031	 * @since 1.0
3032	 */
3033	function mpa_parse_time(timeString) {
3034	  let time = timeString.split(':');
3035	  let hours = parseInt(time[0]);
3036	  let minutes = parseInt(time[1]);
3037	  let date = mpa_today();
3038	  date.setHours(hours, minutes);
3039	  return date;
3040	}
3041
3042	/**
3043	 * Public time format, set in Settings > General.
3044	 *
3045	 * @return {String}
3046	 *
3047	 * @since 1.0
3048	 */
3049	function mpa_time_format() {
3050	  return mpapp().settings().getTimeFormat();
3051	}
3052
3053	/**
3054	 * @param {Number} serviceId
3055	 * @param {Date} fromDate
3056	 * @param {Date} toDate
3057	 * @param {Object} args Optional.
3058	 *     @param {Number[]} args['employees']   Optional. Limited list of employee IDs.
3059	 *     @param {Number[]} args['locations']   Optional. Limited list of location IDs.
3060	 *     @param {String}   args['exclude']     Optional. Items to exclude in the format of the cart items.
3061	 * @return {Promise} <code>{'Y-m-d' => {Time period string => Array of [Employee ID, Location ID]}}</code>
3062	 *
3063	 * @since 1.2.1
3064	 */
3065	function mpa_time_slots(serviceId, fromDate, toDate, args) {
3066	  // Build new args object for REST API
3067	  let restArgs = {
3068		service_id: serviceId,
3069		employees: args['employees'] ? args['employees'].join(',') : '',
3070		locations: args['locations'] ? args['locations'].join(',') : '',
3071		from_date: mpa_format_date(fromDate, 'internal'),
3072		to_date: mpa_format_date(toDate, 'internal'),
3073		format: 'full',
3074		exclude: args['exclude'] ? args['exclude'] : [],
3075		since_today: 'since_today' in args ? args['since_today'] : true
3076	  };
3077
3078	  // Request time slots
3079	  return mpa_rest_get('/calendar/time?nocache', restArgs).catch(error => console.error('Failed to make time slots in mpa_time_slots().', error.message) || {});
3080	}
3081
3082	/**
3083	 * @since 1.0
3084	 */
3085	class TimePeriod {
3086	  /**
3087	   * @param {Date|String} timeOrPeriod
3088	   * @param {Date|String|Null} endTime
3089	   *
3090	   * @since 1.0
3091	   */
3092	  constructor(timeOrPeriod, endTime = null) {
3093		this.setupProperties();
3094		if (endTime == null) {
3095		  this.parsePeriod(timeOrPeriod);
3096		} else {
3097		  this.setStartTime(timeOrPeriod);
3098		  this.setEndTime(endTime);
3099		}
3100	  }
3101
3102	  /**
3103	   * @access protected
3104	   *
3105	   * @since 1.0
3106	   */
3107	  setupProperties() {
3108		this.startTime = null; // Date
3109		this.endTime = null; // Date
3110	  }
3111
3112	  /**
3113	   * @param {String} period
3114	   *
3115	   * @since 1.0
3116	   */
3117	  parsePeriod(period) {
3118		// Explode '08:00 - 14:00' into ['08:00', '14:00']
3119		let time = period.split(' - ');
3120		this.setStartTime(time[0]);
3121		this.setEndTime(time[1]);
3122	  }
3123
3124	  /**
3125	   * @param {String|Number} startTime
3126	   *
3127	   * @since 1.0
3128	   */
3129	  setStartTime(startTime) {
3130		this.startTime = this.convertToTime(startTime);
3131	  }
3132
3133	  /**
3134	   * @param {String|Number} endTime
3135	   *
3136	   * @since 1.0
3137	   */
3138	  setEndTime(endTime) {
3139		this.endTime = this.convertToTime(endTime);
3140	  }
3141
3142	  /**
3143	   * @param {Date|String} input
3144	   * @return {Date}
3145	   *
3146	   * @access protected
3147	   *
3148	   * @since 1.0
3149	   */
3150	  convertToTime(input) {
3151		if (typeof input == 'string') {
3152		  return mpa_parse_time(input);
3153		} else {
3154		  return new Date(input); // Clone
3155		}
3156	  }
3157
3158	  /**
3159	   * @param {Date} date
3160	   *
3161	   * @since 1.0
3162	   */
3163	  setDate(date) {
3164		this.startTime.setFullYear(date.getFullYear());
3165		this.startTime.setMonth(date.getMonth(), date.getDate());
3166		this.endTime.setFullYear(date.getFullYear());
3167		this.endTime.setMonth(date.getMonth(), date.getDate());
3168	  }
3169
3170	  /**
3171	   * @param {TimePeriod} period
3172	   * @return {Boolean}
3173	   *
3174	   * @since 1.0
3175	   */
3176	  intersectsWith(period) {
3177		return this.startTime < period.endTime && this.endTime > period.startTime;
3178	  }
3179
3180	  /**
3181	   * @param {TimePeriod} period
3182	   * @return {Boolean}
3183	   *
3184	   * @since 1.0
3185	   */
3186	  isSubperiodOf(period) {
3187		return this.startTime >= period.startTime && this.endTime <= period.endTime;
3188	  }
3189
3190	  /**
3191	   * @param {TimePeriod} period
3192	   *
3193	   * @since 1.0
3194	   */
3195	  mergePeriod(period) {
3196		this.startTime.setTime(Math.min(this.startTime.getTime(), period.startTime.getTime()));
3197		this.endTime.setTime(Math.max(this.endTime.getTime(), period.endTime.getTime()));
3198	  }
3199
3200	  /**
3201	   * @param {TimePeriod} period
3202	   *
3203	   * @since 1.0
3204	   */
3205	  diffPeriod(period) {
3206		if (this.startTime < period.startTime) {
3207		  this.endTime.setTime(Math.min(period.startTime.getTime(), this.endTime.getTime()));
3208		} else {
3209		  this.startTime.setTime(Math.max(period.endTime.getTime(), this.startTime.getTime()));
3210		}
3211	  }
3212
3213	  /**
3214	   * @param {TimePeriod} period Must be subperiod of the current object.
3215	   * @return {TimePeriod[]}
3216	   *
3217	   * @since 1.0
3218	   */
3219	  splitByPeriod(period) {
3220		let split = [];
3221		if (period.startTime.getTime() - this.startTime.getTime() > 0) {
3222		  split.push(new TimePeriod(this.startTime, period.startTime));
3223		}
3224		if (this.endTime.getTime() - period.endTime.getTime() > 0) {
3225		  split.push(new TimePeriod(period.endTime, this.endTime));
3226		}
3227		return split;
3228	  }
3229
3230	  /**
3231	   * @return {Boolean}
3232	   *
3233	   * @since 1.0
3234	   */
3235	  isEmpty() {
3236		return this.endTime.getTime() - this.startTime.getTime() <= 0;
3237	  }
3238
3239	  /**
3240	   * @param {String} format Optional. 'public', 'short', 'internal' or custom
3241	   *     time format. 'public' by default.
3242	   * @param {String} glue Optional. ' - ' by default.
3243	   * @return {String}
3244	   *
3245	   * @since 1.0
3246	   */
3247	  toString(format = 'public', glue = ' - ') {
3248		// Force glue ' - ' for internal values
3249		if (format == 'internal') {
3250		  glue = ' - ';
3251		}
3252
3253		// mpa_format_time() does not support format 'short'
3254		let timeFormat = format == 'short' ? 'public' : format;
3255		let startTime = mpa_format_time(this.startTime, timeFormat);
3256		let endTime = mpa_format_time(this.endTime, timeFormat);
3257		if (format == 'short' && startTime == endTime) {
3258		  return startTime;
3259		} else {
3260		  return startTime + glue + endTime;
3261		}
3262	  }
3263	}
3264
3265	/**
3266	 * @since 1.0
3267	 */
3268	class Reservation extends AbstractEntity {
3269	  /**
3270	   * @access protected
3271	   *
3272	   * @since 1.0
3273	   */
3274	  setupProperties() {
3275		// Declare only the required minimum for methods. Still get all fields
3276		// from REST requests
3277		super.setupProperties();
3278		this.serviceId = 0;
3279		this.date = null; // Date
3280
3281		this.serviceTime = null; // TimePeriod
3282		this.bufferTime = null; // TimePeriod
3283	  }
3284
3285	  /**
3286	   * @param {Object} properties
3287	   *
3288	   * @access protected
3289	   *
3290	   * @since 1.0
3291	   */
3292	  setupValues(properties) {
3293		for (let property in properties) {
3294		  if (property == 'date') {
3295			this.setDate(properties[property]);
3296		  } else if (property == 'serviceTime') {
3297			this.setServiceTime(properties[property]);
3298		  } else if (property == 'bufferTime') {
3299			this.setBufferTime(properties[property]);
3300		  } else {
3301			this[property] = properties[property];
3302		  }
3303		}
3304	  }
3305
3306	  /**
3307	   * @param {Date|String} date
3308	   *
3309	   * @since 1.0
3310	   */
3311	  setDate(date) {
3312		if (typeof date == 'string') {
3313		  this.date = mpa_parse_date(date);
3314		} else {
3315		  this.date = date;
3316		}
3317		if (this.serviceTime != null) {
3318		  this.serviceTime.setDate(this.date);
3319		}
3320		if (this.bufferTime != null) {
3321		  this.bufferTime.setDate(this.date);
3322		}
3323	  }
3324
3325	  /**
3326	   * @param {TimePeriod|String} serviceTime
3327	   *
3328	   * @since 1.0
3329	   */
3330	  setServiceTime(serviceTime) {
3331		if (typeof serviceTime == 'string') {
3332		  this.serviceTime = new TimePeriod(serviceTime);
3333		} else {
3334		  this.serviceTime = serviceTime;
3335		}
3336		if (this.date != null) {
3337		  this.serviceTime.setDate(this.date);
3338		}
3339	  }
3340
3341	  /**
3342	   * @param {TimePeriod|String} bufferTime
3343	   *
3344	   * @since 1.0
3345	   */
3346	  setBufferTime(bufferTime) {
3347		if (typeof bufferTime == 'string') {
3348		  this.bufferTime = new TimePeriod(bufferTime);
3349		} else {
3350		  this.bufferTime = bufferTime;
3351		}
3352		if (this.date != null) {
3353		  this.bufferTime.setDate(this.date);
3354		}
3355	  }
3356	}
3357
3358	/**
3359	 * @since 1.0
3360	 */
3361	class ReservationRepository extends AbstractRepository {
3362	  /**
3363	   * @param {Number} serviceId
3364	   * @param {Object} args Optional.
3365	   *     @param {Date|String} args['from_date']
3366	   *     @param {Date|String} args['to_date']
3367	   * @return {Promise}
3368	   *
3369	   * @since 1.0
3370	   */
3371	  findAllByService(serviceId, args = {}) {
3372		let restArgs = {
3373		  service_id: serviceId
3374		};
3375
3376		// Add date range
3377		if (args.from_date != undefined) {
3378		  restArgs['from_date'] = mpa_format_date(args.from_date, 'internal');
3379		}
3380		if (args.to_date != undefined) {
3381		  restArgs['to_date'] = mpa_format_date(args.to_date, 'internal');
3382		}
3383
3384		// Request reservations
3385		let findPromise = mpa_rest_get('/bookings/reservations?nocache', restArgs)
3386
3387		// Save entities
3388		.then(reservations => {
3389		  let entities = [];
3390		  for (let reservation of reservations) {
3391			let entity = this.mapRestDataToEntity(reservation);
3392
3393			// Save entities
3394			this.saveEntity(entity.id, entity);
3395			entities.push(entity);
3396		  }
3397		  return entities;
3398		})
3399
3400		// Log error
3401		.catch(error => {
3402		  console.error('No reservations found.', error.message);
3403		  return []; // Always return array
3404		});
3405
3406		return findPromise;
3407	  }
3408
3409	  /**
3410	   * @param {Object} entityData
3411	   * @return {Reservation}
3412	   *
3413	   * @access protected
3414	   *
3415	   * @since 1.0
3416	   */
3417	  mapRestDataToEntity(entityData) {
3418		return new Reservation(entityData.id, entityData);
3419	  }
3420	}
3421
3422	/**
3423	 * @since 1.0
3424	 */
3425	class DatePeriod {
3426	  /**
3427	   * @param {Date|String} dateOrPeriod
3428	   * @param {Date|String|Null} endDate
3429	   *
3430	   * @since 1.0
3431	   */
3432	  constructor(dateOrPeriod, endDate = null) {
3433		this.setupProperties();
3434		if (endDate == null) {
3435		  this.parsePeriod(dateOrPeriod);
3436		} else {
3437		  this.setStartDate(dateOrPeriod);
3438		  this.setEndDate(endDate);
3439		}
3440	  }
3441
3442	  /**
3443	   * @access protected
3444	   *
3445	   * @since 1.0
3446	   */
3447	  setupProperties() {
3448		this.startDate = null; // Date
3449		this.endDate = null; // Date
3450	  }
3451
3452	  /**
3453	   * @param {String} period
3454	   *
3455	   * @access protected
3456	   *
3457	   * @since 1.0
3458	   */
3459	  parsePeriod(period) {
3460		// Explode '2020-01-25 - 2020-02-10' into ['2020-01-25', '2020-02-10']
3461		let dates = period.split(' - ');
3462		this.setStartDate(dates[0]);
3463		this.setEndDate(dates[1]);
3464	  }
3465
3466	  /**
3467	   * @param {String|Date} startDate
3468	   *
3469	   * @since 1.0
3470	   */
3471	  setStartDate(startDate) {
3472		this.startDate = this.convertToDate(startDate);
3473	  }
3474
3475	  /**
3476	   * @param {String|Date} endDate
3477	   *
3478	   * @since 1.0
3479	   */
3480	  setEndDate(endDate) {
3481		this.endDate = this.convertToDate(endDate);
3482	  }
3483
3484	  /**
3485	   * @param {Date|String} input
3486	   * @return {Date}
3487	   *
3488	   * @access protected
3489	   *
3490	   * @since 1.0
3491	   */
3492	  convertToDate(input) {
3493		if (typeof input == 'string') {
3494		  return mpa_parse_date(input) || mpa_today();
3495		} else {
3496		  return new Date(input); // Clone
3497		}
3498	  }
3499
3500	  /**
3501	   * @return {Number}
3502	   *
3503	   * @since 1.0
3504	   */
3505	  calcDays() {
3506		let diff = this.endDate.getTime() - this.startDate.getTime();
3507
3508		// Convert milliseconds to days
3509		let days = Math.round(diff / 1000 / 3600 / 24); // Remember: both dates have time 00:00:00.000,
3510		// so the math function is not so important
3511
3512		return days;
3513	  }
3514
3515	  /**
3516	   * @param {Date|String} date
3517	   * @return {Boolean}
3518	   *
3519	   * @since 1.0
3520	   */
3521	  inPeriod(date) {
3522		if (typeof date == 'string') {
3523		  date = mpa_parse_date(date);
3524		}
3525		return date != null && date >= this.startDate && date <= this.endDate;
3526	  }
3527
3528	  /**
3529	   * @return {Object} {"Y-m-d" date string: Date}
3530	   *
3531	   * @since 1.0
3532	   */
3533	  splitToDates() {
3534		let dates = {};
3535		for (let date = new Date(this.startDate); date <= this.endDate; date.setDate(date.getDate() + 1)) {
3536		  let dateString = mpa_format_date(date, 'internal');
3537		  let dateClone = new Date(date);
3538		  dates[dateString] = dateClone;
3539		}
3540		return dates;
3541	  }
3542
3543	  /**
3544	   * @return {String}
3545	   *
3546	   * @since 1.0
3547	   */
3548	  toString() {
3549		return mpa_format_date(this.startDate, 'internal') + ' - ' + mpa_format_date(this.endDate, 'internal');
3550	  }
3551	}
3552
3553	/**
3554	 * @since 1.0
3555	 */
3556	class Schedule extends AbstractEntity {
3557	  /**
3558	   * @access protected
3559	   *
3560	   * @since 1.0
3561	   */
3562	  setupProperties() {
3563		// Declare only the required minimum for methods. Still get all fields
3564		// from REST requests
3565		super.setupProperties();
3566		this.timetable = []; // [Day index: array of {time_period, location, activity}]
3567		this.workTimetable = []; // [Day index: array of {time_period, location}]
3568		this.customWorkdays = []; // Array of {date_period, time_period}
3569		this.daysOff = {}; // {"Y-m-d" date string: Date}
3570	  }
3571
3572	  /**
3573	   * @param {Object} properties
3574	   *
3575	   * @access protected
3576	   *
3577	   * @since 1.0
3578	   */
3579	  setupValues(properties) {
3580		for (let property in properties) {
3581		  if (property == 'timetable') {
3582			this.setTimetable(properties[property]);
3583		  } else if (property == 'customWorkdays') {
3584			this.setCustomWorkdays(properties[property]);
3585		  } else if (property == 'daysOff') {
3586			this.setDaysOff(properties[property]);
3587		  } else {
3588			this[property] = properties[property];
3589		  }
3590		}
3591	  }
3592
3593	  /**
3594	   * @param {Array} timetable [Day index: array of {time_period, location, activity}].
3595	   *
3596	   * @since 1.0
3597	   */
3598	  setTimetable(timetable) {
3599		this.timetable = [];
3600		this.workTimetable = [];
3601
3602		// For each day of a week
3603		timetable.forEach(periods => {
3604		  let dayPeriods = [];
3605		  let workPeriods = [];
3606
3607		  // Add periods to current day
3608		  periods.forEach(period => {
3609			let timePeriod = new TimePeriod(period['time_period']);
3610
3611			// Add period to general list
3612			dayPeriods.push({
3613			  time_period: timePeriod,
3614			  location: period['location'],
3615			  activity: period['activity']
3616			});
3617
3618			// Add period to working list
3619			if (period['activity'] == 'work') {
3620			  workPeriods.push({
3621				time_period: timePeriod,
3622				location: period['location']
3623			  });
3624			}
3625		  });
3626
3627		  // Save day periods
3628		  this.timetable.push(dayPeriods);
3629		  this.workTimetable.push(workPeriods);
3630		});
3631	  }
3632
3633	  /**
3634	   * @param {Array} customWorkdays Array of {date_period, time_period}.
3635	   *
3636	   * @since 1.0
3637	   */
3638	  setCustomWorkdays(customWorkdays) {
3639		this.customWorkdays = [];
3640		for (let period of customWorkdays) {
3641		  this.customWorkdays.push({
3642			date_period: new DatePeriod(period['date_period']),
3643			time_period: new TimePeriod(period['time_period'])
3644		  });
3645		}
3646	  }
3647
3648	  /**
3649	   * @param {String[]} daysOff
3650	   *
3651	   * @since 1.0
3652	   */
3653	  setDaysOff(daysOff) {
3654		this.daysOff = {};
3655
3656		// For each date period
3657		for (let dayOff of daysOff) {
3658		  let datePeriod = new DatePeriod(dayOff);
3659		  let datesInPeriod = datePeriod.splitToDates();
3660		  jQuery.extend(this.daysOff, datesInPeriod);
3661		}
3662	  }
3663
3664	  /**
3665	   * @param {Date|String} date
3666	   * @return {Boolean}
3667	   *
3668	   * @since 1.0
3669	   */
3670	  isDayOff(date) {
3671		if (typeof date != 'string') {
3672		  date = mpa_format_date(date, 'internal');
3673		}
3674		return date in this.daysOff;
3675	  }
3676
3677	  /**
3678	   * @param {Date|String} date
3679	   * @param {Number} locationId Optional.
3680	   * @return {TimePeriod[]}
3681	   *
3682	   * @since 1.0
3683	   */
3684	  getWorkingHours(date, locationId = 0) {
3685		// Check the days off first
3686		if (this.isDayOff(date)) {
3687		  return [];
3688		}
3689
3690		// Convert date string to Date object
3691		if (typeof date == 'string') {
3692		  date = mpa_parse_date(date);
3693		}
3694		if (date == null) {
3695		  return [];
3696		}
3697
3698		// First, get time periods from the timetable
3699		let timePeriods = [];
3700		let dayOfWeek = date.getDay(); // 0-6 (Sunday-Saturday)
3701
3702		for (let period of this.workTimetable[dayOfWeek]) {
3703		  // Filter by location ID
3704		  if (locationId == 0 || period['location'] == locationId) {
3705			timePeriods.push(period['time_period']);
3706		  }
3707		}
3708
3709		// Second, add custom workdays no matter the location
3710		for (let period of this.customWorkdays) {
3711		  if (period['date_period'].inPeriod(date)) {
3712			timePeriods.push(period['time_period']);
3713		  }
3714		}
3715		return timePeriods;
3716	  }
3717	}
3718
3719	/**
3720	 * @since 1.0
3721	 */
3722	class ScheduleRepository extends AbstractRepository {
3723	  /**
3724	   * @param {Object} entityData
3725	   * @return {Schedule}
3726	   *
3727	   * @access protected
3728	   *
3729	   * @since 1.0
3730	   */
3731	  mapRestDataToEntity(entityData) {
3732		return new Schedule(entityData.id, entityData);
3733	  }
3734	}
3735
3736	/**
3737	 * @since 1.0
3738	 */
3739	class ServiceRepository extends AbstractRepository {
3740	  /**
3741	   * @param {Object} entityData
3742	   * @return {Service}
3743	   *
3744	   * @access protected
3745	   *
3746	   * @since 1.0
3747	   */
3748	  mapRestDataToEntity(entityData) {
3749		return new Service(entityData.id, entityData);
3750	  }
3751	}
3752
3753	/**
3754	 * @since 1.0
3755	 */
3756	class RepositoriesContainer {
3757	  /**
3758	   * @since 1.0
3759	   */
3760	  constructor() {
3761		this.repositories = {};
3762	  }
3763
3764	  /**
3765	   * @return {ScheduleRepository}
3766	   *
3767	   * @since 1.0
3768	   */
3769	  schedule() {
3770		if (this.repositories['schedule'] == undefined) {
3771		  this.repositories['schedule'] = new ScheduleRepository('mpa_schedule');
3772		}
3773		return this.repositories['schedule'];
3774	  }
3775
3776	  /**
3777	   * @return {ServiceRepository}
3778	   *
3779	   * @since 1.0
3780	   */
3781	  service() {
3782		if (this.repositories['service'] == undefined) {
3783		  this.repositories['service'] = new ServiceRepository('mpa_service');
3784		}
3785		return this.repositories['service'];
3786	  }
3787
3788	  /**
3789	   * @return {ReservationRepository}
3790	   *
3791	   * @since 1.0
3792	   */
3793	  reservation() {
3794		if (this.repositories['reservation'] == undefined) {
3795		  this.repositories['reservation'] = new ReservationRepository('mpa_reservation');
3796		}
3797		return this.repositories['reservation'];
3798	  }
3799
3800	  /**
3801	   * @return {CouponRepository}
3802	   *
3803	   * @since 1.11.0
3804	   */
3805	  coupon() {
3806		if (this.repositories['coupon'] == undefined) {
3807		  this.repositories['coupon'] = new CouponRepository('mpa_coupon');
3808		}
3809		return this.repositories['coupon'];
3810	  }
3811
3812	  /**
3813	   * @return {CustomerRepository}
3814	   *
3815	   * @since 1.18.0
3816	   */
3817	  customer() {
3818		if (this.repositories['customer'] === undefined) {
3819		  this.repositories['customer'] = new CustomerRepository();
3820		}
3821		return this.repositories['customer'];
3822	  }
3823
3824	  /**
3825	  * @return {RepositoriesContainer}
3826	  *
3827	  * @since 1.0
3828	  */
3829	  static getInstance() {
3830		if (RepositoriesContainer.instance == undefined) {
3831		  RepositoriesContainer.instance = new RepositoriesContainer();
3832		}
3833		return RepositoriesContainer.instance;
3834	  }
3835	}
3836
3837	// Move repository functions to separate file to prevent a dependency cycle that
3838	// leads to an error "Cannot read property 'default' of undefined" on mpapp()
3839
3840
3841	/**
3842	 * @return {RepositoriesContainer}
3843	 *
3844	 * @since 1.0
3845	 */
3846	function mpa_repositories() {
3847	  return RepositoriesContainer.getInstance();
3848	}
3849
3850	let extractedServices = null; // Promise
3851
3852	/**
3853	 * @param {Boolean} forceReload Optional. False by default.
3854	 * @return {Promise}
3855	 *
3856	 * @since 1.0
3857	 */
3858	function mpa_extract_available_services(forceReload = false) {
3859	  if (forceReload || extractedServices == null) {
3860		// Load available services
3861		extractedServices = mpa_rest_get('/services/available?nocache').catch(error => {
3862		  console.error('Unable to extract available services.');
3863		  return {};
3864		});
3865	  }
3866	  return extractedServices;
3867	}
3868
3869	/**
3870	 * @since 1.4.0
3871	 *
3872	 * @param {*} value
3873	 * @return {Boolean}
3874	 */
3875	function mpa_filter_default(value) {
3876	  return mpa_boolval(value);
3877	}
3878
3879	/**
3880	 * @since 1.4.0 (Replaced the class <code>BookingService</code>)
3881	 */
3882	class AvailabilityService {
3883	  /**
3884	   * @since 1.4.0
3885	   * @access protected
3886	   */
3887	  setupProperties() {
3888		/**
3889		 * @since 1.4.0
3890		 * @var {Object}
3891		 * @access protected
3892		 * @see PHP function mpa_extract_available_services() to see the structure.
3893		 */
3894		this.availability = {};
3895
3896		/**
3897		 * @since 1.4.0
3898		 * @var {Object} {Service ID: Service name}
3899		 */
3900		this.services = {};
3901
3902		/**
3903		 * @since 1.4.0
3904		 * @var {Object} {Category slug: Category name}
3905		 */
3906		this.serviceCategories = {};
3907
3908		/**
3909		 * @since 1.4.0
3910		 * @var {Object} {Employee ID: Employee name}
3911		 */
3912		this.employees = {};
3913
3914		/**
3915		 * @since 1.4.0
3916		 * @var {Object} {Location ID: Location name}
3917		 */
3918		this.locations = {};
3919
3920		/**
3921		 * @since 1.4.0
3922		 * @var {Promise}
3923		 * @access protected
3924		 */
3925		this.readyPromise = null;
3926	  }
3927
3928	  /**
3929	   * @since 1.4.0
3930	   *
3931	   * @param {Boolean} forceReload Optional. False by default.
3932	   */
3933	  constructor(forceReload = false) {
3934		this.setupProperties();
3935		this.readyPromise = mpa_extract_available_services(forceReload).then(availability => {
3936		  this.setAvailability(availability);
3937		  return this;
3938		});
3939	  }
3940
3941	  /**
3942	   * @since 1.4.0
3943	   * @access protected
3944	   *
3945	   * @param {Object} availability
3946	   */
3947	  setAvailability(availability) {
3948		this.availability = availability;
3949
3950		// List available services, categories, employees and locations
3951		for (let serviceId in availability) {
3952		  let service = availability[serviceId];
3953
3954		  // List services
3955		  this.services[serviceId] = service.name;
3956
3957		  // List categories
3958		  for (let categorySlug in service.categories) {
3959			let categoryName = service.categories[categorySlug];
3960			this.serviceCategories[categorySlug] = categoryName;
3961		  }
3962
3963		  // List employees and locations
3964		  for (let employeeId in service.employees) {
3965			let employee = service.employees[employeeId];
3966
3967			// List employees
3968			this.employees[employeeId] = employee.name;
3969
3970			// List locations
3971			for (let locationId in employee.locations) {
3972			  let locationName = employee.locations[locationId];
3973			  this.locations[locationId] = locationName;
3974			}
3975		  }
3976		}
3977	  }
3978
3979	  /**
3980	   * @since 1.4.0
3981	   *
3982	   * @return {Boolean}
3983	   */
3984	  isEmpty() {
3985		return mpa_empty(this.availability);
3986	  }
3987
3988	  /**
3989	   * @since 1.4.0
3990	   *
3991	   * @return {Promise}
3992	   */
3993	  ready() {
3994		return this.readyPromise;
3995	  }
3996
3997	  /**
3998	   * @since 1.4.0
3999	   *
4000	   * @param {Number} serviceId
4001	   * @param {Boolean} load Optional. Load properties of the service. True by
4002	   *		default.
4003	   * @return {Service}
4004	   */
4005	  getService(serviceId, load = true, callbackAfterLoading = null) {
4006		let service = new Service(serviceId);
4007
4008		// Set known values
4009		if (this.services.hasOwnProperty(serviceId)) {
4010		  service.name = this.services[serviceId];
4011		}
4012
4013		// Load all other properties
4014		if (load === true) {
4015		  EntityUtils.loadInBackground(service, mpa_repositories().service()).then(callbackAfterLoading);
4016		}
4017		return service;
4018	  }
4019
4020	  /**
4021	   * @param {Number} serviceId
4022	   * @returns {category slug: category name, ...}
4023	   */
4024	  getServiceCategories(serviceId) {
4025		return this.availability[serviceId].categories;
4026	  }
4027
4028	  /**
4029	   * @since 1.4.0
4030	   *
4031	   * @param {Number} employeeId
4032	   * @return {Employee}
4033	   */
4034	  getEmployee(employeeId) {
4035		let employee = new Employee(employeeId);
4036
4037		// Set known values
4038		if (this.employees.hasOwnProperty(employeeId)) {
4039		  employee.name = this.employees[employeeId];
4040		}
4041		return employee;
4042	  }
4043
4044	  /**
4045	   * @since 1.4.0
4046	   *
4047	   * @param {Number} locationId
4048	   * @return {Location}
4049	   */
4050	  getLocation(locationId) {
4051		let location = new Location(locationId);
4052
4053		// Set known values
4054		if (this.locations.hasOwnProperty(locationId)) {
4055		  location.name = this.locations[locationId];
4056		}
4057		return location;
4058	  }
4059
4060	  /**
4061	   * @since 1.4.0
4062	   * @since 1.19.0 param locationId
4063	   * @since 1.19.0 param employeeId
4064	   *
4065	   * @param {String} categorySlug Optional. '' by default (all categories).
4066	   * @param {Number} locationId Optional. 0 by default (all locations).
4067	   * @param {Number} employeeId Optional. 0 by default (all employees).
4068	   * @return {Object} {Service ID: Service name}
4069	   */
4070	  getAvailableServices(categorySlug = '', locationId = 0, employeeId = 0) {
4071		let services = {};
4072
4073		// For each service
4074		for (let serviceId in this.availability) {
4075		  let service = this.availability[serviceId];
4076
4077		  // Filter by category
4078		  if (categorySlug !== '' && !(categorySlug in service.categories)) {
4079			continue;
4080		  }
4081
4082		  // Filter by location
4083		  if (locationId !== 0) {
4084			let found = false;
4085			Object.keys(service.employees).forEach(employeeIndex => {
4086			  if (service.employees[employeeIndex].locations.hasOwnProperty(locationId)) {
4087				found = true;
4088			  }
4089			});
4090			if (!found) {
4091			  continue;
4092			}
4093		  }
4094
4095		  // Filter by employee
4096		  if (employeeId !== 0 && !(employeeId in service.employees)) {
4097			continue;
4098		  }
4099
4100		  // Add service
4101		  services[serviceId] = service.name;
4102		}
4103		return services;
4104	  }
4105
4106	  /**
4107	   * @since 1.4.0
4108	   * @since 1.19.0 param serviceId
4109	   *
4110	   * @param {Number} serviceId Optional. 0 by default (all categories).
4111	   *
4112	   * @return {Object} {Category slug: Category name}
4113	   */
4114	  getAvailableServiceCategories(serviceId = 0) {
4115		let categories = {};
4116
4117		// For each service
4118		for (let offerId in this.availability) {
4119		  // Filter by service ID
4120		  if (serviceId != 0 && offerId != serviceId) {
4121			continue;
4122		  }
4123		  let service = this.availability[offerId];
4124
4125		  // Merge service categories
4126		  jQuery.extend(categories, service.categories);
4127		}
4128		return categories;
4129	  }
4130
4131	  /**
4132	   * @since 1.4.0
4133	   * @since 1.19.0 param locationId
4134	   *
4135	   * @param {Number} serviceId Optional. 0 by default (all services).
4136	   * @param {Number} locationId Optional. 0 by default (all locations).
4137	   * @return {Object} {Employee ID: Employee name}
4138	   */
4139	  getAvailableEmployees(serviceId = 0, locationId = 0) {
4140		let employees = {};
4141
4142		// For each service
4143		for (let offerId in this.availability) {
4144		  // Filter by service ID
4145		  if (serviceId != 0 && offerId != serviceId) {
4146			continue;
4147		  }
4148		  let service = this.availability[offerId];
4149
4150		  // For each employee
4151		  for (let employeeId in service.employees) {
4152			let employee = service.employees[employeeId];
4153
4154			// Filter by locationId
4155			if (locationId !== 0 && !(locationId in employee.locations)) {
4156			  continue;
4157			}
4158
4159			// Add employee
4160			employees[employeeId] = employee.name;
4161		  }
4162		}
4163		return employees;
4164	  }
4165
4166	  /**
4167	   * @since 1.4.0
4168	   *
4169	   * @param {Number} serviceId Optional. 0 by default (all services).
4170	   * @param {Number} employeeId Optional. 0 by default (all employees).
4171	   * @return {Object} {Location ID: Location name}
4172	   */
4173	  getAvailableLocations(serviceId = 0, employeeId = 0) {
4174		let locations = {};
4175
4176		// For each service
4177		for (let offerId in this.availability) {
4178		  // Filter by service ID
4179		  if (serviceId != 0 && offerId != serviceId) {
4180			continue;
4181		  }
4182		  let service = this.availability[offerId];
4183
4184		  // For each employee
4185		  for (let workerId in service.employees) {
4186			// Filter by employee ID
4187			if (employeeId != 0 && workerId != employeeId) {
4188			  continue;
4189			}
4190			let employee = service.employees[workerId];
4191
4192			// Merge locations
4193			jQuery.extend(locations, employee.locations);
4194		  }
4195		}
4196		return locations;
4197	  }
4198
4199	  /**
4200	   * @since 1.19.0
4201	   *
4202	   * @param {string} categorySlug
4203	   * @return boolean
4204	   */
4205	  isAvailableServiceCategory(categorySlug) {
4206		return this.getAvailableServiceCategories().hasOwnProperty(categorySlug);
4207	  }
4208
4209	  /**
4210	   * @since 1.19.0
4211	   *
4212	   * @param {Number} serviceId
4213	   * @return boolean
4214	   */
4215	  isAvailableService(serviceId) {
4216		return this.getAvailableServices().hasOwnProperty(serviceId);
4217	  }
4218
4219	  /**
4220	   * @since 1.19.0
4221	   *
4222	   * @param {Number} locationId
4223	   * @return boolean
4224	   */
4225	  isAvailableLocation(locationId) {
4226		return this.getAvailableLocations().hasOwnProperty(locationId);
4227	  }
4228
4229	  /**
4230	   * @since 1.19.0
4231	   *
4232	   * @param {Number} employeeId
4233	   * @return boolean
4234	   */
4235	  isAvailableEmployee(employeeId) {
4236		return this.getAvailableEmployees().hasOwnProperty(employeeId);
4237	  }
4238
4239	  /**
4240	   * @since 1.4.0
4241	   *
4242	   * @param {Number} serviceId
4243	   * @param {Number|Number[]} locationId Optional. One or move allowed location.
4244	   * @param {String} output Optional. ids|entities. 'ids' by default.
4245	   * @return {Array}
4246	   */
4247	  filterAvailableEmployees(serviceId, locationId = 0, output = 'ids') {
4248		if (!(serviceId in this.availability)) {
4249		  // No such service in available services list
4250		  return [];
4251		}
4252
4253		// Init allowed location IDs
4254		let allowedLocations = [];
4255		if (Array.isArray(locationId)) {
4256		  allowedLocations = locationId.filter(mpa_filter_default);
4257		} else if (locationId !== 0) {
4258		  allowedLocations.push(locationId);
4259		}
4260
4261		// Select all valid employee IDs
4262		let employees = [];
4263
4264		// Filter #1: filter by service ID
4265		for (let employeeId in this.availability[serviceId]['employees']) {
4266		  employeeId = mpa_intval(employeeId);
4267		  let employee = this.availability[serviceId]['employees'][employeeId];
4268
4269		  // Filter #2: filter by location ID
4270		  if (allowedLocations.length === 0) {
4271			// Any employee with any locations list is valid
4272			employees.push(employeeId);
4273		  } else {
4274			// Does employee have a valid location?
4275			let employeeLocations = Object.keys(employee.locations).map(mpa_intval);
4276			let allowedForEmployee = mpa_array_intersect(allowedLocations, employeeLocations);
4277			if (allowedForEmployee.length > 0) {
4278			  employees.push(employeeId);
4279			}
4280		  }
4281		}
4282
4283		// Convert to proper output format
4284		if (employees.length === 0) {
4285		  return [];
4286		} else if (output === 'entities') {
4287		  return employees.map(employeeId => this.getEmployee(employeeId));
4288		} else {
4289		  return employees;
4290		}
4291	  }
4292
4293	  /**
4294	   * @since 1.4.0
4295	   *
4296	   * @param {Number} serviceId
4297	   * @param {Number|Number[]} employeeId Optional. One or move allowed employee.
4298	   * @param {String} output Optional. ids|entities. 'ids' by default.
4299	   * @return {Array}
4300	   */
4301	  filterAvailableLocations(serviceId, employeeId = 0, output = 'ids') {
4302		if (!(serviceId in this.availability)) {
4303		  // No such service in available services list
4304		  return [];
4305		}
4306
4307		// Init allowed employee IDs
4308		let allowedEmployees = [];
4309		if (Array.isArray(employeeId)) {
4310		  allowedEmployees = employeeId.filter(mpa_filter_default);
4311		} else if (employeeId !== 0) {
4312		  allowedEmployees.push(employeeId);
4313		}
4314
4315		// Select all valid location IDs
4316		let locations = [];
4317
4318		// Filter #1: filter by service ID
4319		for (employeeId in this.availability[serviceId]['employees']) {
4320		  employeeId = mpa_intval(employeeId);
4321
4322		  // Filter #2: filter by employee ID
4323		  if (allowedEmployees.length > 0 && allowedEmployees.indexOf(employeeId) === -1) {
4324			continue;
4325		  }
4326		  let employee = this.availability[serviceId]['employees'][employeeId];
4327
4328		  // Get all available location
4329		  for (let locationId in employee.locations) {
4330			locations.push(mpa_intval(locationId));
4331		  }
4332		}
4333
4334		// Filter duplicates
4335		locations = mpa_array_unique(locations);
4336
4337		// Convert to proper output format
4338		if (locations.length === 0) {
4339		  return [];
4340		} else if (output === 'entities') {
4341		  return locations.map(locationId => this.getLocation(locationId));
4342		} else {
4343		  return locations;
4344		}
4345	  }
4346	}
4347
4348	class BookingSteps {
4349	  /**
4350	   * @param {Cart} cart
4351	   */
4352	  constructor(cart) {
4353		this.cart = cart;
4354		this.steps = new Map();
4355		this.currentStep = null;
4356		this.currentStepId = '';
4357	  }
4358
4359	  /**
4360	   * @param {AbstractStep} step
4361	   * @return {BookingSteps}
4362	   */
4363	  addStep(step) {
4364		this.steps.push(step.stepId, step);
4365		return this;
4366	  }
4367
4368	  /**
4369	   * @param {String} stepId
4370	   * @return {AbstractStep|null}
4371	   */
4372	  getStep(stepId) {
4373		return this.steps.find(stepId);
4374	  }
4375	  mount($element) {
4376		this.addListeners($element);
4377	  }
4378
4379	  /**
4380	   * @access protected
4381	   */
4382	  addListeners($element) {
4383		$element.children('.mpa-booking-step').on('mpa_booking_step_next', (event, details) => this.onStep('next', details)).on('mpa_booking_step_back', (event, details) => this.onStep('back', details)).on('mpa_booking_step_new', (event, details) => this.onStep('new', details)).on('mpa_reset_booking', (event, details) => this.onStep('reset', details));
4384	  }
4385
4386	  /**
4387	   * @access protected
4388	   *
4389	   * @param {String} move next|back|new|reset
4390	   * @param {Object} eventDetails
4391	   *		@param {String} eventDetails['step'] Optional. Step ID that initiates the switch.
4392	   */
4393	  onStep(move, eventDetails) {
4394		if (eventDetails && eventDetails.step && eventDetails.step !== this.currentStepId) {
4395		  return; // Don't accept events from inactive steps
4396		}
4397
4398		// OK, switch step
4399		switch (move) {
4400		  case 'next':
4401			this.goToNextStep();
4402			break;
4403		  case 'back':
4404			this.goToPreviousStep();
4405			break;
4406		  case 'new':
4407			this.goToFirstStep();
4408			break;
4409		  case 'reset':
4410			this.reset();
4411			break;
4412		}
4413	  }
4414
4415	  /**
4416	   * @since 1.19.0
4417	   */
4418	  isCurrentStepHiddenGoToNextStep() {
4419		if (this.currentStep === null) {
4420		  return;
4421		}
4422		this.currentStep.ready().finally(() => {
4423		  if (this.currentStep.isHiddenStep) {
4424			// go to next step with submitting data
4425			this.currentStep.submit();
4426		  }
4427		});
4428	  }
4429
4430	  /**
4431	   * @since 1.19.0
4432	   */
4433	  isCurrentStepHiddenGoToPreviousStep() {
4434		if (this.currentStep === null) {
4435		  return;
4436		}
4437		this.currentStep.ready().finally(() => {
4438		  if (this.currentStep.isHiddenStep) {
4439			// go to previous step
4440			this.currentStep.cancel();
4441		  }
4442		});
4443	  }
4444	  goToNextStep() {
4445		if (this.steps.isEmpty()) {
4446		  return; // Nowhere to go
4447		}
4448
4449		let nextStepId = !this.currentStep ? this.steps.firstKey() : this.steps.findNextKey(this.currentStepId);
4450		if (nextStepId === this.currentStepId) {
4451		  return; // Nowhere to go, already at the last step
4452		}
4453
4454		this.switchStep(nextStepId);
4455		this.isCurrentStepHiddenGoToNextStep();
4456	  }
4457	  goToPreviousStep() {
4458		if (this.steps.isEmpty()) {
4459		  return; // Nowhere to go
4460		}
4461
4462		let previousStepId = !this.currentStep ? '' : this.steps.findPreviousKey(this.currentStepId);
4463		if (!previousStepId || previousStepId === this.currentStepId) {
4464		  return; // Nowhere to go, already at the beginning
4465		}
4466
4467		this.switchStep(previousStepId);
4468		this.isCurrentStepHiddenGoToPreviousStep();
4469	  }
4470
4471	  /**
4472	   * Goes to the first step when adding new item to the cart.
4473	   */
4474	  goToFirstStep() {
4475		if (this.steps.isEmpty()) {
4476		  return; // Nowhere to go
4477		}
4478
4479		// Add new item to the cart
4480		this.cart.createItem();
4481
4482		// Reset cart item steps
4483		this.steps.forEach(step => {
4484		  if (step.getCartContext() === 'cart item') {
4485			step.reset();
4486		  }
4487		});
4488
4489		// Switch to the first step
4490		let firstStepId = this.steps.firstKey();
4491		this.switchStep(firstStepId);
4492		this.isCurrentStepHiddenGoToNextStep();
4493	  }
4494
4495	  /**
4496	   * @param {String} stepId
4497	   */
4498	  goToStep(stepId) {
4499		this.switchStep(stepId);
4500	  }
4501
4502	  /**
4503	   * @since 1.19.0
4504	   * @return {String} Step Id
4505	   */
4506	  getFirstVisibleStepId() {
4507		let firstVisibleStepId = null;
4508		this.steps.forEach(step => {
4509		  if (step.isHiddenStep === false) {
4510			firstVisibleStepId = step.stepId;
4511			return false;
4512		  }
4513		});
4514		return firstVisibleStepId;
4515	  }
4516
4517	  /**
4518	   * @since 1.19.0
4519	   * @param {String} stepId
4520	   * @return {boolean}
4521	   */
4522	  isFirstVisibleStepId(stepId) {
4523		return this.getFirstVisibleStepId() === stepId;
4524	  }
4525
4526	  /**
4527	   * @access protected
4528	   *
4529	   * @param {String} newStepId
4530	   */
4531	  switchStep(newStepId) {
4532		let newStep = this.steps.find(newStepId);
4533		if (newStep == null) {
4534		  return; // Step not found
4535		}
4536
4537		if (this.isFirstVisibleStepId(newStepId)) {
4538		  newStep.hideButtonBack();
4539		}
4540
4541		// Hide current step
4542		if (this.currentStep != null) {
4543		  this.currentStep.hide();
4544		}
4545		this.currentStep = newStep;
4546		this.currentStepId = newStepId;
4547
4548		// Load and show new step
4549		newStep.load();
4550		newStep.ready().finally(() => newStep.show());
4551	  }
4552	  reset() {
4553		this.cart.reset();
4554
4555		// goToFirstStep() will also do the cart.createItem() after the reset
4556		this.goToFirstStep();
4557
4558		// Cart item steps already reseted. Reset all other steps.
4559		this.steps.forEach(step => {
4560		  if (step.getCartContext() !== 'cart item') {
4561			step.reset();
4562		  }
4563		});
4564	  }
4565	}
4566
4567	/**
4568	 * @abstract
4569	 *
4570	 * @since 1.0
4571	 */
4572	class AbstractStep {
4573	  /**
4574	   * @param {Object} $element jQuery element.
4575	   * @param {Cart} cart
4576	   *
4577	   * @since 1.0
4578	   */
4579	  constructor($element, cart) {
4580		this.$element = $element;
4581		this.cart = cart;
4582		this.setupProperties();
4583		this.addListeners();
4584	  }
4585
4586	  /**
4587	   * @access protected
4588	   *
4589	   * @since 1.0
4590	   */
4591	  setupProperties() {
4592		this.stepId = this.theId();
4593		this.schema = this.propertiesSchema();
4594		this.isActive = false;
4595		this.isLoaded = false;
4596		this.isHiddenStep = false;
4597		this.preventReact = false; // See setProperty()
4598		this.preventUpdate = false; // See setProperty()
4599
4600		this.hideButtons = false; // Hide buttons if one of the values is not
4601		// valid. If false, then buttons will be
4602		// disabled, but visible
4603
4604		this.readyPromise = null;
4605		this.$buttons = this.$element.find('.mpa-actions');
4606		this.$buttonBack = this.$buttons.find('.mpa-button-back');
4607		this.$buttonNext = this.$buttons.find('.mpa-button-next');
4608	  }
4609
4610	  /**
4611	   * @abstract
4612	   *
4613	   * @return {String}
4614	   *
4615	   * @access protected
4616	   *
4617	   * @since 1.0
4618	   */
4619	  theId() {
4620		return 'abstract';
4621	  }
4622
4623	  /**
4624	   * @return {String} 'cart'|'cart item'
4625	   *
4626	   * @since 1.9.0
4627	   */
4628	  getCartContext() {
4629		return 'cart';
4630	  }
4631
4632	  /**
4633	   * @return {Object} {Property name: {type, default}}
4634	   *
4635	   * @access protected
4636	   *
4637	   * @since 1.0
4638	   */
4639	  propertiesSchema() {
4640		return {};
4641	  }
4642
4643	  /**
4644	   * @access protected
4645	   *
4646	   * @since 1.0
4647	   */
4648	  addListeners() {
4649		this.$buttonBack.on('click', this.cancel.bind(this));
4650		this.$buttonNext.on('click', this.submit.bind(this));
4651	  }
4652
4653	  /**
4654	   * @since 1.0
4655	   */
4656	  load() {
4657		if (!this.isLoaded) {
4658		  this.readyPromise = this.loadEntities();
4659		  this.isLoaded = true;
4660		} else {
4661		  this.readyPromise = this.reload();
4662		}
4663	  }
4664
4665	  /**
4666	   * @return {Promise}
4667	   * @access protected
4668	   *
4669	   * @since 1.0
4670	   */
4671	  loadEntities() {
4672		return Promise.resolve(this);
4673	  }
4674
4675	  /**
4676	   * @return {Promise}
4677	   * @access protected
4678	   *
4679	   * @since 1.0
4680	   */
4681	  reload() {
4682		return Promise.resolve(this);
4683	  }
4684
4685	  /**
4686	   * Reset step before a new item.
4687	   *
4688	   * @since 1.4.0
4689	   * @abstract
4690	   */
4691	  reset() {}
4692
4693	  /**
4694	   * @return {Promise}
4695	   *
4696	   * @since 1.0
4697	   */
4698	  ready() {
4699		return this.readyPromise;
4700	  }
4701
4702	  /**
4703	   * @abstract
4704	   *
4705	   * @return {Boolean}
4706	   *
4707	   * @since 1.0
4708	   */
4709	  isValidInput() {
4710		return false;
4711	  }
4712
4713	  /**
4714	   * @param {String} propertyName
4715	   * @param {*} value
4716	   *
4717	   * @since 1.0
4718	   */
4719	  setProperty(propertyName, value) {
4720		if (this.preventUpdate) {
4721		  return;
4722		}
4723		let newValue = this.validateProperty(propertyName, value);
4724
4725		// Do nothing if nothing changed
4726		if (newValue === this[propertyName]) {
4727		  return;
4728		}
4729
4730		// Is it a change of the dependent property?
4731		let isChain = this.preventReact;
4732
4733		// Block excess reacts and update linked values
4734		this.preventReact = true;
4735		this.updateProperty(propertyName, newValue);
4736
4737		// Run react() after all changes (once)
4738		if (!isChain) {
4739		  if (this.isActive) {
4740			this.react();
4741		  }
4742		  this.preventReact = false;
4743		}
4744	  }
4745
4746	  /**
4747	   * @param {String} propertyName
4748	   *
4749	   * @since 1.0
4750	   */
4751	  resetProperty(propertyName) {
4752		this.setProperty(propertyName);
4753	  }
4754
4755	  /**
4756	   * @param {String} propertyName
4757	   * @param {*} value
4758	   * @return {*}
4759	   *
4760	   * @access protected
4761	   *
4762	   * @since 1.0
4763	   */
4764	  validateProperty(propertyName, value) {
4765		let validValue = value;
4766		if (propertyName in this.schema) {
4767		  let propertySchema = this.schema[propertyName];
4768		  if (value == null) {
4769			validValue = propertySchema.default;
4770		  } else {
4771			switch (propertySchema.type) {
4772			  case 'bool':
4773				validValue = mpa_boolval(value);
4774				break;
4775			  case 'integer':
4776				validValue = mpa_intval(value);
4777				break;
4778			}
4779
4780			// Check allowed options (but don't check empty values, like 0
4781			// or "" - they are kind of allowed (to filter dependent fields))
4782			if (!mpa_empty(validValue) && propertySchema.options != undefined) {
4783			  let isAllowed = propertySchema.options.indexOf(validValue) >= 0;
4784
4785			  // If new value is not allowed, then don't change the value
4786			  if (!isAllowed) {
4787				validValue = this[propertyName];
4788			  }
4789			}
4790		  } // If value not null
4791		} else if (value == undefined) {
4792		  validValue = null;
4793		}
4794		return validValue;
4795	  }
4796
4797	  /**
4798	   * Executed only when the new value is different from the old one.
4799	   *
4800	   * @param {String} propertyName
4801	   * @param {*} newValue
4802	   *
4803	   * @access protected
4804	   *
4805	   * @since 1.0
4806	   */
4807	  updateProperty(propertyName, newValue) {
4808		let oldValue = this[propertyName];
4809		this[propertyName] = newValue;
4810		this.afterUpdate(propertyName, newValue, oldValue);
4811	  }
4812
4813	  /**
4814	   * Executed only when the new value is different from the old one.
4815	   *
4816	   * @param {String} propertyName
4817	   * @param {*} newValue
4818	   * @param {*} oldValue
4819	   *
4820	   * @access protected
4821	   *
4822	   * @since 1.0
4823	   * @since 1.5.0 added the <code>oldValue</code> argument.
4824	   */
4825	  afterUpdate(propertyName, newValue, oldValue) {}
4826
4827	  /**
4828	   * @access protected
4829	   *
4830	   * @since 1.0
4831	   */
4832	  react() {
4833		let inputsValid = this.isValidInput();
4834		this.$buttonNext.prop('disabled', !inputsValid);
4835		if (this.hideButtons) {
4836		  this.$buttons.toggleClass('mpa-hide', !inputsValid);
4837		}
4838	  }
4839
4840	  /**
4841	   * @since 1.0
4842	   */
4843	  show() {
4844		// Set active
4845		this.enable();
4846
4847		// Update elements due to the state
4848		this.react();
4849
4850		// Show element
4851		this.$element.removeClass('mpa-hide');
4852
4853		// Hide loading GIF
4854		this.readyPromise.finally(() => this.showReady());
4855	  }
4856
4857	  /**
4858	   * @access protected
4859	   *
4860	   * @since 1.0
4861	   */
4862	  showReady() {
4863		// Hide loading GIF
4864		this.$element.addClass('mpa-loaded');
4865
4866		// Show buttons
4867		if (!this.hideButtons) {
4868		  this.$buttons.removeClass('mpa-hide');
4869		}
4870	  }
4871
4872	  /**
4873	   * @since 1.0
4874	   */
4875	  hide() {
4876		// Set inactive
4877		this.disable();
4878
4879		// Hide element
4880		this.$element.addClass('mpa-hide');
4881	  }
4882
4883	  /**
4884	   * @access protected
4885	   *
4886	   * @since 1.0
4887	   */
4888	  enable() {
4889		this.isActive = true;
4890		this.$buttonBack.prop('disabled', false);
4891		this.$buttonNext.prop('disabled', false);
4892	  }
4893
4894	  /**
4895	   * @access protected
4896	   *
4897	   * @since 1.0
4898	   */
4899	  disable() {
4900		this.isActive = false;
4901		this.$buttonBack.prop('disabled', true);
4902		this.$buttonNext.prop('disabled', true);
4903	  }
4904
4905	  /**
4906	   * @since 1.0
4907	   */
4908	  cancel(event) {
4909		if (typeof event !== 'undefined') {
4910		  event.stopPropagation();
4911		}
4912		if (!this.isActive) {
4913		  return;
4914		}
4915
4916		// Stop editing
4917		this.disable();
4918
4919		// Trigger as cancelled
4920		this.triggerBack();
4921	  }
4922
4923	  /**
4924	   * @since 1.0
4925	   */
4926	  submit(event) {
4927		if (typeof event !== 'undefined') {
4928		  event.stopPropagation();
4929		}
4930		if (!this.isActive || !this.isValidInput()) {
4931		  return;
4932		}
4933
4934		// Stop editing
4935		this.disable();
4936
4937		// Submit data
4938		let submitted = this.maybeSubmit();
4939		if (submitted == undefined) {
4940		  // No response - take it as a success
4941		  this.triggerNext();
4942		} else if (typeof submitted !== 'object') {
4943		  // If not a Promise
4944		  // Succeded or failed
4945		  submitted ? this.triggerNext() : this.cancelSubmission();
4946		} else {
4947		  // Wait for a Promise
4948		  submitted.then(
4949		  // executes if submitted will be resolved
4950		  this.triggerNext.bind(this),
4951		  // executes if submitted will be rejected
4952		  this.cancelSubmission.bind(this));
4953		}
4954	  }
4955
4956	  /**
4957	   * @since 1.0
4958	   * @since 1.5.0 may return <code>Promise</code>.
4959	   *
4960	   * @access protected
4961	   *
4962	   * @return {*}
4963	   */
4964	  maybeSubmit() {}
4965
4966	  /**
4967	   * @since 1.5.0
4968	   *
4969	   * @access protected
4970	   */
4971	  cancelSubmission() {
4972		this.enable();
4973		this.react();
4974	  }
4975
4976	  /**
4977	   * @access protected
4978	   *
4979	   * @since 1.0
4980	   */
4981	  triggerBack() {
4982		this.$element.trigger('mpa_booking_step_back', {
4983		  step: this.stepId
4984		});
4985	  }
4986
4987	  /**
4988	   * @access protected
4989	   *
4990	   * @since 1.0
4991	   */
4992	  triggerNext() {
4993		this.$element.trigger('mpa_booking_step_next', {
4994		  step: this.stepId
4995		});
4996	  }
4997
4998	  /**
4999	   * @since 1.19.0
5000	   */
5001	  hideButtonBack() {
5002		this.$buttonBack.prop('disabled', true);
5003		this.$buttonBack.toggleClass('mpa-hide', true);
5004	  }
5005	}
5006
5007	/**
5008	 * @since 1.0
5009	 */
5010	class StepBooking extends AbstractStep {
5011	  /**
5012	   * @access protected
5013	   *
5014	   * @since 1.0
5015	   */
5016	  setupProperties() {
5017		super.setupProperties();
5018		this.hideButtons = true;
5019		this.isPosted = false;
5020		this.isBooked = false;
5021		this.$message = this.$element.find('.mpa-message').first();
5022		this.$buttonReset = this.$buttons.find('.mpa-button-reset');
5023	  }
5024
5025	  /**
5026	   * @return {Promise}
5027	   * @access protected
5028	   *
5029	   * @since 1.0
5030	   */
5031	  reload() {
5032		this.isPosted = false;
5033		this.isBooked = false;
5034		this.setMessage(__('Making a reservation...', 'motopress-appointment') + ' <span class="mpa-preloader"></span>');
5035		return Promise.resolve(this);
5036	  }
5037
5038	  /**
5039	   * @since 1.6.2
5040	   * @access protected
5041	   */
5042	  addListeners() {
5043		super.addListeners();
5044		this.$buttonReset.on('click', this.resetForm.bind(this));
5045	  }
5046
5047	  /**
5048	   * @return {String}
5049	   *
5050	   * @access protected
5051	   *
5052	   * @since 1.0
5053	   */
5054	  theId() {
5055		return 'booking';
5056	  }
5057
5058	  /**
5059	   * @since 1.6.2
5060	   * @access protected
5061	   */
5062	  react() {
5063		if (!this.isPosted) {
5064		  return;
5065		}
5066		this.$buttons.removeClass('mpa-hide');
5067		this.$buttonBack.toggleClass('mpa-hide', this.isBooked);
5068		this.$buttonReset.toggleClass('mpa-hide', !this.isBooked || this.isRedirectNeeded());
5069	  }
5070
5071	  /**
5072	   * @since 1.0
5073	   */
5074	  show() {
5075		super.show();
5076
5077		// Book services
5078		mpa_rest_post('/bookings', this.cart.toArray()).then(response => {
5079		  this.isPosted = this.isBooked = true;
5080		  this.setMessage(response.message);
5081		  if (this.isRedirectNeeded()) {
5082			this.redirectPayment();
5083		  } else {
5084			this.react();
5085		  }
5086		}, error => {
5087		  this.isPosted = true;
5088		  this.setMessage(error.message);
5089		  this.react();
5090		});
5091	  }
5092
5093	  /**
5094	   * @since 1.6.2
5095	   * @access protected
5096	   */
5097	  showReady() {
5098		super.showReady();
5099		this.$buttonBack.addClass('mpa-hide');
5100		this.$buttonReset.addClass('mpa-hide');
5101	  }
5102
5103	  /**
5104	   * @param {String} message
5105	   *
5106	   * @since 1.0
5107	   */
5108	  setMessage(message) {
5109		this.$message.html(message);
5110	  }
5111
5112	  /**
5113	   * Redirect to complete the payment.
5114	   *
5115	   * @since 1.6.2
5116	   */
5117	  redirectPayment() {
5118		let paymentDetails = this.cart.getPaymentDetails();
5119		window.location.href = paymentDetails.redirect_url;
5120	  }
5121
5122	  /**
5123	   * @since 1.6.2
5124	   * @access protected
5125	   */
5126	  isRedirectNeeded() {
5127		let paymentDetails = this.cart.getPaymentDetails();
5128		return 'redirect_url' in paymentDetails && paymentDetails.redirect_url != '';
5129	  }
5130
5131	  /**
5132	   * @since 1.6.2
5133	   * @access protected
5134	   */
5135	  resetForm(event) {
5136		event.preventDefault();
5137		if (this.isPosted && this.isBooked) {
5138		  this.$element.trigger('mpa_reset_booking');
5139		}
5140	  }
5141	}
5142
5143	/**
5144	 * @param {Object} atts
5145	 * @return {String}
5146	 *
5147	 * @since 1.0
5148	 */
5149	function mpa_tmpl_atts(atts) {
5150	  let output = '';
5151	  for (let name in atts) {
5152		output += ' ' + name + '="' + atts[name] + '"';
5153	  }
5154	  return output;
5155	}
5156
5157	/**
5158	 * @param {String} label
5159	 * @param {Object} atts
5160	 * @return {String}
5161	 *
5162	 * @since 1.0
5163	 */
5164	function mpa_tmpl_button(label, atts = {}) {
5165	  atts = jQuery.extend({}, {
5166		type: 'button',
5167		'class': 'button'
5168	  }, atts);
5169	  return '<button' + mpa_tmpl_atts(atts) + '>' + label + '</button>';
5170	}
5171
5172	/**
5173	 * @since 1.0.0
5174	 * @since 1.4.0 builds output only by template. Added the <code>$template</code> argument.
5175	 *
5176	 * @param {CartItem} cartItem
5177	 * @param {Object} $template
5178	 * @return {String}
5179	 */
5180	function mpa_tmpl_cart_item(cartItem, $template) {
5181	  let selectors = {
5182		// Template tag:			jQuery selector
5183		service_id: '.mpa-service-id',
5184		service_name: '.mpa-service-name',
5185		service_thumbnail: '.mpa-service-thumbnail',
5186		employee_id: '.mpa-employee-id',
5187		employee_name: '.mpa-employee-name',
5188		location_id: '.mpa-location-id',
5189		location_name: '.mpa-location-name',
5190		reservation_date: '.mpa-reservation-date',
5191		// Public date string
5192		reservation_save_date: '.mpa-reservation-save-date',
5193		// Internal date string
5194
5195		reservation_time: '.mpa-reservation-time',
5196		// Public start time string
5197		reservation_period: '.mpa-reservation-period',
5198		// Public period time string
5199		reservation_save_period: '.mpa-reservation-save-period',
5200		// Internal period time string
5201
5202		reservation_capacity: '.mpa-reservation-capacity',
5203		// Wrapper for <select> with capacity options
5204		reservation_clients: '.mpa-reservation-clients',
5205		// Only the capacity <option>'s
5206		reservation_clients_count: '.mpa-reservation-clients-count',
5207		// Capacity number: "1", "2" etc.
5208
5209		reservation_price: '.mpa-reservation-price'
5210	  };
5211
5212	  // Generate new item
5213	  let $cartItem = $template.clone();
5214
5215	  // Set cart ID
5216	  $cartItem.attr('data-id', cartItem.getItemId());
5217
5218	  // Replace all tags
5219	  for (let tagName in selectors) {
5220		let selector = selectors[tagName];
5221		let $element = $cartItem.find(selector).first();
5222		let elementHtml = $element.length > 0 ? $element.html() : '';
5223		let tag = '{' + tagName + '}';
5224		if (!elementHtml.includes(tag)) {
5225		  continue;
5226		}
5227		let content = '';
5228		switch (tagName) {
5229		  case 'service_id':
5230			content = cartItem.service.id;
5231			break;
5232		  case 'service_name':
5233			content = cartItem.service.name;
5234			break;
5235		  case 'service_thumbnail':
5236			content = mpa_tmpl_thumbnail_image(cartItem.service.thumbnail);
5237			break;
5238		  case 'employee_id':
5239			content = cartItem.employee.id;
5240			break;
5241		  case 'employee_name':
5242			content = cartItem.employee.name;
5243			break;
5244		  case 'location_id':
5245			content = cartItem.location.id;
5246			break;
5247		  case 'location_name':
5248			content = cartItem.location.name;
5249			break;
5250		  case 'reservation_date':
5251			content = mpa_format_date(cartItem.date);
5252			break;
5253		  case 'reservation_save_date':
5254			content = mpa_format_date(cartItem.date, 'internal');
5255			break;
5256		  case 'reservation_time':
5257			content = mpa_format_time(cartItem.time.startTime);
5258			break;
5259		  case 'reservation_period':
5260			content = cartItem.time.toString();
5261			break;
5262		  case 'reservation_save_period':
5263			content = cartItem.time.toString('internal');
5264			break;
5265		  case 'reservation_capacity':
5266			let capacityRange = cartItem.service.getCapacityRange(cartItem.employee.id);
5267			let capacityVariants = mpa_array_combine(capacityRange, capacityRange);
5268			content = mpa_tmpl_select(capacityVariants, cartItem.capacity);
5269			break;
5270		  case 'reservation_clients':
5271			let capacityLimits = cartItem.service.getCapacityRange(cartItem.employee.id);
5272			let capacityClients = mpa_array_combine(capacityLimits, capacityLimits);
5273			content = mpa_tmpl_select_options(capacityClients, cartItem.capacity);
5274			break;
5275		  case 'reservation_clients_count':
5276			content = cartItem.capacity;
5277			break;
5278		  case 'reservation_price':
5279			let employeeId = cartItem.employee.id;
5280			let price = cartItem.service.getPrice(employeeId, cartItem.capacity);
5281			content = mpa_tmpl_price(price);
5282			break;
5283		}
5284		$element.html($element.html().replace(tag, content));
5285	  } // For each tag
5286
5287	  // Replace {item_id} with the actual ID
5288	  $cartItem.find('[name*="{item_id}"]').each((i, element) => {
5289		element.name = element.name.replace('{item_id}', cartItem.getItemId());
5290	  });
5291
5292	  // hide capacity if we have only one possible client
5293	  if (1 === cartItem.service.getCapacityRange().length) {
5294		$cartItem.find('.cell-people').addClass('mpa-hide');
5295	  }
5296	  return $cartItem;
5297	}
5298
5299	/**
5300	 * @since 1.4.0
5301	 * @see Cart.getOrder()
5302	 *
5303	 * @param {Object} order
5304	 * @return {String}
5305	 */
5306	function mpa_tmpl_order(order) {
5307	  let output = '';
5308	  output += '<table class="mpa-order widefat">';
5309	  output += '<tbody>';
5310	  for (let product of order.products) {
5311		output += '<tr class="mpa-order-service">';
5312		output += '<td class="column-service">' + product.name + '</td>';
5313		output += '<td class="column-price">' + mpa_tmpl_price_number(product.price) + '</td>';
5314		output += '</tr>';
5315	  }
5316
5317	  // Show subtotal price
5318	  output += '<tr class="mpa-order-subtotal">';
5319	  output += '<th class="column-subtotal">
5319' + __('Subtotal', 'motopress-appointment') + '</th>';
5320	  output += '<th class="column-price">' + mpa_tmpl_price_number(order.subtotal) + '</th>';
5321	  output += '</tr>';
5322	  output += '</tbody>';
5323	  output += '<tfoot>';
5324	  if (order.coupon) {
5325		output += '<tr class="mpa-order-coupon">';
5326		output += '<th class="column-coupon">';
5327		output +=
5328		// Translators: %s: Coupon code.
5329		__('Coupon: %s', 'motopress-appointment').replace('%s', order.coupon.code);
5330		output += '</th>';
5331		output += '<td class="column-price">';
5332		output += mpa_tmpl_price_number(-order.coupon.amount);
5333		output += ' ';
5334		output += '<a href="#" class="mpa-remove-coupon">' + __('Remove', 'motopress-appointment') + '</a>';
5335		output += '</td>';
5336		output += '</tr>';
5337	  }
5338
5339	  // Shot total price
5340	  output += '<tr class="mpa-order-total">';
5341	  output += '<th class="column-total">' + __('Total', 'motopress-appointment') + '</th>';
5342	  output += '<th class="column-price">' + mpa_tmpl_price_number(order.total) + '</th>';
5343	  output += '</tr>';
5344	  output += '</tfoot>';
5345	  output += '</table>';
5346	  return output;
5347	}
5348
5349	/**
5350	 * @since 1.14.0
5351	 * @see Cart.getOrder()
5352	 *
5353	 * @param {Object} order
5354	 * @return {String}
5355	 */
5356	function mpa_tmpl_deposit(order) {
5357	  const leftToPay = parseFloat(order.total) - parseFloat(order.deposit);
5358	  let output = '';
5359	  if (leftToPay > 0) {
5360		output += '<table class="widefat">';
5361		output += '<tbody>';
5362		output += '<tr class="mpa-deposit-title">';
5363		output += '<td class="column-title" colspan="2">';
5364		output += __('Deposit', 'motopress-appointment');
5365		output += '</td>';
5366		output += '</tr>';
5367		output += '<tr class="mpa-deposit-now">';
5368		output += '<th class="column-title">';
5369		output += __('Paying now', 'motopress-appointment');
5370		output += '</th>';
5371		output += '<th class="column-price">';
5372		output += mpa_tmpl_price_number(order.deposit);
5373		output += '</th>';
5374		output += '</tr>';
5375		output += '<tr class="mpa-deposit-left">';
5376		output += '<th class="column-title">';
5377		output += __('Left to pay', 'motopress-appointment');
5378		output += '</th>';
5379		output += '<th class="column-price">';
5380		output += mpa_tmpl_price_number(leftToPay);
5381		output += '</th>';
5382		output += '</tr>';
5383		output += '</tbody>';
5384		output += '</table>';
5385	  }
5386	  return output;
5387	}
5388
5389	/**
5390	 * @param {Number} price
5391	 * @param {Object} args
5392	 * @return {String}
5393	 *
5394	 * @since 1.0
5395	 */
5396	function mpa_tmpl_price(price, args = {}) {
5397	  let settings = mpapp().settings();
5398	  args = jQuery.extend({
5399		currency_symbol: settings.getCurrencySymbol(),
5400		currency_position: settings.getCurrencyPosition(),
5401		decimal_separator: settings.getDecimalSeparator(),
5402		thousand_separator: settings.getThousandSeparator(),
5403		decimals: settings.getDecimalsCount(),
5404		literal_free: true,
5405		trim_zeros: true
5406	  }, args);
5407	  let priceString = mpa_number_format(Math.abs(price), args.decimals, args.decimal_separator, args.thousand_separator);
5408	  let classes = 'mpa-price';
5409	  if (price == 0) {
5410		classes += ' mpa-zero-price';
5411	  }
5412	  if (price == 0 && args.literal_free) {
5413		// Use text 'Free' as a price string
5414		classes += ' mpa-price-free';
5415		priceString = _x('Free', 'Zero price', 'motopress-appointment');
5416	  } else {
5417		// Trim zeros
5418		if (args.trim_zeros) {
5419		  priceString = mpa_trim_price(priceString);
5420		}
5421
5422		// Add currency to the price
5423		let currencySpan = '<span class="mpa-currency">' + args.currency_symbol + '</span>';
5424		switch (args.currency_position) {
5425		  case 'before':
5426			priceString = currencySpan + priceString;
5427			break;
5428		  case 'after':
5429			priceString = priceString + currencySpan;
5430			break;
5431		  case 'before_with_space':
5432			priceString = currencySpan + '&nbsp;' + priceString;
5433			break;
5434		  case 'after_with_space':
5435			priceString = priceString + '&nbsp;' + currencySpan;
5436			break;
5437		}
5438
5439		// Add sign
5440		if (price < 0) {
5441		  priceString = '-' + priceString;
5442		}
5443	  }
5444	  let priceHtml = '<span class="' + classes + '">' + priceString + '</span>';
5445	  return priceHtml;
5446	}
5447
5448	/**
5449	 * @since 1.4.0
5450	 *
5451	 * @param {Number} price
5452	 * @param {Object} args Optional. See mpa_tmpl_price() for details.
5453	 * @return {String}
5454	 */
5455	function mpa_tmpl_price_number(price, args = {}) {
5456	  // Force number for all results
5457	  args.literal_free = false;
5458	  return mpa_tmpl_price(price, args);
5459	}
5460
5461	/**
5462	 * @param {Object} options
5463	 * @param {*} selected
5464	 * @param {Object} atts Optional. {} by default.
5465	 * @return {String}
5466	 *
5467	 * @since 1.0
5468	 */
5469	function mpa_tmpl_select(options, selected, atts = {}) {
5470	  let output = '<select' + mpa_tmpl_atts(atts) + '>';
5471	  output += mpa_tmpl_select_options(options, selected);
5472	  output += '</select>';
5473	  return output;
5474	}
5475
5476	/**
5477	 * @param {String|Number} value
5478	 * @param {String} label
5479	 * @param {Boolean} isSelected
5480	 * @return {String}
5481	 *
5482	 * @since 1.0
5483	 */
5484	function mpa_tmpl_select_option(value, label, isSelected = false) {
5485	  let output = '';
5486	  let selectedAttr = isSelected ? ' selected="selected"' : '';
5487	  output = '<option value="' + value + '"' + selectedAttr + '>';
5488	  output += label;
5489	  output += '</option>';
5490	  return output;
5491	}
5492
5493	/**
5494	 * @param {Object} options
5495	 * @param {*} selected
5496	 * @return {String}
5497	 *
5498	 * @since 1.0
5499	 */
5500	function mpa_tmpl_select_options(options, selected) {
5501	  let output = '';
5502	  for (let value in options) {
5503		output += mpa_tmpl_select_option(value, options[value], value == selected);
5504	  }
5505	  return output;
5506	}
5507
5508	/**
5509	 * @param {Object} $select jQuery element.
5510	 * @param {Object} emptyOptions '— Select —'/'— Any —' value.
5511	 * @param {Object} allowedOptions All other values.
5512	 * @param {*} selected Selected option value.
5513	 *
5514	 * @since 1.19.0
5515	 */
5516	function update_select_options($select, emptyOptions, allowedOptions, selected) {
5517	  let emptyOptionsHtml = mpa_tmpl_select_options(emptyOptions, selected);
5518	  let allowedOptionsHtml = mpa_tmpl_select_options(allowedOptions, selected);
5519	  let optionsHtml = emptyOptionsHtml + allowedOptionsHtml;
5520	  $select.empty().append(optionsHtml).val(selected);
5521	}
5522
5523	/**
5524	 * @since 1.4.0
5525	 *
5526	 * @param {String} imageUrl
5527	 * @return {String}
5528	 */
5529	function mpa_tmpl_thumbnail_image(imageUrl) {
5530	  let {
5531		width,
5532		height
5533	  } = mpapp().settings().getThumbnailSize();
5534	  let atts = {
5535		width,
5536		height,
5537		src: imageUrl,
5538		'class': 'attachment-thumbnail size-thumbnail'
5539	  };
5540	  return '<img' + mpa_tmpl_atts(atts) + '>';
5541	}
5542
5543	/**
5544	 * @since 1.4.0
5545	 */
5546	class StepCart extends AbstractStep {
5547	  /**
5548	   * @since 1.4.0
5549	   * @access protected
5550	   */
5551	  setupProperties() {
5552		super.setupProperties();
5553		this.isBeginCheckoutEventSent = false;
5554		this.$cart = this.$element.find('.mpa-cart');
5555		this.$items = this.$cart.find('.mpa-cart-items');
5556		this.$itemTemplate = this.$cart.find('.mpa-cart-item-template');
5557		this.$noItems = this.$element.find('.no-items'); // May be out of the .mpa-cart
5558
5559		this.$totalPrice = this.$element.find('.mpa-cart-total-price');
5560		this.$buttonNew = this.$buttons.find('.mpa-button-new');
5561	  }
5562
5563	  /**
5564	   * @since 1.4.0
5565	   * @access protected
5566	   *
5567	   * @return {String}
5568	   */
5569	  theId() {
5570		return 'cart';
5571	  }
5572
5573	  /**
5574	   * @since 1.4.0
5575	   * @access protected
5576	   */
5577	  addListeners() {
5578		super.addListeners();
5579		this.$buttonNew.on('click', this.createNew.bind(this));
5580	  }
5581
5582	  /**
5583	   * @since 1.4.0
5584	   */
5585	  load() {
5586		// Remove template from the cart
5587		this.$itemTemplate.remove();
5588		this.$itemTemplate.removeClass('mpa-cart-item-template');
5589		this.updateActiveItemCapacity();
5590		this.refreshCart();
5591		this.isLoaded = true;
5592		this.readyPromise = Promise.resolve(this);
5593	  }
5594
5595	  /**
5596	   * @since 1.6.2
5597	   */
5598	  reset() {
5599		this.$items.find('.mpa-cart-item').remove();
5600		this.$noItems.removeClass('mpa-hide');
5601		this.isBeginCheckoutEventSent = false;
5602	  }
5603
5604	  /**
5605	   * @since 1.4.0
5606	   * @access protected
5607	   */
5608	  updateActiveItemCapacity() {
5609		let cartItem = this.cart.getActiveItem();
5610		if (!cartItem) {
5611		  return;
5612		}
5613		let service = cartItem.service;
5614		let employeeId = cartItem.employee.id;
5615		let minCapacity = service.getMinCapacity(employeeId);
5616		let maxCapacity = service.getMaxCapacity(employeeId);
5617
5618		// Validate capacity to the limits of the new employee
5619		cartItem.capacity = mpa_limit(cartItem.capacity, minCapacity, maxCapacity);
5620	  }
5621
5622	  /**
5623	   * @since 1.4.0
5624	   * @access protected
5625	   */
5626	  refreshCart() {
5627		let activeCartItemId = this.cart.getActiveItemId();
5628
5629		// Add new items
5630		this.cart.items.forEach((cartItem, _, cartItemId) => {
5631		  let itemSelector = '.mpa-cart-item[data-id="' + cartItemId + '"]';
5632		  let $cartItem = this.$items.find(itemSelector);
5633		  if ($cartItem.length === 0) {
5634			// Add new item
5635			$cartItem = this.addItem(cartItem);
5636			this.bindListeners($cartItem);
5637		  } else if (cartItemId === activeCartItemId) {
5638			// Update active item (only)
5639			$cartItem = this.updateItem($cartItem, cartItem);
5640			this.bindListeners($cartItem);
5641		  }
5642		});
5643
5644		// Update total price
5645		this.updateTotalPrice();
5646	  }
5647
5648	  /**
5649	   * @since 1.4.0
5650	   * @access protected
5651	   *
5652	   * @param {CartItem} cartItem
5653	   * @return {Object} New cart item element (jQuery object).
5654	   */
5655	  addItem(cartItem) {
5656		let $cartItem = mpa_tmpl_cart_item(cartItem, this.$itemTemplate);
5657		this.$items.append($cartItem);
5658		this.$noItems.addClass('mpa-hide');
5659		return $cartItem;
5660	  }
5661
5662	  /**
5663	   * @since 1.4.0
5664	   * @access protected
5665	   *
5666	   * @param {Object} $foundItem Existing item (jQuery element).
5667	   * @param {CartItem} cartItem
5668	   * @return {Object} New cart item element (jQuery object).
5669	   */
5670	  updateItem($cartItem, cartItem) {
5671		let $newCartItem = mpa_tmpl_cart_item(cartItem, this.$itemTemplate);
5672		$cartItem.replaceWith($newCartItem);
5673		return $newCartItem;
5674	  }
5675
5676	  /**
5677	   * @since 1.4.0
5678	   *
5679	   * @param {Object} $item
5680	   */
5681	  bindListeners($item) {
5682		let cartItemId = $item.data('id');
5683		let cartItem = this.cart.getItem(cartItemId);
5684		let $people = $item.find('.mpa-reservation-capacity select, .mpa-reservation-clients select');
5685		let $price = $item.find('.mpa-reservation-price');
5686		let $removeButton = $item.find('.mpa-button-remove, .mpa-button-edit-or-remove');
5687		let $updateButton = $item.find('.mpa-button-edit, .mpa-button-edit-or-remove');
5688
5689		// Change cart item capacity and prices when amount of people changes
5690		$people.on('change', event => {
5691		  let newCapacity = mpa_intval(event.target.value);
5692		  let employeeId = cartItem.employee.id;
5693
5694		  // Update item capacity
5695		  cartItem.capacity = newCapacity;
5696
5697		  // Update item price
5698		  let itemPrice = cartItem.service.getPrice(employeeId, newCapacity);
5699		  $price.html(mpa_tmpl_price(itemPrice));
5700
5701		  // Update total price
5702		  this.updateTotalPrice();
5703		});
5704
5705		// Add "Remove" listener
5706		if (this.isMultibookingEnabled()) {
5707		  $removeButton.on('click', event => {
5708			event.stopPropagation();
5709
5710			// Remove item element
5711			$item.remove();
5712
5713			// Remove item from the cart
5714			let removedCartItem = this.cart.getItem(cartItemId);
5715			this.cart.removeItem(cartItemId);
5716			if (this.cart.isEmpty()) {
5717			  this.$noItems.removeClass('mpa-hide');
5718			}
5719
5720			// React to changes
5721			this.updateTotalPrice();
5722			this.react();
5723			document.dispatchEvent(new CustomEvent('mpa_remove_from_cart', {
5724			  detail: {
5725				cartItem: removedCartItem,
5726				currencyCode: mpapp().settings().getCurrency()
5727			  }
5728			}));
5729		  });
5730		}
5731
5732		// Add "Update" listener
5733		if (!this.isMultibookingEnabled()) {
5734		  $updateButton.on('click', () => {
5735			this.cart.setActiveItem(cartItemId);
5736
5737			// Go back
5738			this.cancel();
5739		  });
5740		}
5741	  }
5742
5743	  /**
5744	   * @since 1.4.0
5745	   * @access protected
5746	   */
5747	  updateTotalPrice() {
5748		this.$totalPrice.html(mpa_tmpl_price_number(this.cart.getTotalPrice()));
5749	  }
5750	  isMultibookingEnabled() {
5751		return mpapp().settings().isMultibookingEnabled();
5752	  }
5753
5754	  /**
5755	   * @since 1.4.0
5756	   *
5757	   * @return {Boolean}
5758	   */
5759	  isValidInput() {
5760		return !this.cart.isEmpty();
5761	  }
5762
5763	  /**
5764	   * @since 1.4.0
5765	   * @access protected
5766	   */
5767	  createNew() {
5768		if (!this.isActive) {
5769		  return;
5770		}
5771
5772		// Stop editing
5773		this.disable();
5774
5775		// Trigger as cancelled
5776		this.triggerNew();
5777	  }
5778
5779	  /**
5780	   * @since 1.4.0
5781	   * @access protected
5782	   */
5783	  triggerNew() {
5784		this.$element.trigger('mpa_booking_step_new', {
5785		  step: this.stepId
5786		});
5787	  }
5788
5789	  /**
5790	   * @access protected
5791	   */
5792	  maybeSubmit() {
5793		if (!this.isBeginCheckoutEventSent) {
5794		  document.dispatchEvent(new CustomEvent('mpa_begin_checkout', {
5795			detail: {
5796			  cart: this.cart,
5797			  currencyCode: mpapp().settings().getCurrency()
5798			}
5799		  }));
5800		  this.isBeginCheckoutEventSent = true;
5801		}
5802	  }
5803	}
5804
5805	const KEY_ENTER = 'Enter';
5806
5807	/**
5808	 * @since 1.11.0
5809	 */
5810	class CouponSection {
5811	  /**
5812	   * @param {jQuery} $element
5813	   * @param {Cart} cart
5814	   */
5815	  constructor($element, cart) {
5816		this.cart = cart;
5817		this.$element = $element;
5818		this.$couponCode = $element.find('[name="coupon_code"]');
5819		this.$applyButton = $element.find('.mpa-apply-coupon-button');
5820		this.$messageHolder = $element.find('.mpa-message-wrapper');
5821		this.$preloader = $element.find('.mpa-preloader');
5822		this.$parentForm = $element.parents('.mpa-booking-step').first();
5823		this.addListeners();
5824	  }
5825
5826	  /**
5827	   * @access protected
5828	   */
5829	  addListeners() {
5830		this.$couponCode.on('keydown', event => {
5831		  if (event.code === KEY_ENTER) {
5832			this.onEnter(event);
5833		  }
5834		});
5835		this.$applyButton.on('click', this.onSubmit.bind(this));
5836	  }
5837
5838	  /**
5839	   * Triggers when user types [Enter] in the input field.
5840	   *
5841	   * @access protected
5842	   *
5843	   * @param {Event} event
5844	   */
5845	  onEnter(event) {
5846		event.preventDefault();
5847		event.stopPropagation();
5848		this.applyCouponCode(event.target.value);
5849	  }
5850
5851	  /**
5852	   * Triggers when user clicks "Apply" button.
5853	   *
5854	   * @access protected
5855	   *
5856	   * @param {Event} event
5857	   */
5858	  onSubmit(event) {
5859		event.preventDefault();
5860		event.stopPropagation();
5861		this.applyCouponCode(this.$couponCode.val());
5862	  }
5863
5864	  /**
5865	   * @param {String} couponCode
5866	   */
5867	  applyCouponCode(couponCode) {
5868		this.clearMessage();
5869		if (!couponCode) {
5870		  this.setMessage(__('Coupon code is empty.', 'motopress-appointment'));
5871		  return;
5872		}
5873		this.pauseAll();
5874		mpa_repositories().coupon().findByCode(couponCode).then(coupon => {
5875		  if (coupon.isApplicableForCart(this.cart)) {
5876			this.cart.setCoupon(coupon);
5877			this.reset();
5878			this.triggerApplied(coupon);
5879			this.setMessage(__('Coupon code applied successfully.', 'motopress-appointment'));
5880		  } else {
5881			this.setMessage(__('Sorry, your booking is not eligible for this coupon.', 'motopress-appointment'));
5882		  }
5883		  this.unpauseAll();
5884		}, error => {
5885		  this.setMessage(error.message);
5886		  this.unpauseAll();
5887		});
5888	  }
5889	  reset() {
5890		this.$couponCode.val('');
5891		this.clearMessage();
5892		this.enable();
5893	  }
5894	  disable() {
5895		this.$couponCode.prop('disabled', true);
5896		this.$applyButton.prop('disabled', true);
5897	  }
5898	  enable() {
5899		this.$couponCode.prop('disabled', false);
5900		this.$applyButton.prop('disabled', false);
5901	  }
5902
5903	  /**
5904	   * @access protected
5905	   */
5906	  pauseAll() {
5907		this.disable();
5908		this.showPreloader();
5909		this.$parentForm.trigger('mpa_booking_step_disable');
5910	  }
5911
5912	  /**
5913	   * @access protected
5914	   */
5915	  unpauseAll() {
5916		this.enable();
5917		this.hidePreloader();
5918		this.$parentForm.trigger('mpa_booking_step_enable');
5919	  }
5920
5921	  /**
5922	   * @access protected
5923	   *
5924	   * @param {Coupon} coupon
5925	   */
5926	  triggerApplied(coupon) {
5927		this.$parentForm.trigger('mpa_booking_coupon_applied', {
5928		  coupon
5929		});
5930	  }
5931
5932	  /**
5933	   * @param {String} message
5934	   */
5935	  setMessage(message) {
5936		this.$messageHolder.html(message).removeClass('mpa-hide');
5937	  }
5938	  clearMessage() {
5939		this.$messageHolder.html('').addClass('mpa-hide');
5940	  }
5941
5942	  /**
5943	   * @access protected
5944	   */
5945	  showPreloader() {
5946		this.$preloader.removeClass('mpa-hide');
5947	  }
5948
5949	  /**
5950	   * @access protected
5951	   */
5952	  hidePreloader() {
5953		this.$preloader.addClass('mpa-hide');
5954	  }
5955	}
5956
5957	/**
5958	 * @since 1.0
5959	 */
5960	class StepCheckout extends AbstractStep {
5961	  /**
5962	   * @access protected
5963	   *
5964	   * @since 1.0
5965	   */
5966	  setupProperties() {
5967		super.setupProperties();
5968		this.name = '';
5969		this.email = '';
5970		this.phone = '';
5971		this.notes = '';
5972		this.acceptTerms = false;
5973		this.createAccount = false;
5974		this.$checkoutForm = this.$element.find('.mpa-checkout-form');
5975		this.$name = this.$element.find('.mpa-customer-name');
5976		this.$email = this.$element.find('.mpa-customer-email');
5977		this.$phone = this.$element.find('.mpa-customer-phone');
5978		this.$notes = this.$element.find('.mpa-customer-notes');
5979		this.$order = this.$element.find('.mpa-order');
5980		if (mpapp().settings().getTermsPageIdForAcceptance()) {
5981		  this.$acceptTerms = this.$element.find('.mpa-accept-terms');
5982		}
5983		this.$messageHolder = this.$element.find('.mpa-message').first();
5984		this.$preloader = this.$element.find('.mpa-loading');
5985		if (mpapp().settings().isAllowCustomerAccountCreation()) {
5986		  this.$createAccount = this.$element.find('.mpa-customer-create-account');
5987		  this.$createAccountDescription = this.$element.find('.mpa-customer-create-account-description');
5988
5989		  // Set default value of property createAccount.
5990		  // Actually for mode: 'Customer account creation' = 'create_automatically'
5991		  this.setProperty('createAccount', this.$createAccount.prop('checked'));
5992		}
5993		if (window.MPA_CURRENT_CUSTOMER !== undefined) {
5994		  this.setProperty('name', window.MPA_CURRENT_CUSTOMER.name);
5995		  this.$name.val(window.MPA_CURRENT_CUSTOMER.name);
5996		  this.setProperty('email', window.MPA_CURRENT_CUSTOMER.email);
5997		  this.$email.val(window.MPA_CURRENT_CUSTOMER.email);
5998		  this.setProperty('phone', window.MPA_CURRENT_CUSTOMER.phone);
5999		  let iti = window.intlTelInputGlobals.getInstance(this.$phone[0]);
6000		  // set phone number through itt to remove country code from number if it is there
6001		  iti.setNumber(window.MPA_CURRENT_CUSTOMER.phone);
6002		  // send event to validate phone number
6003		  this.$phone.trigger('input');
6004		}
6005		this.service = null;
6006		this.couponSection = null;
6007	  }
6008
6009	  /**
6010	   * @return {String}
6011	   *
6012	   * @access protected
6013	   *
6014	   * @since 1.0
6015	   */
6016	  theId() {
6017		return 'checkout';
6018	  }
6019
6020	  /**
6021	   * @return {Object} {Property name: {type, default}}
6022	   *
6023	   * @access protected
6024	   *
6025	   * @since 1.0
6026	   */
6027	  propertiesSchema() {
6028		return {
6029		  name: {
6030			type: 'string',
6031			default: ''
6032		  },
6033		  email: {
6034			type: 'string',
6035			default: ''
6036		  },
6037		  phone: {
6038			type: 'string',
6039			default: ''
6040		  },
6041		  notes: {
6042			type: 'string',
6043			default: ''
6044		  },
6045		  acceptTerms: {
6046			type: 'bool',
6047			default: false
6048		  },
6049		  $createAccount: {
6050			type: 'bool',
6051			default: false
6052		  }
6053		};
6054	  }
6055
6056	  /**
6057	   * @access protected
6058	   *
6059	   * @since 1.0
6060	   */
6061	  addListeners() {
6062		super.addListeners();
6063		this.$checkoutForm.on('submit', event => false); // Disable submit
6064
6065		this.$name.on('input', event => this.setProperty('name', event.target.value));
6066		this.$email.on('input', event => this.setProperty('email', event.target.value));
6067		this.$phone.on('input', event => {
6068		  this.setProperty('phone', '');
6069		  let iti = window.intlTelInputGlobals.getInstance(this.$phone[0]);
6070		  if (iti.isValidNumber()) {
6071			this.setProperty('phone', iti.getNumber(intlTelInputUtils.numberFormat.E164));
6072		  }
6073		});
6074		this.$phone.on("countrychange", event => {
6075		  this.setProperty('phone', '');
6076		  let iti = window.intlTelInputGlobals.getInstance(this.$phone[0]);
6077		  if (iti.isValidNumber()) {
6078			this.setProperty('phone', iti.getNumber(intlTelInputUtils.numberFormat.E164));
6079		  }
6080		});
6081		this.$notes.on('input', event => this.setProperty('notes', event.target.value));
6082		if (mpapp().settings().getTermsPageIdForAcceptance()) {
6083		  this.$acceptTerms.on('input', event => this.setProperty('acceptTerms', event.target.checked));
6084		}
6085		if (mpapp().settings().isAllowCustomerAccountCreation()) {
6086		  this.$createAccount.on('input', event => {
6087			this.setProperty('createAccount', event.target.checked);
6088			if (event.target.checked) {
6089			  this.$createAccountDescription.removeClass('mpa-hide');
6090			} else {
6091			  this.$createAccountDescription.addClass('mpa-hide');
6092			}
6093		  });
6094		}
6095		this.$element.on('mpa_booking_step_disable', this.disable.bind(this));
6096		this.$element.on('mpa_booking_step_enable', this.enable.bind(this));
6097		this.$element.on('mpa_booking_coupon_applied', () => this.updateOrder());
6098	  }
6099
6100	  /**
6101	   * @since 1.4.0
6102	   */
6103	  load() {
6104		// Initialize coupon section control
6105		if (!this.couponSection) {
6106		  if (mpapp().settings().isCouponsEnabled()) {
6107			this.couponSection = new CouponSection(this.$element.find('.mpa-coupon-details'), this.cart);
6108		  }
6109		} else {
6110		  // Instead of reload()
6111		  this.couponSection.reset(); // Clear previous messages
6112		}
6113
6114		// Is the coupon still applicable to the cart?
6115		if (this.cart.hasCoupon()) {
6116		  this.cart.testCoupon();
6117		}
6118		this.updateOrder();
6119		this.isLoaded = true;
6120		this.readyPromise = Promise.resolve(this);
6121	  }
6122
6123	  /**
6124	   * @since 1.6.2
6125	   */
6126	  reset() {
6127		// do not clear name, email and phone for second reservation and nextes
6128		this.$notes.val('');
6129		// val() will not trigger 'input' event, so reset the properties manually
6130		this.resetProperty('notes');
6131		if (mpapp().settings().getTermsPageIdForAcceptance()) {
6132		  this.$acceptTerms.prop('checked', false);
6133		  this.resetProperty('acceptTerms');
6134		}
6135		if (mpapp().settings().isAllowCustomerAccountCreation()) {
6136		  this.clearMessage();
6137		  this.$createAccount.prop('checked', false);
6138		  this.resetProperty('createAccount');
6139		}
6140
6141		// Reset coupon section
6142		if (this.couponSection) {
6143		  this.couponSection.reset();
6144		}
6145	  }
6146
6147	  /**
6148	   * @since 1.4.0
6149	   * @access protected
6150	   */
6151	  updateOrder() {
6152		if (this.$order.length === 0) {
6153		  return;
6154		}
6155		this.$order.empty();
6156		this.$order.html(mpa_tmpl_order(this.cart.getOrder()));
6157
6158		// Bind remove coupon button
6159		let $removeCoupon = this.$order.find('.mpa-remove-coupon');
6160		if ($removeCoupon.length > 0) {
6161		  $removeCoupon.on('click', this.removeCoupon.bind(this));
6162		}
6163	  }
6164
6165	  /**
6166	   * @since 1.11.0
6167	   *
6168	   * @access protected
6169	   *
6170	   * @param {Event} event
6171	   */
6172	  removeCoupon(event) {
6173		event.preventDefault();
6174		event.stopPropagation();
6175		this.cart.removeCoupon();
6176		this.couponSection.clearMessage();
6177		this.updateOrder();
6178	  }
6179
6180	  /**
6181	   * @return {Boolean}
6182	   *
6183	   * @since 1.0
6184	   */
6185	  isValidInput() {
6186		return this.isValidName() && this.isValidEmail() && this.isValidPhone() && this.isValidAcceptTerms();
6187	  }
6188
6189	  /**
6190	   * @since 1.3.1
6191	   * @access protected
6192	   *
6193	   * @return {Boolean} 
6194	   */
6195	  isValidName() {
6196		return this.name !== '';
6197	  }
6198
6199	  /**
6200	   * @return {Boolean}
6201	   *
6202	   * @access protected
6203	   *
6204	   * @since 1.1.0
6205	   */
6206	  isValidEmail() {
6207		return this.email !== '' && !!this.email.match(/.+@.+/);
6208	  }
6209
6210	  /**
6211	   * @return {Boolean}
6212	   *
6213	   * @access protected
6214	   *
6215	   * @since 1.1.0
6216	   */
6217	  isValidPhone() {
6218		let iti = window.intlTelInputGlobals.getInstance(this.$phone[0]);
6219		return iti.isValidNumber();
6220	  }
6221
6222	  /**
6223	   * @return {Boolean}
6224	   *
6225	   * @access protected
6226	   *
6227	   * @since 1.10.2
6228	   */
6229	  isValidAcceptTerms() {
6230		return !mpapp().settings().getTermsPageIdForAcceptance() || mpapp().settings().isPaymentsEnabled() || this.acceptTerms;
6231	  }
6232
6233	  /**
6234	   * @access protected
6235	   *
6236	   * @since 1.0
6237	   */
6238	  react() {
6239		super.react();
6240
6241		// Always enable submit button to allow validation messages
6242		this.$buttonNext.prop('disabled', false);
6243	  }
6244
6245	  /**
6246	   * @param {String} message
6247	   *
6248	   * @since 1.18.0
6249	   */
6250	  setMessage(message) {
6251		this.$messageHolder.html(message).removeClass('mpa-hide');
6252	  }
6253
6254	  /**
6255	   * @since 1.18.0
6256	   */
6257	  clearMessage() {
6258		this.$messageHolder.html('').addClass('mpa-hide');
6259	  }
6260
6261	  /**
6262	   * @access protected
6263	   *
6264	   * @since 1.18.0
6265	   */
6266	  showPreloader() {
6267		this.$preloader.removeClass('mpa-hide');
6268	  }
6269
6270	  /**
6271	   * @access protected
6272	   *
6273	   * @since 1.18.0
6274	   */
6275	  hidePreloader() {
6276		this.$preloader.addClass('mpa-hide');
6277	  }
6278
6279	  /**
6280	   * @access protected
6281	   *
6282	   * @since 1.0
6283	   */
6284	  maybeSubmit() {
6285		// Disable coupons section
6286		if (this.couponSection) {
6287		  this.couponSection.disable();
6288		}
6289		this.cart.setCustomerDetails({
6290		  name: this.name,
6291		  email: this.email,
6292		  phone: this.phone,
6293		  notes: this.notes,
6294		  acceptTerms: this.acceptTerms
6295		});
6296		if (this.createAccount) {
6297		  this.showPreloader();
6298		  const customerData = {
6299			name: this.name,
6300			email: this.email,
6301			phone: this.phone
6302		  };
6303		  return mpa_rest_post('/customers/create', customerData).then(customer => {
6304			this.hidePreloader();
6305			this.clearMessage();
6306		  }, error => {
6307			this.hidePreloader();
6308			this.setMessage(error);
6309			throw error;
6310		  });
6311		}
6312	  }
6313	}
6314
6315	/**
6316	 * @since 1.5.0
6317	 */
6318	class PaymentGateway {
6319	  /**
6320	   * @since 1.5.0
6321	   * @access protected
6322	   */
6323	  setupProperties() {
6324		/**
6325		 * @since 1.5.0
6326		 * @var {String}
6327		 */
6328		this.gatewayId = 'basic';
6329
6330		/**
6331		 * @since 1.5.0
6332		 * @var {Object}
6333		 */
6334		this.settings = this.getDefaults();
6335
6336		/**
6337		 * @since 1.5.0
6338		 * @var {Object}
6339		 */
6340		this.$mountWrapper = null;
6341
6342		/**
6343		 * @since 1.5.0
6344		 * @var {Promise}
6345		 */
6346		this.loadPromise = null;
6347
6348		/**
6349		 * @since 1.5.0
6350		 * @var {Boolean}
6351		 */
6352		this.isEnabled = false;
6353
6354		/**
6355		 * @since 1.5.0
6356		 * @var {Boolean}
6357		 */
6358		this.isMounted = false;
6359
6360		/**
6361		 * Input(s) have errors.
6362		 *
6363		 * @since 1.5.0
6364		 * @var {Boolean}
6365		 */
6366		this.haveErrors = false;
6367	  }
6368
6369	  /**
6370	   * @since 1.5.0
6371	   * @since 1.6.0 Added cart parameter
6372	   *
6373	   * @param {Object} $mountWrapper
6374	   */
6375	  constructor($mountWrapper, cart) {
6376		this.setupProperties();
6377		this.$mountWrapper = $mountWrapper;
6378		this.cart = cart;
6379	  }
6380
6381	  /**
6382	   * @since 1.5.0
6383	   *
6384	   * @return {Promise}
6385	   */
6386	  load() {
6387		this.addListeners();
6388		this.loadPromise = Promise.resolve(this);
6389		return this.loadPromise;
6390	  }
6391
6392	  /**
6393	   * @since 1.5.0
6394	   * @access protected
6395	   */
6396	  addListeners() {}
6397
6398	  /**
6399	   * @since 1.5.0
6400	   */
6401	  onCartChange(cart) {}
6402
6403	  /**
6404	   * @since 1.5.0
6405	   * @access protected
6406	   *
6407	   * @param {Object} $mountWrapper
6408	   */
6409	  mount($mountWrapper) {}
6410
6411	  /**
6412	   * @since 1.5.0
6413	   *
6414	   * @return {Promise}
6415	   */
6416	  ready() {
6417		return this.loadPromise;
6418	  }
6419
6420	  /**
6421	   * @since 1.5.0
6422	   */
6423	  enable() {
6424		if (!this.isEnabled) {
6425		  if (!this.isMounted) {
6426			this.mount(this.$mountWrapper);
6427			this.isMounted = true;
6428		  }
6429		  this.$mountWrapper.removeClass('mpa-hide');
6430		  this.isEnabled = true;
6431		}
6432	  }
6433
6434	  /**
6435	   * @since 1.5.0
6436	   */
6437	  disable() {
6438		if (this.isEnabled) {
6439		  this.$mountWrapper.addClass('mpa-hide');
6440		  this.isEnabled = false;
6441		}
6442	  }
6443
6444	  /**
6445	   * @since 1.5.0
6446	   *
6447	   * @return {Boolean}
6448	   */
6449	  isValid() {
6450		return !this.haveErrors;
6451	  }
6452
6453	  /**
6454	   * @since 1.5.0
6455	   *
6456	   * @param {Cart} cart
6457	   * @param {Object} bookingDetails
6458	   * @return {Promise} On fulfillment: return processed payment data. On
6459	   *		rejection: throw error.
6460	   */
6461	  processPayment(cart, bookingDetails) {
6462		return mpa_rest_post('/payments/prepare', {
6463		  payment_details: cart.paymentDetails
6464		});
6465	  }
6466
6467	  /**
6468	   * @since 1.5.0
6469	   * @access protected
6470	   *
6471	   * @return {Object}
6472	   */
6473	  getDefaults() {
6474		return {
6475		  country: mpapp().settings().getCountry(),
6476		  redirect_url: {
6477			payment_received: mpapp().settings().getPaymentReceivedPageUrl(),
6478			failed_transaction: mpapp().settings().getFailedTransactionPageUrl()
6479		  }
6480		};
6481	  }
6482
6483	  /**
6484	   * @since 1.6.2
6485	   */
6486	  reset() {}
6487	}
6488
6489	/**
6490	 * @since 1.14.0
6491	 */
6492	class FreeGateway extends PaymentGateway {
6493	  enable() {}
6494	}
6495
6496	/**
6497	 * @since 1.5.0
6498	 */
6499	class StripeGatewayView {
6500	  /**
6501	   * @since 1.5.0
6502	   * @access protected
6503	   */
6504	  setupProperties() {
6505		/**
6506		 * @since 1.14.0
6507		 * @var {Methods}
6508		 */
6509		this.methods = null;
6510
6511		/**
6512		 * @since 1.5.0
6513		 * @var {String}
6514		 */
6515		this.uid = '';
6516
6517		/**
6518		 * @since 1.5.0
6519		 * @var {Map} Key - method name ('card', 'ideal'), value - {$nav, $fields}.
6520		 */
6521		this.paymentMethods = new Map();
6522
6523		/**
6524		 * @since 1.5.0
6525		 * @var {String}
6526		 */
6527		this.selectedMethod = '';
6528
6529		/**
6530		 * @since 1.5.0
6531		 * @var {Object|null}
6532		 */
6533		this.$mountWrapper = null;
6534
6535		/**
6536		 * @since 1.5.0
6537		 * @var {Object|null}
6538		 */
6539		this.$errorsWrapper = null;
6540
6541		/**
6542		 * @since 1.16.0
6543		 * @type {Object}
6544		 */
6545		this.$gatewayPreloader = null;
6546
6547		/**
6548		 * @since 1.16.0
6549		 * @type {Array}
6550		 * [ {method_name} => (bool), true - mounted ]
6551		 */
6552		this.mountedMethods = [];
6553	  }
6554
6555	  /**
6556	   * @since 1.5.0
6557	   *
6558	   * @param {Object[]} Stripe payment methods
6559	   */
6560	  constructor(methods) {
6561		this.setupProperties();
6562		this.methods = methods;
6563		this.uid = mpa_uniqid();
6564		this.addPaymentMethods(this.methods);
6565	  }
6566	  mountedMethod() {
6567		let hasUnmountedMethod = false;
6568		Object.entries(this.mountedMethods).forEach((mountedMethod, isMountedMethod) => {
6569		  if (!isMountedMethod) {
6570			hasUnmountedMethod = true;
6571		  }
6572		});
6573		if (hasUnmountedMethod) {
6574		  this.$gatewayPreloader.addClass('mpa-hide');
6575		}
6576	  }
6577
6578	  /**
6579	   * @since 1.5.0
6580	   *
6581	   * @param {String[]} paymentMethods
6582	   */
6583	  addPaymentMethods(paymentMethods) {
6584		for (const paymentMethod in paymentMethods) {
6585		  if (this.paymentMethods.includesKey(paymentMethod)) {
6586			continue; // Already exists
6587		  }
6588
6589		  // Add new payment method
6590		  this.paymentMethods.push(paymentMethod, {
6591			$nav: null,
6592			$fields: null
6593		  });
6594
6595		  // Init this.selectedMethod property
6596		  if (!this.selectedMethod) {
6597			this.selectedMethod = paymentMethod;
6598		  }
6599		}
6600	  }
6601
6602	  /**
6603	   * @since 1.5.0
6604	   *
6605	   * @return {Boolean}
6606	   */
6607	  isMounted() {
6608		return this.$mountWrapper !== null;
6609	  }
6610
6611	  /**
6612	   * @since 1.5.0
6613	   *
6614	   * @param {Object} $mountWrapper
6615	   */
6616	  mount($mountWrapper) {
6617		$mountWrapper.append(this.render());
6618		this.$gatewayPreloader = $mountWrapper.parent().find('.mpa-payment-gateway-title .mpa-preloader');
6619		this.$gatewayPreloader.removeClass('mpa-hide');
6620
6621		// Init all payment methods
6622		this.paymentMethods.forEach((references, i, paymentMethod) => {
6623		  references.$nav = $mountWrapper.find('.mpa-stripe-payment-method.' + paymentMethod);
6624		  references.$fields = $mountWrapper.find('.mpa-stripe-payment-fields.' + paymentMethod);
6625		  const methodControl = this.methods[paymentMethod].getControl();
6626		  if (methodControl !== null) {
6627			const elementSelector = this.getElementSelector(paymentMethod);
6628			this.mountedMethods[paymentMethod] = false;
6629			methodControl.mount(elementSelector);
6630			methodControl.on('ready', paymentMethod => {
6631			  this.mountedMethod(paymentMethod);
6632			  document.querySelector(elementSelector).classList.remove('mpa-preloader-skeleton-pulsate');
6633			});
6634		  }
6635
6636		  // todo: better to refactor
6637		  if (paymentMethod === 'card') {
6638			this.methods['card'].isCanMakePaymentRequest().then(canMakePayment => {
6639			  const elementPaymentRequestButtonSelector = this.getElementSelector('payment-request-button');
6640			  const elementPaymentRequestButton = document.querySelector(elementPaymentRequestButtonSelector);
6641			  if (!elementPaymentRequestButton) {
6642				return;
6643			  }
6644			  if (canMakePayment) {
6645				this.mountedMethods['payment_request_button'] = false;
6646				this.methods['card'].paymentRequestButton.mount(elementPaymentRequestButtonSelector);
6647				this.methods['card'].paymentRequestButton.on('ready', event => {
6648				  this.mountedMethod('payment_request_button');
6649				  elementPaymentRequestButton.classList.remove('mpa-preloader-skeleton-pulsate');
6650				});
6651			  } else {
6652				elementPaymentRequestButton.classList.add('mpa-hide');
6653				document.querySelector('.mpa-stripe-payment-request-button-separator').classList.add('mpa-hide');
6654			  }
6655			});
6656		  }
6657		});
6658
6659		// Listen change of payment method
6660		$mountWrapper.find('input[name="stripe_payment_method"]').on('change', this.onPaymentMethodChange.bind(this));
6661		this.$mountWrapper = $mountWrapper;
6662		this.$errorsWrapper = $mountWrapper.find('.mpa-errors');
6663	  }
6664
6665	  /**
6666	   * @since 1.5.0
6667	   * @access protected
6668	   *
6669	   * @param {Event} event
6670	   */
6671	  onPaymentMethodChange(event) {
6672		// Clear previous control
6673		let previousControl = null;
6674		switch (this.selectedMethod) {
6675		  case 'card':
6676		  case 'ideal':
6677		  case 'sepa_debit':
6678			previousControl = this.methods[this.selectedMethod].getControl();
6679			break;
6680		}
6681		if (previousControl !== null) {
6682		  previousControl.clear();
6683		}
6684
6685		// Select new payment method
6686		this.selectPaymentMethod(event.target.value);
6687	  }
6688
6689	  /**
6690	   * @since 1.5.0
6691	   *
6692	   * @param {String} paymentMethod
6693	   */
6694	  selectPaymentMethod(paymentMethod) {
6695		if (paymentMethod !== this.selectedMethod) {
6696		  this.togglePaymentMethod(this.selectedMethod, false);
6697		  this.togglePaymentMethod(paymentMethod, true);
6698		  this.selectedMethod = paymentMethod;
6699		}
6700	  }
6701
6702	  /**
6703	   * @since 1.5.0
6704	   *
6705	   * @param {String} paymentMethod
6706	   * @param {Boolean} enable
6707	   */
6708	  togglePaymentMethod(paymentMethod, enable) {
6709		if (this.isMounted() && this.paymentMethods.includesKey(paymentMethod)) {
6710		  let refs = this.paymentMethods.find(paymentMethod);
6711		  refs.$nav.toggleClass('active', enable);
6712		  refs.$fields.toggleClass('mpa-hide', !enable);
6713		}
6714	  }
6715
6716	  /**
6717	   * @since 1.5.0
6718	   *
6719	   * @param {String} paymentMethod
6720	   * @return {String}
6721	   */
6722	  getElementSelector(paymentMethod) {
6723		if (paymentMethod === 'sepa_debit') {
6724		  paymentMethod = 'iban';
6725		}
6726		return '#mpa-stripe-' + paymentMethod + '-element-' + this.uid;
6727	  }
6728
6729	  /**
6730	   * @since 1.5.0
6731	   *
6732	   * @return {String}
6733	   */
6734	  render() {
6735		let output = '';
6736		output += '<section class="mpa-stripe-payment-container">';
6737
6738		// Navigation tabs
6739		if (this.paymentMethods.length > 1) {
6740		  output += this.renderNavigation();
6741		}
6742
6743		// Payment fields
6744		for (let paymentMethod of this.paymentMethods.keys) {
6745		  output += this.renderFields(paymentMethod);
6746		}
6747
6748		// Errors wrapper
6749		output += '<div class="mpa-errors"></div>';
6750		output += '</section>';
6751		return output;
6752	  }
6753
6754	  /**
6755	   * @since 1.5.0
6756	   * @access protected
6757	   *
6758	   * @return {String}
6759	   */
6760	  renderNavigation() {
6761		let output = '';
6762		output += '<nav class="mpa-stripe-payment-methods">';
6763		output += '<ul>';
6764		for (let paymentMethod of this.paymentMethods.keys) {
6765		  let isSelected = paymentMethod === this.selectedMethod;
6766		  let activeClass = isSelected ? ' active' : '';
6767		  let checkedAttr = isSelected ? ' checked="checked"' : '';
6768		  output += '<li class="mpa-stripe-payment-method ' + paymentMethod + activeClass + '">';
6769		  output += '<label>';
6770		  output += '<input type="radio" name="stripe_payment_method" value="' + paymentMethod + '"' + checkedAttr + '>';
6771		  output += ' ' + this.methods[paymentMethod].title;
6772		  output += '</label>';
6773		  output += '</li>';
6774		}
6775		output += '</ul>';
6776		output += '</nav>';
6777		return output;
6778	  }
6779
6780	  /**
6781	   * @since 1.5.0
6782	   * @access protected
6783	   *
6784	   * @param {String} paymentMethod
6785	   * @return {String}
6786	   */
6787	  renderFields(paymentMethod) {
6788		let isSelected = paymentMethod === this.selectedMethod;
6789		let hideClass = isSelected ? '' : ' mpa-hide';
6790		let output = '';
6791		output += '<div class="mpa-stripe-payment-fields ' + paymentMethod + hideClass + '">';
6792		output += '<fieldset>';
6793		switch (paymentMethod) {
6794		  case 'card':
6795			output += this.renderCardFields();
6796			break;
6797		  case 'ideal':
6798			output += this.renderIdealFields();
6799			break;
6800		  case 'sepa_debit':
6801			output += this.renderSepaDebitFields();
6802			break;
6803		  default:
6804			output += this.renderRedirectNotice();
6805			break;
6806		}
6807		output += '</fieldset>';
6808		if (paymentMethod === 'sepa_debit') {
6809		  output += '<p class="notice">';
6810		  output +=
6811		  // https://stripe.com/docs/sources/sepa-debit#prerequisite
6812		  // Translators: %s: Business name.
6813		  __('By providing your IBAN and confirming this payment, you authorise (A) %s and Stripe, our payment service provider, to send instructions to your bank to debit your account and (B) your bank to debit your account in accordance with those instructions. You are entitled to a refund from your bank under the terms and conditions of your agreement with your bank. A refund must be claimed within 8 weeks starting from the date on which your account was debited.', 'motopress-appointment').replace('%s', mpapp().settings().getBusinessName());
6814		  output += '</p>';
6815		}
6816		output += '</div>';
6817		return output;
6818	  }
6819
6820	  /**
6821	   * @since 1.5.0
6822	   * @access protected
6823	   *
6824	   * @return {String}
6825	   */
6826	  renderCardFields() {
6827		let output = '';
6828		output += '<label for="mpa-stripe-card-element-' + this.uid + '">';
6829		output += __('Credit or debit card', 'motopress-appointment');
6830		output += '</label>';
6831		if (this.methods['card'].isEnabledWallets()) {
6832		  output += '<div id="mpa-stripe-payment-request-button-element-' + this.uid + '" class="mpa-stripe-element mpa-stripe-payment-request-button-element mpa-preloader-skeleton-pulsate StripeElement"></div>';
6833		  output += '<div class="mpa-stripe-payment-request-button-separator">' + __('or', 'motopress-appointment') + '</div>';
6834		}
6835		output += '<div id="mpa-stripe-card-element-' + this.uid + '" class="mpa-stripe-element mpa-stripe-card-element mpa-preloader-skeleton-pulsate"></div>';
6836		return output;
6837	  }
6838
6839	  /**
6840	   * @since 1.5.0
6841	   * @access protected
6842	   *
6843	   * @return {String}
6844	   */
6845	  renderIdealFields() {
6846		let output = '';
6847		output += '<label for="mpa-stripe-ideal-element-' + this.uid + '">';
6848		output += __('Select iDEAL Bank', 'motopress-appointment');
6849		output += '</label>';
6850		output += '<div id="mpa-stripe-ideal-element-' + this.uid + '" class="mpa-stripe-element mpa-stripe-ideal-element mpa-preloader-skeleton-pulsate"></div>';
6851		return output;
6852	  }
6853
6854	  /**
6855	   * @since 1.5.0
6856	   * @access protected
6857	   *
6858	   * @return {String}
6859	   */
6860	  renderSepaDebitFields() {
6861		let output = '';
6862		output += '<label for="mpa-stripe-iban-element-' + this.uid + '">';
6863		output += __('IBAN', 'motopress-appointment');
6864		output += '</label>';
6865		output += '<div id="mpa-stripe-iban-element-' + this.uid + '" class="mpa-stripe-element mpa-stripe-iban-element mpa-preloader-skeleton-pulsate"></div>';
6866		return output;
6867	  }
6868
6869	  /**
6870	   * @since 1.5.0
6871	   * @access protected
6872	   *
6873	   * @return {String}
6874	   */
6875	  renderRedirectNotice() {
6876		let output = '';
6877		output += '<p class="notice">';
6878		output += __('You will be redirected to a secure page to complete the payment.', 'motopress-appointment');
6879		output += '</p>';
6880		return output;
6881	  }
6882
6883	  /**
6884	   * @since 1.5.0
6885	   *
6886	   * @param {String} message
6887	   */
6888	  showError(message) {
6889		if (this.isMounted()) {
6890		  this.$errorsWrapper.html(message).removeClass('mpa-hide');
6891		}
6892	  }
6893
6894	  /**
6895	   * @since 1.5.0
6896	   */
6897	  hideErrors() {
6898		if (this.isMounted()) {
6899		  this.$errorsWrapper.addClass('mpa-hide').html('');
6900		}
6901	  }
6902
6903	  /**
6904	   * @since 1.6.2
6905	   */
6906	  reset() {
6907		let resetMethod = this.paymentMethods.firstKey();
6908		this.selectPaymentMethod(resetMethod);
6909
6910		// No need to clear the control, StripeGateway will do the job:
6911		//     this.methods.reset();
6912	  }
6913	}
6914
6915	/**
6916	 * @since 1.5.0
6917	 */
6918	class CustomizablePaymentGateway extends PaymentGateway {
6919	  /**
6920	   * @since 1.5.0
6921	   *
6922	   * @return {Promise}
6923	   */
6924	  load() {
6925		// Note: there is no addListeners() here anymore
6926		this.loadPromise = mpa_rest_get('/payments/settings?nocache', {
6927		  gateway_id: this.gatewayId
6928		}).catch(error => console.error(error.message) || {}) // "Invalid request: ..."
6929		.then(settings => {
6930		  jQuery.extend(this.settings, settings);
6931		  return this;
6932		});
6933		return this.loadPromise;
6934	  }
6935	}
6936
6937	/**
6938	 * @since 1.14.0
6939	 */
6940	class AbstractMethod {
6941	  name = null;
6942	  title = null;
6943	  control = null;
6944	  api = null;
6945	  elements = null;
6946
6947	  /**
6948	   *
6949	   * @param {Stripe} api Stripe api ref
6950	   * @param {Elements} elements Stripe.elements()
6951	   * @param {Elements} elements Stripe.elements()
6952	   */
6953	  constructor(api, elements, settings) {
6954		this.api = api;
6955		this.settings = settings;
6956		this.elements = elements;
6957		if (new.target === AbstractMethod) {
6958		  throw new Error("Cannot construct Abstract instances directly");
6959		}
6960		if (this.setupProperties === undefined) {
6961		  throw new Error("Must override method: setupProperties()");
6962		}
6963		this.setupProperties();
6964		if (this.name === null || this.name === undefined) {
6965		  throw new Error('"name" must be defined in a non-abstract payment method class');
6966		}
6967		if (this.title === null || this.title === undefined) {
6968		  throw new Error('"title" must be defined in a non-abstract payment method class');
6969		}
6970	  }
6971
6972	  /**
6973	   * If payment method has control elements, this method must creates an instance of the Payment Element.
6974	   * If hasn't controls, need return null.
6975	   *
6976	   * @see {@link https://stripe.com/docs/js/elements_object/create_payment_element}
6977	   *
6978	   * @return {Promise<Object>|null}
6979	   */
6980	  createControl() {
6981		return null;
6982	  }
6983
6984	  /**
6985	   * If payment method has control elements, this method must creates an instance of the Payment Element.
6986	   * If hasn't controls, need return null.
6987	   *
6988	   * @see {@link https://stripe.com/docs/js/elements_object/create_payment_element}
6989	   *
6990	   * @return {Promise<Object>|null}
6991	   */
6992	  getControl() {
6993		if (!this.control) {
6994		  this.control = this.createControl();
6995		}
6996		return this.control;
6997	  }
6998
6999	  /**
7000	   * @see {@link https://stripe.com/docs/js/element/other_methods/clear}
7001	   */
7002	  reset() {
7003		if (this.control !== null) {
7004		  this.control.clear();
7005		}
7006	  }
7007
7008	  /**
7009	   *
7010	   * @param {string} name
7011	   * @param {string} email
7012	   * @param {string} phone
7013	   *
7014	   * @return {Object} PaymentMethodData
7015	   * @see https://stripe.com/docs/api/payment_methods/object
7016	   */
7017	  createPaymentMethodData(name, email, phone) {
7018		let paymentMethodData = {
7019		  type: this.name,
7020		  billing_details: {
7021			name: name.padEnd(3, ' '),
7022			// Some methods require name length 3 or more symbols. Fill requirements by spaces.
7023			email: email,
7024			phone: phone
7025		  }
7026		};
7027		if (this.control !== null) {
7028		  paymentMethodData[this.name] = this.control;
7029		}
7030		return paymentMethodData;
7031	  }
7032
7033	  /**
7034	   *
7035	   * @param {Object} paymentMethodData
7036	   * @see https://stripe.com/docs/api/payment_methods/object
7037	   *
7038	   * @return {Promise} paymentMethod or error
7039	   */
7040	  createPaymentMethod(paymentMethodData) {
7041		return this.api.createPaymentMethod(paymentMethodData);
7042	  }
7043
7044	  /**
7045	   *
7046	   * @param client_secret The client secret of the PaymentIntent.
7047	   * @param redirect_url_received
7048	   * @return {Promise}
7049	   */
7050	  confirmPayment(client_secret, redirect_url_received) {
7051		throw new Error('Abstract Method has no implementation');
7052	  }
7053
7054	  /**
7055	   * @param {Cart} cart
7056	   * @param {Object} paymentDetails Gateway settings + booking details.
7057	   * @param {Object} processArgs Optional.
7058	   *        @param {Function} processArgs['error_handler'] Callback to log errors.
7059	   * @return {Promise}
7060	   */
7061	  processPayment(cart, paymentDetails, processArgs) {
7062		const customer = cart.getCustomer();
7063		const paymentMethodData = this.createPaymentMethodData(customer.name, customer.email, customer.phone);
7064
7065		// Step 1: create payment method on the client side
7066		return this.createPaymentMethod(paymentMethodData)
7067
7068		// Step 2: create payment intent on the server side
7069		.then(paymentMethod => {
7070		  if (paymentMethod.error) {
7071			throw new Error(paymentMethod.error.message);
7072		  }
7073		  const paymentDetails = jQuery.extend(cart.paymentDetails, {
7074			payment_method_id: paymentMethod.paymentMethod.id
7075		  });
7076		  return mpa_rest_post('/payments/prepare', {
7077			payment_details: paymentDetails
7078		  });
7079		})
7080
7081		// Step 3: confirm payment intent on the client side
7082		.then(paymentIntentClientSecret => {
7083		  // todo: param paymentDetails.redirect_url.payment_received is not used in all implementations.
7084		  // It is better to move the parameter passing to another place of initialization.
7085		  return this.confirmPayment(paymentIntentClientSecret, paymentDetails.redirect_url.payment_received)
7086
7087		  // Handle errors
7088		  .then(confirmation => {
7089			if (confirmation.error) {
7090			  throw new Error(confirmation.error.message);
7091			} else {
7092			  return confirmation.paymentIntent;
7093			}
7094		  });
7095		})
7096
7097		// Step 4: build the final payment data
7098		.then(paymentIntent => {
7099		  let paymentData = {
7100			payment_method: this.name,
7101			payment_intent_id: paymentIntent.id
7102		  };
7103		  if (paymentIntent.status == 'requires_action' && paymentIntent.next_action.type == 'redirect_to_url') {
7104			paymentData.redirect_url = paymentIntent.next_action.redirect_to_url.url;
7105		  }
7106		  return paymentData;
7107		})
7108
7109		// Catch any error
7110		.catch(error => {
7111		  console.error('Unable to payment process.', error.message);
7112		  if (processArgs.error_handler != undefined) {
7113			processArgs.error_handler(error.message);
7114		  }
7115		  throw error;
7116		});
7117	  }
7118	}
7119
7120	/**
7121	 * snake_case to camelCase
7122	 *
7123	 * @param {String}
7124	 * @return {String}
7125	 *
7126	 * @since 1.16.0
7127	 */
7128	function mpa_snake_to_camel_case(string) {
7129	  return string.toLowerCase().replace(/([-_][a-z])/g, group => group.toUpperCase().replace('-', '').replace('_', ''));
7130	}
7131
7132	/**
7133	 * @since 1.14.0
7134	 */
7135	class CardMethod extends AbstractMethod {
7136	  setupProperties() {
7137		this.name = 'card';
7138		this.title = __('Card', 'motopress-appointment');
7139
7140		/**
7141		 * Payment process event object with payment request button
7142		 * Necessary for correct processing by the processPayment() method and completion of the payment process
7143		 *
7144		 * @since 1.16.0
7145		 * @protected
7146		 *
7147		 * @type {Event}
7148		 */
7149		this.paymentRequestButtonEvent = null;
7150
7151		/**
7152		 * @since 1.16.0
7153		 * @protected
7154		 *
7155		 * @type {Promise<array|null>}
7156		 */
7157		this.canMakePaymentRequest = Promise.resolve(null);
7158		if (this.isEnabledWallets()) {
7159		  this.paymentRequest = this.createPaymentRequest();
7160		  this.canMakePaymentRequest = this.paymentRequest.canMakePayment();
7161		}
7162	  }
7163
7164	  /**
7165	   * @since 1.16.0
7166	   *
7167	   * @protected
7168	   *
7169	   * @return {PaymentRequest} Object of PaymentRequest
7170	   */
7171	  createPaymentRequest() {
7172		if (this.paymentRequest) {
7173		  return this.paymentRequest;
7174		}
7175		return this.api.paymentRequest({
7176		  country: this.settings.country,
7177		  currency: mpapp().settings().getCurrency().toLowerCase(),
7178		  total: {
7179			label: __('Total', 'motopress-appointment'),
7180			amount: 0,
7181			pending: true
7182		  },
7183		  requestPayerName: false,
7184		  requestPayerEmail: false,
7185		  requestPayerPhone: false,
7186		  requestShipping: false,
7187		  disableWallets: this.getDisabledWallets()
7188		});
7189	  }
7190
7191	  /**
7192	   * @since 1.16.0
7193	   *
7194	   * @return {Promise<null|array>}
7195	   */
7196	  isCanMakePaymentRequest() {
7197		return this.canMakePaymentRequest;
7198	  }
7199
7200	  /**
7201	   * @since 1.16.0
7202	   *
7203	   * @protected
7204	   *
7205	   * @return {string[]}
7206	   */
7207	  getPossibleWallets() {
7208		return ['apple_pay', 'google_pay', 'link'];
7209	  }
7210
7211	  /**
7212	   * @since 1.16.0
7213	   *
7214	   * @protected
7215	   *
7216	   * @return {Boolean}
7217	   */
7218	  isEnabledWallets() {
7219		let isEnabledWallets = false;
7220		const wallets = this.getPossibleWallets();
7221		wallets.forEach(wallet => {
7222		  if (this.settings.payment_methods.includes(wallet)) {
7223			isEnabledWallets = true;
7224		  }
7225		});
7226		return isEnabledWallets;
7227	  }
7228
7229	  /**
7230	   * @since 1.16.0
7231	   *
7232	   * @return {Array} Array of disabled wallets name
7233	   */
7234	  getDisabledWallets() {
7235		let disabledWallets = [];
7236		const wallets = this.getPossibleWallets();
7237		wallets.forEach(wallet => {
7238		  if (!this.settings.payment_methods.includes(wallet)) {
7239			const walletNameInCamelCase = mpa_snake_to_camel_case(wallet);
7240			disabledWallets.push(walletNameInCamelCase);
7241		  }
7242		});
7243		return disabledWallets;
7244	  }
7245
7246	  /**
7247	   * @since 1.16.0
7248	   *
7249	   * @see https://stripe.com/docs/js/elements_object/create_payment_element
7250	   *
7251	   * @protected
7252	   *
7253	   * @return {Object}
7254	   */
7255	  createPaymentRequestButton() {
7256		return this.elements.create('paymentRequestButton', {
7257		  paymentRequest: this.paymentRequest,
7258		  style: {
7259			paymentRequestButton: {
7260			  height: '50px'
7261			}
7262		  }
7263		});
7264	  }
7265
7266	  /**
7267	   * @since 1.16.0
7268	   * @protected
7269	   *
7270	   * @param {Event}
7271	   */
7272	  processPaymentRequestButton(event) {
7273		this.paymentRequestButtonEvent = event;
7274		jQuery('.mpa-booking-step-payment .mpa-actions .mpa-button-next').trigger('click');
7275	  }
7276
7277	  /**
7278	   * @since 1.16.0
7279	   *
7280	   * @param {Cart} cart
7281	   * @param {Object} processArgs Optional.
7282	   *        @param {Function} processArgs['error_handler'] Callback to log errors.
7283	   * @return {Promise}
7284	   */
7285	  proccessPaymentRequestButtonHandler(cart, processArgs) {
7286		const customer = cart.getCustomer();
7287		return this.api.createPaymentMethod({
7288		  type: 'card',
7289		  card: {
7290			token: this.paymentRequestButtonEvent.token.id
7291		  },
7292		  billing_details: {
7293			name: customer.name,
7294			email: customer.email,
7295			phone: customer.phone
7296		  }
7297		}).then(paymentMethod => {
7298		  if (paymentMethod.error) {
7299			this.paymentRequestButtonEvent.complete('fail');
7300			throw new Error(paymentMethod.error.message);
7301		  }
7302		  const paymentDetails = jQuery.extend(cart.paymentDetails, {
7303			payment_method_id: paymentMethod.paymentMethod.id
7304		  });
7305		  return mpa_rest_post('/payments/prepare', {
7306			payment_details: paymentDetails
7307		  });
7308		}).then(paymentIntentClientSecret => {
7309		  // todo: param paymentDetails.redirect_url.payment_received is not used in all implementations.
7310		  // It is better to move the parameter passing to another place of initialization.
7311		  return this.confirmPayment(paymentIntentClientSecret)
7312
7313		  // Handle errors
7314		  .then(confirmation => {
7315			if (confirmation.error) {
7316			  this.paymentRequestButtonEvent.complete('fail');
7317			  this.paymentRequestButtonEvent = null;
7318			  throw new Error(confirmation.error.message);
7319			} else {
7320			  return confirmation.paymentIntent;
7321			}
7322		  });
7323		})
7324
7325		// Step 4: build the final payment data
7326		.then(paymentIntent => {
7327		  let paymentData = {
7328			payment_method: this.name,
7329			payment_intent_id: paymentIntent.id
7330		  };
7331		  this.paymentRequestButtonEvent.complete('success');
7332		  this.paymentRequestButtonEvent = null;
7333		  return paymentData;
7334		})
7335
7336		// Catch any error
7337		.catch(error => {
7338		  this.paymentRequestButtonEvent.complete('fail');
7339		  this.paymentRequestButtonEvent = null;
7340		  console.error('Unable to payment process.', error.message);
7341		  if (processArgs.error_handler != undefined) {
7342			processArgs.error_handler(error.message);
7343		  }
7344		  throw error;
7345		});
7346	  }
7347
7348	  /**
7349	   *
7350	   * @param client_secret The client secret of the PaymentIntent.
7351	   *
7352	   * @see {@link https://stripe.com/docs/js/payment_intents/confirm_card_payment}
7353	   *
7354	   * @return {Promise}
7355	   */
7356	  confirmPayment(client_secret) {
7357		return this.api.confirmCardPayment(client_secret);
7358	  }
7359
7360	  /**
7361	   * @param {Cart} cart
7362	   * @param {Object} paymentDetails Gateway settings + booking details.
7363	   * @param {Object} processArgs Optional.
7364	   *        @param {Function} processArgs['error_handler'] Callback to log errors.
7365	   * @return {Promise}
7366	   */
7367	  processPayment(cart, paymentDetails, processArgs) {
7368		if (this.paymentRequestButtonEvent) {
7369		  return this.proccessPaymentRequestButtonHandler(cart, processArgs);
7370		}
7371		return super.processPayment(cart, paymentDetails, processArgs);
7372	  }
7373
7374	  /**
7375	   * @see https://stripe.com/docs/js/elements_object/create_payment_element
7376	   *
7377	   * @return {Object}
7378	   */
7379	  createControl() {
7380		return this.elements.create(this.name, {
7381		  style: this.settings.style,
7382		  hidePostalCode: this.settings.hide_postal_code
7383		});
7384	  }
7385	}
7386
7387	/**
7388	 * @since 1.14.0
7389	 */
7390	class SepaDebitMethod extends AbstractMethod {
7391	  setupProperties() {
7392		this.name = 'sepa_debit';
7393		this.title = __('SEPA Direct Debit', 'motopress-appointment');
7394	  }
7395
7396	  /**
7397	   *
7398	   * @param client_secret The client secret of the PaymentIntent.
7399	   * @param redirect_url_received
7400	   *
7401	   * @see {@link https://stripe.com/docs/js/payment_intents/confirm_sepa_debit_payment}
7402	   *
7403	   * @return {Promise}
7404	   */
7405	  confirmPayment(client_secret, redirect_url_received) {
7406		return this.api.confirmSepaDebitPayment(client_secret);
7407	  }
7408
7409	  /**
7410	   * @see https://stripe.com/docs/js/elements_object/create_payment_element
7411	   *
7412	   * @return {Promise<Object>}
7413	   */
7414	  createControl() {
7415		return this.elements.create('iban', {
7416		  style: this.settings.style,
7417		  supportedCountries: ['SEPA']
7418		});
7419	  }
7420	}
7421
7422	/**
7423	 * @since 1.14.0
7424	 */
7425	class BancontactMethod extends AbstractMethod {
7426	  setupProperties() {
7427		this.name = 'bancontact';
7428		this.title = __('Bancontact', 'motopress-appointment');
7429	  }
7430
7431	  /**
7432	   *
7433	   * @param client_secret The client secret of the PaymentIntent.
7434	   * @param redirect_url_received
7435	   *
7436	   * @see {@link https://stripe.com/docs/js/payment_intents/confirm_bancontact_payment}
7437	   *
7438	   * @return {Promise}
7439	   */
7440	  confirmPayment(client_secret, redirect_url_received) {
7441		return this.api.confirmBancontactPayment(client_secret, {
7442		  return_url: redirect_url_received
7443		}, {
7444		  handleActions: false
7445		});
7446	  }
7447	}
7448
7449	/**
7450	 * @since 1.14.0
7451	 */
7452	class IdealMethod extends AbstractMethod {
7453	  setupProperties() {
7454		this.name = 'ideal';
7455		this.title = __('iDEAL', 'motopress-appointment');
7456	  }
7457
7458	  /**
7459	   *
7460	   * @param client_secret The client secret of the PaymentIntent.
7461	   * @param redirect_url_received
7462	   *
7463	   * @see {@link https://stripe.com/docs/js/payment_intents/confirm_ideal_payment}
7464	   *
7465	   * @return {Promise}
7466	   */
7467	  confirmPayment(client_secret, redirect_url_received) {
7468		return this.api.confirmIdealPayment(client_secret, {
7469		  return_url: redirect_url_received
7470		}, {
7471		  handleActions: false
7472		});
7473	  }
7474
7475	  /**
7476	   * @see https://stripe.com/docs/js/elements_object/create_payment_element
7477	   *
7478	   * @return {Promise<Object>}
7479	   */
7480	  createControl() {
7481		return this.elements.create('idealBank', {
7482		  style: this.settings.style
7483		});
7484	  }
7485	}
7486
7487	/**
7488	 * @since 1.14.0
7489	 */
7490	class GiropayMethod extends AbstractMethod {
7491	  setupProperties() {
7492		this.name = 'giropay';
7493		this.title = __('Giropay', 'motopress-appointment');
7494	  }
7495
7496	  /**
7497	   *
7498	   * @param client_secret The client secret of the PaymentIntent.
7499	   * @param redirect_url_received
7500	   *
7501	   * @see {@link https://stripe.com/docs/js/payment_intents/confirm_giropay_payment}
7502	   *
7503	   * @return {Promise}
7504	   */
7505	  confirmPayment(client_secret, redirect_url_received) {
7506		return this.api.confirmGiropayPayment(client_secret, {
7507		  return_url: redirect_url_received
7508		}, {
7509		  handleActions: false
7510		});
7511	  }
7512	}
7513
7514	/**
7515	 * @since 1.14.0
7516	 */
7517	class SofortMethod extends AbstractMethod {
7518	  setupProperties() {
7519		this.name = 'sofort';
7520		this.title = __('SOFORT', 'motopress-appointment');
7521	  }
7522
7523	  /**
7524	   *
7525	   * @param client_secret The client secret of the PaymentIntent.
7526	   * @param redirect_url_received
7527	   *
7528	   * @see {@link https://stripe.com/docs/js/payment_intents/confirm_sofort_payment}
7529	   *
7530	   * @return {Promise}
7531	   */
7532	  confirmPayment(client_secret, redirect_url_received) {
7533		return this.api.confirmSofortPayment(client_secret, {
7534		  return_url: redirect_url_received
7535		}, {
7536		  handleActions: false
7537		});
7538	  }
7539
7540	  /**
7541	   *
7542	   * @param {string} name
7543	   * @param {string} email
7544	   * @param {string} phone
7545	   *
7546	   * @return {Promise} paymentMethod or error
7547	   */
7548	  createPaymentMethodData(name, email, phone) {
7549		let paymentMethodData = super.createPaymentMethodData(name, email, phone);
7550		paymentMethodData.sofort = {
7551		  country: this.settings.country
7552		};
7553		return paymentMethodData;
7554	  }
7555	}
7556
7557	/**
7558	 * @since 1.5.0
7559	 */
7560	class StripeGateway extends CustomizablePaymentGateway {
7561	  /**
7562	   * @since 1.5.0
7563	   * @access protected
7564	   */
7565	  setupProperties() {
7566		super.setupProperties();
7567
7568		/**
7569		 * @since 1.5.0
7570		 * @var {Object}
7571		 */
7572		this.$gatewayPreloader = null;
7573
7574		/**
7575		 * @since 1.5.0
7576		 * @var {String}
7577		 */
7578		this.gatewayId = 'stripe';
7579
7580		/**
7581		 * @since 1.14.0
7582		 * @var {Object[]} Stripe payment methods
7583		 */
7584		this.methods = null;
7585
7586		/**
7587		 * @since 1.5.0
7588		 * @var {StripeGatewayView}
7589		 */
7590		this.view = null;
7591	  }
7592
7593	  /**
7594	   * @since 1.5.0
7595	   * @since 1.6.0 Added cart parameter
7596	   *
7597	   * @param {Object} $mountWrapper
7598	   */
7599	  constructor($mountWrapper, cart) {
7600		super($mountWrapper, cart);
7601		this.$gatewayPreloader = $mountWrapper.parent().find('.mpa-payment-gateway-title .mpa-preloader');
7602	  }
7603
7604	  /**
7605	   * @since 1.5.0
7606	   *
7607	   * @return {Promise}
7608	   */
7609	  load() {
7610		this.loadPromise = super.load().then(() => {
7611		  this.methods = this.createPaymentMethods();
7612		  this.view = new StripeGatewayView(this.methods);
7613		  return this;
7614		});
7615		return this.loadPromise;
7616	  }
7617
7618	  /**
7619	   * Validation checks Terms & Conditions checkbox. Checkbox must be checked.
7620	   * If validation crash, showing default html validation message.
7621	   *
7622	   * todo: paypal has the same method, we need to move the check to another method
7623	   *
7624	   * @return {Boolean}
7625	   */
7626	  isValidAcceptTerms() {
7627		if (!mpapp().settings().getTermsPageIdForAcceptance()) {
7628		  return true;
7629		}
7630		const acceptTermsMarkup = jQuery('.mpa-booking-step-payment .mpa-accept-terms')[0];
7631		if (!acceptTermsMarkup.checkValidity()) {
7632		  acceptTermsMarkup.reportValidity();
7633		  return false;
7634		}
7635		return true;
7636	  }
7637
7638	  /**
7639	   * @see Similar method to StripeAPI::convertToSmallestUnit() in StripeAPI.php.
7640	   *
7641	   * @since 1.16.0
7642	   *
7643	   * Only used for wallets payments in the paymentRequest object.
7644	   * This is necessary to display the correct price in the paymentRequest browser payment interface.
7645	   * The real payment price is formed on the backend side when the PaymentIntent is created.
7646	   *
7647	   * @param {Number} amount
7648	   * @param {String} currency Optional.
7649	   *
7650	   * @return {Number}
7651	   */
7652	  convertToSmallestUnit(amount, currency) {
7653		if (!currency) {
7654		  currency = mpapp().settings().getCurrency();
7655		}
7656
7657		// See all currencies presented as links on page
7658		// https://stripe.com/docs/currencies#presentment-currencies
7659		switch (currency.toUpperCase()) {
7660		  // Zero decimal currencies
7661		  case 'BIF':
7662		  case 'CLP':
7663		  case 'DJF':
7664		  case 'GNF':
7665		  case 'JPY':
7666		  case 'KMF':
7667		  case 'KRW':
7668		  case 'MGA':
7669		  case 'PYG':
7670		  case 'RWF':
7671		  case 'UGX':
7672		  case 'VND':
7673		  case 'VUV':
7674		  case 'XAF':
7675		  case 'XOF':
7676		  case 'XPF':
7677			amount = Math.floor(amount); // Remove cents
7678			break;
7679		  default:
7680			amount = Math.round(amount * 100); // In cents
7681			break;
7682		}
7683		return amount;
7684	  }
7685
7686	  /**
7687	   * @since 1.16.0
7688	   * @access protected
7689	   *
7690	   * @param {Event} event
7691	   */
7692	  onClickPaymentRequestButton(event) {
7693		if (!this.isValidAcceptTerms()) {
7694		  event.preventDefault();
7695		  return;
7696		}
7697		const order = this.cart.getOrder();
7698		let totalPrice = parseFloat(order.total);
7699		if (this.cart.paymentDetails.deposit) {
7700		  totalPrice = parseFloat(order.deposit);
7701		}
7702		const totalPriceFormatted = this.convertToSmallestUnit(totalPrice, mpapp().settings().getCurrency().toLowerCase());
7703		this.methods['card'].paymentRequest.update({
7704		  total: {
7705			amount: totalPriceFormatted,
7706			label: __('Total', 'motopress-appointment'),
7707			pending: false
7708		  }
7709		});
7710	  }
7711
7712	  /**
7713	   * @since 1.5.0
7714	   * @access protected
7715	   *
7716	   * @param {Event} event
7717	   */
7718	  onChange(event) {
7719		this.haveErrors = !!event.error;
7720		if (this.haveErrors) {
7721		  this.view.showError(event.error.message);
7722		} else {
7723		  this.view.hideErrors();
7724		}
7725	  }
7726
7727	  /**
7728	   * @since 1.5.0
7729	   * @access protected
7730	   *
7731	   * @param {Object} $mountWrapper
7732	   */
7733	  mount($mountWrapper) {
7734		this.ready().then(() => {
7735		  this.view.mount($mountWrapper);
7736		  this.addListeners();
7737		});
7738	  }
7739
7740	  /**
7741	   * @since 1.5.0
7742	   *
7743	   * @param {Cart} cart
7744	   * @param {Object} bookingDetails Booking and payment ID/UID.
7745	   * @return {Promise}
7746	   */
7747	  processPayment(cart, bookingDetails) {
7748		if (!this.isValid()) {
7749		  return Promise.reject(new Error('The payment gateway is not valid.'));
7750		}
7751		this.$gatewayPreloader.removeClass('mpa-hide');
7752		let paymentMethod = this.view.selectedMethod;
7753		let paymentDetails = jQuery.extend({
7754		  payment_method: paymentMethod
7755		}, this.settings, bookingDetails);
7756		let processArgs = {
7757		  error_handler: this.view.showError.bind(this.view)
7758		};
7759		const processPayment = this.methods[paymentMethod].processPayment(cart, paymentDetails, processArgs);
7760		return processPayment.then(paymentData => {
7761		  this.$gatewayPreloader.addClass('mpa-hide');
7762		  return paymentData;
7763		}, error => {
7764		  this.$gatewayPreloader.addClass('mpa-hide');
7765		  throw error;
7766		});
7767	  }
7768
7769	  /**
7770	   * @since 1.5.0
7771	   * @access protected
7772	   *
7773	   * @return {Object}
7774	   */
7775	  getDefaults() {
7776		return jQuery.extend(super.getDefaults(), {
7777		  hide_postal_code: true,
7778		  locale: 'auto',
7779		  payment_methods: [],
7780		  public_key: '',
7781		  style: {}
7782		});
7783	  }
7784
7785	  /**
7786	   * @since 1.14.0
7787	   *
7788	   * @return {Object[]} Stripe payment methods
7789	   */
7790	  createPaymentMethods() {
7791		let methods = [];
7792		const stripeApi = Stripe(this.settings.public_key, {
7793		  apiVersion: "2022-11-15"
7794		});
7795		const stripeElements = stripeApi.elements({
7796		  locale: this.settings.locale
7797		});
7798		this.settings.payment_methods.forEach(method => {
7799		  switch (method) {
7800			case 'card':
7801			  methods['card'] = new CardMethod(stripeApi, stripeElements, this.settings);
7802			  methods['card'].getControl().on('change', this.onChange.bind(this));
7803
7804			  // todo: better to refactor
7805			  methods['card'].isCanMakePaymentRequest().then(canMakePayment => {
7806				if (canMakePayment) {
7807				  methods['card'].paymentRequest.on('token', async event => methods['card'].processPaymentRequestButton(event));
7808				  methods['card'].paymentRequest.on('cancel', () => {
7809					methods['card'].paymentRequestButtonEvent = null;
7810				  });
7811				  methods['card'].paymentRequestButton = methods['card'].createPaymentRequestButton();
7812				  methods['card'].paymentRequestButton.on('click', this.onClickPaymentRequestButton.bind(this));
7813				}
7814			  });
7815			  break;
7816			case 'sepa_debit':
7817			  methods['sepa_debit'] = new SepaDebitMethod(stripeApi, stripeElements, this.settings);
7818			  methods['sepa_debit'].getControl().on('change', this.onChange.bind(this));
7819			  break;
7820			case 'bancontact':
7821			  methods['bancontact'] = new BancontactMethod(stripeApi, stripeElements, this.settings);
7822			  break;
7823			case 'ideal':
7824			  methods['ideal'] = new IdealMethod(stripeApi, stripeElements, this.settings);
7825			  break;
7826			case 'giropay':
7827			  methods['giropay'] = new GiropayMethod(stripeApi, stripeElements, this.settings);
7828			  break;
7829			case 'sofort':
7830			  methods['sofort'] = new SofortMethod(stripeApi, stripeElements, this.settings);
7831			  break;
7832		  }
7833		});
7834		return methods;
7835	  }
7836
7837	  /**
7838	   * @since 1.6.2
7839	   */
7840	  reset() {
7841		// resetting each of methods
7842		Object.entries(this.methods).forEach(([methodName, method]) => {
7843		  method.reset();
7844		});
7845		this.view.reset();
7846	  }
7847	}
7848
7849	/**
7850	 * @since 1.6.0
7851	 */
7852	class PayPalGateway extends CustomizablePaymentGateway {
7853	  /**
7854	   * @since 1.6.0
7855	   */
7856	  setupProperties() {
7857		super.setupProperties();
7858
7859		/**
7860		 * @var {String}
7861		 */
7862		this.gatewayId = 'paypal';
7863	  }
7864
7865	  /**
7866	   * @since 1.6.0
7867	   */
7868	  enable() {
7869		super.enable();
7870		if (this.isEnabled) {
7871		  this.$mountWrapper.closest('form').find('.mpa-button-next').hide();
7872		}
7873	  }
7874
7875	  /**
7876	   * @since 1.6.0
7877	   */
7878	  disable() {
7879		super.disable();
7880		if (!this.isEnabled) {
7881		  this.$mountWrapper.closest('form').find('.mpa-button-next').show();
7882		}
7883	  }
7884
7885	  /**
7886	   * Validation checks Terms & Conditions checkbox. Checkbox must be checked.
7887	   * If validation crash, showing default html validation message.
7888	   *
7889	   * @return {Boolean}
7890	   *
7891	   * @access protected
7892	   *
7893	   * @since 1.14.0
7894	   */
7895	  isValidAcceptTerms() {
7896		if (!mpapp().settings().getTermsPageIdForAcceptance()) {
7897		  return true;
7898		}
7899		const acceptTermsMarkup = jQuery('.mpa-booking-step-payment .mpa-accept-terms')[0];
7900		if (!acceptTermsMarkup.checkValidity()) {
7901		  acceptTermsMarkup.reportValidity();
7902		  return false;
7903		}
7904		return true;
7905	  }
7906
7907	  /**
7908	   * @since 1.6.0
7909	   * @access protected
7910	   * @param {Object} $mountWrapper
7911	   */
7912	  mount($mountWrapper) {
7913		let self = this;
7914		self.$errorWrapper = $mountWrapper.find('.mpa-paypal-error');
7915		self.$gatewayPreloader = $mountWrapper.parent().find('.mpa-payment-gateway-title .mpa-preloader');
7916		paypal.Buttons({
7917		  onClick: function (data, actions) {
7918			if (0 === self.cart.getTotalPrice()) {
7919			  self.paypalDetails = {};
7920			  jQuery('.mpa-booking-step-payment .mpa-actions .mpa-button-next').trigger('click');
7921			}
7922
7923			// Rejects payment for invalid form field
7924			if (!self.isValidAcceptTerms()) {
7925			  return actions.reject();
7926			}
7927		  },
7928		  /**
7929		   * PayPal checkout server side integration.
7930		   * Creates PayPal order via REST API request from backend with a correct price.
7931		   *
7932		   * @param data
7933		   * @param actions
7934		   * @returns {Promise<string>} Order id
7935		   */
7936		  createOrder: function (data, actions) {
7937			self.$errorWrapper.addClass('mpa-hide');
7938			self.$gatewayPreloader.removeClass('mpa-hide');
7939			return mpa_rest_post('/payments/prepare', {
7940			  payment_details: self.cart.paymentDetails
7941			}).then(orderId => {
7942			  self.$gatewayPreloader.addClass('mpa-hide');
7943			  return orderId;
7944			});
7945		  },
7946		  onApprove: function (data, actions) {
7947			return actions.order.capture().then(function (details) {
7948			  self.paypalDetails = details;
7949			  jQuery('.mpa-booking-step-payment .mpa-actions .mpa-button-next').trigger('click');
7950			});
7951		  },
7952		  onCancel: function (data) {},
7953		  onError: function (error) {
7954			console.log(error);
7955			self.$errorWrapper.text(self.settings.paypal_error_message);
7956			self.$errorWrapper.removeClass('mpa-hide');
7957		  }
7958		}).render($mountWrapper.find('.mpa-paypal-container')[0]);
7959	  }
7960
7961	  /**
7962	   * @since 1.6.0
7963	   * @param {Cart} cart
7964	   * @param {Object} bookingDetails Booking and payment ID/UID.
7965	   * @return {Promise}
7966	   */
7967	  processPayment(cart, bookingDetails) {
7968		return Promise.resolve({
7969		  paypalDetails: this.paypalDetails
7970		});
7971	  }
7972	}
7973
7974	/**
7975	 * @since 1.5.0
7976	 */
7977	class GatewayFactory {
7978	  /**
7979	   * @since 1.5.0
7980	   * @since 1.6.0 Added cart parameter
7981	   *
7982	   * @param {Object} $paymentGateways
7983	   * @return {PaymentGateway[]}
7984	   */
7985	  static createGateways($paymentGateways, cart) {
7986		let activeGateways = {};
7987		for (let gatewayId of mpapp().settings().getActiveGateways()) {
7988		  let $billingFields = $paymentGateways.find('.mpa-' + gatewayId + '-payment-gateway .mpa-billing-fields');
7989		  let gateway = $billingFields.length !== 0 ? GatewayFactory.createGateway(gatewayId, $billingFields, cart) : null;
7990		  if (gateway !== null) {
7991			activeGateways[gatewayId] = gateway;
7992		  }
7993		}
7994		activeGateways['free'] = new FreeGateway({}, cart);
7995		return activeGateways;
7996	  }
7997
7998	  /**
7999	   * @since 1.5.0
8000	   * @since 1.6.0 Added cart parameter
8001	   * @access protected
8002	   *
8003	   * @return {PaymentGateway|null}
8004	   */
8005	  static createGateway(gatewayId, $billingFields, cart) {
8006		switch (gatewayId) {
8007		  case 'manual':
8008		  case 'test':
8009		  case 'cash':
8010		  case 'bank':
8011			return new PaymentGateway($billingFields, cart);
8012		  case 'paypal':
8013			return new PayPalGateway($billingFields, cart);
8014		  case 'stripe':
8015			return new StripeGateway($billingFields, cart);
8016		  default:
8017			return wp.hooks.applyFilters('mpa_create_gateway', null, gatewayId, $billingFields, cart);
8018		}
8019	  }
8020	}
8021
8022	/**
8023	 * @since 1.5.0
8024	 */
8025	class StepPayment extends AbstractStep {
8026	  /**
8027	   * @since 1.5.0
8028	   * @access protected
8029	   */
8030	  setupProperties() {
8031		super.setupProperties();
8032		this.lastCartHash = ''; // Track cart changes
8033
8034		this.gatewayId = '';
8035		this.gateways = {}; // {Gateway ID: Gateway object}
8036
8037		this.bookingDetails = {}; // Booking and payment ID/UID
8038
8039		this.$form = this.$element.find('.mpa-checkout-form');
8040		this.$order = this.$element.find('.mpa-order');
8041		this.$billingSection = this.$element.find('.mpa-billing-details');
8042		this.$paymentGateways = this.$billingSection.find('.mpa-payment-gateway');
8043		this.$paymentGatewayButtons = this.$paymentGateways.find('input[name="payment_gateway_id"]');
8044		this.$message = this.$element.find('.mpa-message').first();
8045		this.acceptTerms = false;
8046		this.onlinePayment = false;
8047		this.isDepositDisabled = false;
8048		this.$deposit = this.$element.find('.mpa-deposit-section');
8049		this.$depositSwitcher = this.$element.find('input[name="mpa-deposit-switcher"]');
8050		this.$depositTable = this.$element.find('#mpa-deposit-table');
8051		if (mpapp().settings().getTermsPageIdForAcceptance()) {
8052		  this.$acceptTerms = this.$element.find('.mpa-accept-terms');
8053		}
8054		this.couponSection = null;
8055	  }
8056
8057	  /**
8058	   * @since 1.5.0
8059	   * @access protected
8060	   *
8061	   * @return {String}
8062	   */
8063	  theId() {
8064		return 'payment';
8065	  }
8066
8067	  /**
8068	   * @since 1.5.0
8069	   * @access protected
8070	   *
8071	   * @return {Object} {Property name: Property settings}
8072	   */
8073	  propertiesSchema() {
8074		return {
8075		  gatewayId: {
8076			type: 'string',
8077			default: ''
8078		  },
8079		  isDepositDisabled: {
8080			type: 'bool',
8081			default: false
8082		  },
8083		  acceptTerms: {
8084			type: 'bool',
8085			default: false
8086		  }
8087		};
8088	  }
8089
8090	  /**
8091	   * @param {String} message
8092	   *
8093	   * @since 1.19.1
8094	   */
8095	  setErrorMessage(message) {
8096		this.$message.html(message);
8097		this.$message.toggleClass('mpa-hide', !message.trim().length);
8098	  }
8099
8100	  /**
8101	   * @since 1.19.1
8102	   */
8103	  clearErrorMessage() {
8104		this.setErrorMessage('');
8105	  }
8106
8107	  /**
8108	   * @since 1.14.0
8109	   */
8110	  hideDeposit() {
8111		this.$deposit.addClass('mpa-hide');
8112		this.$depositSwitcher.prop('disabled', true);
8113		this.isDepositDisabled = true;
8114	  }
8115
8116	  /**
8117	   * @since 1.14.0
8118	   */
8119	  showDeposit() {
8120		this.$deposit.removeClass('mpa-hide');
8121		this.$depositSwitcher.prop('disabled', false);
8122		this.setProperty('isDepositDisabled', this.$depositSwitcher.prop('checked'));
8123	  }
8124
8125	  /**
8126	   * @since 1.14.0
8127	   */
8128	  toggleDepositSection() {
8129		const order = this.cart.getOrder();
8130		const leftToPay = parseFloat(order.total) - parseFloat(order.deposit);
8131		if (leftToPay && this.onlinePayment) {
8132		  this.showDeposit();
8133		} else {
8134		  this.hideDeposit();
8135		}
8136	  }
8137
8138	  /**
8139	   * Set payment gateway name to payment details.
8140	   * Toggle deposit functions If it unavailable for setted gateway
8141	   *
8142	   * @param {string} gatewayId Payment gateway name
8143	   * @param {integer} isOnlinePayment 0 or 1
8144	   * @since 1.14.0
8145	   */
8146	  setGatewayId(gatewayId, isOnlinePayment) {
8147		this.setProperty('gatewayId', gatewayId);
8148		this.onlinePayment = parseInt(isOnlinePayment);
8149		this.toggleDepositSection();
8150		this.cart.setPaymentDetails({
8151		  gateway_id: this.gatewayId,
8152		  deposit: !this.isDepositDisabled
8153		});
8154	  }
8155
8156	  /**
8157	   * @since 1.5.0
8158	   * @access protected
8159	   */
8160	  addListeners() {
8161		super.addListeners();
8162
8163		// Disable submit
8164		this.$form.on('submit', event => false);
8165		this.$paymentGatewayButtons.on('change', event => {
8166		  this.setGatewayId(event.target.value, event.target.dataset.isOnlinePayment);
8167		});
8168		if (mpapp().settings().getTermsPageIdForAcceptance()) {
8169		  this.$acceptTerms.on('input', event => this.setProperty('acceptTerms', event.target.checked));
8170		}
8171		if (this.$depositSwitcher.length > 0) {
8172		  this.$depositSwitcher.on('input', event => {
8173			this.$depositTable.toggleClass('mpa-hide', event.target.checked);
8174			this.setProperty('isDepositDisabled', event.target.checked);
8175			this.cart.setPaymentDetails({
8176			  deposit: !this.isDepositDisabled
8177			});
8178		  });
8179		}
8180		this.$element.on('mpa_booking_step_disable', this.disable.bind(this));
8181		this.$element.on('mpa_booking_step_enable', this.enable.bind(this));
8182		this.$element.on('mpa_booking_coupon_applied', () => {
8183		  this.notifyCartChanged();
8184		  this.updateOrderDetails();
8185		  this.cart.setPaymentDetails({
8186			coupon_code: this.cart.hasCoupon() ? this.cart.coupon.getCode() : ''
8187		  });
8188		});
8189	  }
8190
8191	  /**
8192	   * @since 1.5.0
8193	   * @access protected
8194	   *
8195	   * @return {Promise}
8196	   */
8197	  loadEntities() {
8198		// show payment step before start loading.
8199		// preloader toggling in another place.
8200		if (!this.isLoaded) {
8201		  this.$element.removeClass('mpa-hide');
8202		}
8203		this.lastCartHash = this.cart.getHash('order');
8204
8205		// Initialize coupon section control
8206		if (mpapp().settings().isCouponsEnabled()) {
8207		  this.couponSection = new CouponSection(this.$element.find('.mpa-coupon-details'), this.cart);
8208		}
8209
8210		// Init/update the order
8211		this.updateOrderDetails();
8212
8213		// Load gateways
8214		let awaitLoading = [];
8215		if (this.gatewayId !== 'free') {
8216		  awaitLoading.push(this.loadGateways());
8217		} else {
8218		  this.loadGateways(); // Just load in the background
8219		}
8220
8221		// Draft posts
8222		awaitLoading.push(this.loadDrafts());
8223		return Promise.all(awaitLoading).then(() => {
8224		  // Actions after loaded all entities and inited gateways
8225
8226		  // init default gateway by :checked markup received from backend
8227		  // calling enable() for needle gateway
8228		  // which mount loaded control element to checkout markup
8229		  this.initDefaultGateway();
8230		  return this;
8231		});
8232	  }
8233
8234	  /**
8235	   * @since 1.5.0
8236	   * @return {Promise}
8237	   * @access protected
8238	   */
8239	  reload() {
8240		// Clear previous messages
8241		if (this.couponSection) {
8242		  this.couponSection.reset();
8243		}
8244		this.clearErrorMessage();
8245
8246		// Is the coupon still applicable to the cart?
8247		if (this.cart.hasCoupon()) {
8248		  this.cart.testCoupon();
8249		}
8250
8251		// Init/update the order
8252		this.updateOrderDetails();
8253
8254		// Maybe notify payment gateways about cart changes
8255		if (this.cart.didChange(this.lastCartHash, 'order')) {
8256		  this.lastCartHash = this.cart.getHash('order');
8257		  this.notifyCartChanged();
8258
8259		  // Update booking draft on the backend
8260		  return this.loadDrafts();
8261		}
8262		return Promise.resolve(this);
8263	  }
8264
8265	  /**
8266	   * @since 1.6.2
8267	   */
8268	  reset() {
8269		if (mpapp().settings().getTermsPageIdForAcceptance()) {
8270		  this.$acceptTerms.prop('checked', false);
8271		  this.resetProperty('acceptTerms');
8272		}
8273		this.lastCartHash = '';
8274
8275		// Enable default gateway
8276		let defaultGateway = mpapp().settings().getDefaultPaymentGateway();
8277		this.$paymentGatewayButtons.filter(':checked').prop('checked', false);
8278		if (defaultGateway in this.gateways) {
8279		  this.setProperty('gatewayId', defaultGateway);
8280		  this.$paymentGatewayButtons.filter('[value="' + defaultGateway + '"]').prop('checked', true);
8281		} else {
8282		  this.resetProperty('gatewayId');
8283		}
8284
8285		// Reset payment gateways
8286		for (let gatewayId in this.gateways) {
8287		  this.gateways[gatewayId].reset();
8288		}
8289
8290		// Reset coupon section
8291		if (this.couponSection) {
8292		  this.couponSection.reset();
8293		}
8294	  }
8295
8296	  /**
8297	   * @since 1.11.0
8298	   *
8299	   * @access protected
8300	   */
8301	  notifyCartChanged() {
8302		for (let gatewayId in this.gateways) {
8303		  this.gateways[gatewayId].onCartChange(this.cart);
8304		}
8305	  }
8306
8307	  /**
8308	   * @since 1.5.0
8309	   * @access protected
8310	   */
8311	  updateOrderDetails() {
8312		this.$order.empty();
8313		this.$order.html(mpa_tmpl_order(this.cart.getOrder()));
8314		if (this.$depositTable.length > 0) {
8315		  const depositTableHtml = mpa_tmpl_deposit(this.cart.getOrder());
8316		  this.$depositTable.html(depositTableHtml);
8317		  if (this.$paymentGatewayButtons.filter(':checked').length > 0) {
8318			this.toggleDepositSection();
8319		  }
8320		}
8321
8322		// Bind remove coupon button
8323		let $removeCoupon = this.$order.find('.mpa-remove-coupon');
8324		if ($removeCoupon.length > 0) {
8325		  $removeCoupon.on('click', this.removeCoupon.bind(this));
8326		}
8327		this.toggleAvailablePaymentMethods();
8328	  }
8329
8330	  /**
8331	   * @since 1.11.0
8332	   *
8333	   * @access protected
8334	   *
8335	   * @param {Event} event
8336	   */
8337	  removeCoupon(event) {
8338		event.preventDefault();
8339		event.stopPropagation();
8340		this.cart.removeCoupon();
8341		this.couponSection.clearMessage();
8342		this.cart.setPaymentDetails({
8343		  coupon_code: ''
8344		});
8345		this.notifyCartChanged();
8346		this.updateOrderDetails();
8347	  }
8348
8349	  /**
8350	   * Hide unavailable payment methods for order.
8351	   *
8352	   * @since 1.14.0
8353	   * @access protected
8354	   */
8355	  toggleAvailablePaymentMethods() {
8356		const isFreeOrder = this.cart.getTotalPrice() === 0;
8357		if (isFreeOrder) {
8358		  this.setGatewayId('free', false);
8359		} else {
8360		  const $currentGateway = this.$paymentGatewayButtons.filter(':checked');
8361		  if ($currentGateway.length > 0) {
8362			this.setGatewayId($currentGateway[0].value, $currentGateway[0].dataset.isOnlinePayment);
8363		  }
8364		}
8365		this.$billingSection.toggleClass('mpa-hide', isFreeOrder);
8366		this.$paymentGatewayButtons.prop('required', !isFreeOrder);
8367	  }
8368
8369	  /**
8370	   * @since 1.5.0
8371	   * @access protected
8372	   *
8373	   * @return {Promise[]}
8374	   */
8375	  loadGateways() {
8376		let $paymentGateways = this.$billingSection.find('.mpa-payment-gateways');
8377		this.gateways = GatewayFactory.createGateways($paymentGateways, this.cart);
8378		let awaitGateways = [];
8379		for (let gatewayId in this.gateways) {
8380		  awaitGateways.push(this.gateways[gatewayId].load());
8381		}
8382		return awaitGateways;
8383	  }
8384
8385	  /**
8386	   * @since 1.5.0
8387	   * @access protected
8388	   *
8389	   * @return {Promise}
8390	   */
8391	  loadDrafts() {
8392		let cartData = this.cart.toArray();
8393		cartData.payment = true;
8394		return mpa_rest_post('/bookings/draft', cartData).then(drafts => {
8395		  this.bookingDetails = drafts;
8396		  /**
8397		   * @todo: param paymentDetails[payment_id] need only for backward compatibility with a mpa-woocommerce addon v1.0.0
8398		   */
8399		  const paymentDetails = {
8400			booking_id: drafts.booking_id,
8401			payment_id: drafts.payment_id
8402		  };
8403		  this.cart.setPaymentDetails(paymentDetails);
8404		}, error => {
8405		  this.setErrorMessage(error.message);
8406		}).then(() => {
8407		  this.enableGateways();
8408		  return this;
8409		});
8410	  }
8411
8412	  /**
8413	   * @since 1.5.0
8414	   * @access protected
8415	   */
8416	  enableGateways() {
8417		this.$paymentGatewayButtons.prop('disabled', false);
8418	  }
8419
8420	  /**
8421	   * @since 1.15.0
8422	   * @access protected
8423	   */
8424	  initDefaultGateway() {
8425		let $defaultGateway = this.$paymentGatewayButtons.filter(':checked');
8426		if ($defaultGateway.length > 0) {
8427		  this.gateways[$defaultGateway.val()].enable();
8428		}
8429	  }
8430
8431	  /**
8432	   * @since 1.5.0
8433	   *
8434	   * @return {Boolean}
8435	   */
8436	  isValidInput() {
8437		return this.isValidGatewayId() && this.isValidGateway() && this.isValidAcceptTerms();
8438	  }
8439
8440	  /**
8441	   * @since 1.5.0
8442	   * @access protected
8443	   *
8444	   * @return {Boolean} 
8445	   */
8446	  isValidGatewayId() {
8447		return this.gatewayId !== '';
8448	  }
8449
8450	  /**
8451	   * @since 1.5.0
8452	   *
8453	   * @access protected
8454	   *
8455	   * @return {Boolean}
8456	   */
8457	  isValidGateway() {
8458		if (this.gatewayId in this.gateways) {
8459		  return this.gateways[this.gatewayId].isValid();
8460		} else {
8461		  return true;
8462		}
8463	  }
8464
8465	  /**
8466	   * @return {Boolean}
8467	   *
8468	   * @access protected
8469	   *
8470	   * @since 1.10.2
8471	   */
8472	  isValidAcceptTerms() {
8473		return !mpapp().settings().getTermsPageIdForAcceptance() || this.acceptTerms;
8474	  }
8475
8476	  /**
8477	   * @since 1.5.0
8478	   * @access protected
8479	   *
8480	   * @param {String} propertyName
8481	   * @param {*} newValue
8482	   * @param {*} oldValue
8483	   */
8484	  afterUpdate(propertyName, newValue, oldValue) {
8485		// Toggle gateways
8486		if (oldValue in this.gateways) {
8487		  this.gateways[oldValue].disable();
8488		}
8489		if (newValue in this.gateways) {
8490		  this.gateways[newValue].enable();
8491		}
8492	  }
8493
8494	  /**
8495	   * @since 1.5.0
8496	   * @access protected
8497	   */
8498	  react() {
8499		super.react();
8500
8501		// Always enable submit button to allow validation messages
8502		this.$buttonNext.prop('disabled', false);
8503	  }
8504
8505	  /**
8506	   * @since 1.5.0
8507	   * @access protected
8508	   *
8509	   * @return {undefined|Promise}
8510	   */
8511	  maybeSubmit() {
8512		// Disable coupons section
8513		if (this.couponSection) {
8514		  this.couponSection.disable();
8515		}
8516		if (this.gatewayId in this.gateways) {
8517		  let gateway = this.gateways[this.gatewayId];
8518		  let result = gateway.processPayment(this.cart, this.bookingDetails);
8519
8520		  // If a Promise
8521		  if (typeof result === 'object' && typeof result.then === 'function') {
8522			result.then(paymentData => {
8523			  this.cart.setPaymentDetails(paymentData);
8524			  return paymentData;
8525			}, error => {
8526			  this.setErrorMessage(error.message);
8527			}
8528
8529			// Leave errors to AbstractStep.submit() or re-throw it, so
8530			// AbstractStep may cancel submission
8531			);
8532		  }
8533
8534		  return result;
8535		}
8536	  }
8537
8538	  /**
8539	   * @since 1.11.0
8540	   *
8541	   * @access protected
8542	   */
8543	  cancelSubmission() {
8544		super.cancelSubmission();
8545		if (this.couponSection) {
8546		  this.couponSection.enable();
8547		}
8548	  }
8549	}
8550
8551	/**
8552	 * @param {Object} element jQuery or DOM element.
8553	 * @param {Object} args
8554	 * @return {Object}
8555	 *
8556	 * @since 1.2.1
8557	 */
8558	function mpa_flatpickr(element, args) {
8559	  let locale = args.locale || mpapp().settings().getFlatpickrLocale();
8560	  let l10n = flatpickr.l10ns[locale] || locale;
8561
8562	  // Use first day of the week value from Settings > General > Week Starts On
8563	  if (typeof l10n == 'object') {
8564		l10n.firstDayOfWeek = mpapp().settings().getFirstDayOfWeek();
8565	  }
8566
8567	  // Merge args
8568	  let defaultArgs = {
8569		formatDate: mpa_format_date,
8570		inline: true,
8571		locale: l10n,
8572		monthSelectorType: 'static',
8573		showMonths: 1
8574	  };
8575	  args = jQuery.extend({}, defaultArgs, args);
8576
8577	  // Create instance
8578	  let instance = null;
8579	  if (element instanceof jQuery) {
8580		instance = flatpickr(element[0], args);
8581	  } else {
8582		instance = flatpickr(element, args);
8583	  }
8584	  return instance;
8585	}
8586
8587	/**
8588	 * @since 1.0
8589	 */
8590	class StepPeriod extends AbstractStep {
8591	  /**
8592	   * @access protected
8593	   *
8594	   * @since 1.0
8595	   */
8596	  setupProperties() {
8597		super.setupProperties();
8598		this.cartItem = null;
8599		this.lastHash = ''; // Track cart changes
8600
8601		this.monthSlots = {}; // {'YYYY-MM': {'Y-m-d': Time slots}}, where time slots
8602		// is {Time period: Array of [Employee ID, Location ID]}.
8603		// For example: {'10:00 - 11:00': [[19, 21], [89, 21]], ...}
8604
8605		this.date = '';
8606		this.time = '';
8607		this.datepicker = null;
8608		this.$dateWrapper = this.$element.find('.mpa-date-wrapper');
8609		this.$dateInput = this.$element.find('.mpa-date');
8610		this.$timeWrapper = this.$element.find('.mpa-time-wrapper');
8611		this.$times = this.$timeWrapper.find('.mpa-times');
8612
8613		/**
8614		 * Recursion counters
8615		 * @since 1.19.0
8616		 */
8617		this.selectFirstDateTimeSlotRecursionCounter = 0;
8618		this.selectFirstDateTimeSlotRecursionCounterMax = 12;
8619		this.isAlreadySelectedFirstDateTimeSlotAfterFirstRenderCalendar = false;
8620	  }
8621
8622	  /**
8623	   * @return {String}
8624	   *
8625	   * @access protected
8626	   *
8627	   * @since 1.0
8628	   */
8629	  theId() {
8630		return 'period';
8631	  }
8632
8633	  /**
8634	   * @return {String} 'cart'|'cart item'
8635	   *
8636	   * @since 1.9.0
8637	   */
8638	  getCartContext() {
8639		return 'cart item';
8640	  }
8641
8642	  /**
8643	   * @return {Object}
8644	   *
8645	   * @access protected
8646	   *
8647	   * @since 1.0
8648	   */
8649	  propertiesSchema() {
8650		return {
8651		  date: {
8652			type: 'string',
8653			default: ''
8654		  },
8655		  time: {
8656			type: 'string',
8657			default: ''
8658		  }
8659		};
8660	  }
8661
8662	  /**
8663	   * @access protected
8664	   *
8665	   * @since 1.0
8666	   */
8667	  addListeners() {
8668		super.addListeners();
8669		this.$dateInput.on('change', event => this.setProperty('date', event.target.value));
8670	  }
8671
8672	  /**
8673	   * @return {Promise}
8674	   *
8675	   * @access protected
8676	   *
8677	   * @since 1.0
8678	   */
8679	  loadEntities() {
8680		this.cartItem = this.cart.getActiveItem();
8681		this.lastHash = this.cartItem.getHash('availability');
8682		return Promise.resolve(this);
8683	  }
8684
8685	  /**
8686	   * @access protected
8687	   *
8688	   * @since 1.0
8689	   */
8690	  reload() {
8691		// Did something changed?
8692		if (this.cartItem.didChange(this.lastHash, 'availability')) {
8693		  // Reset all and reload
8694		  this.$element.removeClass('mpa-loaded');
8695		  this.resetDate();
8695 // This also resets the time
8696		  this.readyPromise = this.loadEntities();
8697
8698		  // Reset saved time slots
8699		  this.monthSlots = {};
8700
8701		  // Re-enable days
8702		  if (this.datepicker != null) {
8703			this.setEnabledDays([]);
8704			this.readyPromise.finally(() => this.resetEnabledDays());
8705		  }
8706		  return this.readyPromise;
8707		} else {
8708		  return Promise.resolve(this);
8709		}
8710	  }
8711
8712	  /**
8713	   * Reset step before a new item.
8714	   *
8715	   * @since 1.4.0
8716	   */
8717	  reset() {
8718		this.cartItem = this.cart.getActiveItem();
8719		this.lastHash = '';
8720		this.monthSlots = {};
8721		this.resetDate();
8722	  }
8723
8724	  /**
8725	   * @return {Boolean}
8726	   *
8727	   * @since 1.0
8728	   */
8729	  isValidInput() {
8730		return this.date != '' && this.time != '';
8731	  }
8732
8733	  /**
8734	   * @access protected
8735	   *
8736	   * @since 1.0
8737	   */
8738	  resetDate() {
8739		// Will also reset the time in afterUpdate()
8740		this.resetProperty('date');
8741	  }
8742
8743	  /**
8744	   * @access protected
8745	   *
8746	   * @since 1.0
8747	   */
8748	  resetTime() {
8749		this.$times.empty();
8750		this.resetProperty('time');
8751	  }
8752
8753	  /**
8754	   * @param {Array}
8755	   *
8756	   * @since 1.0
8757	   */
8758	  setEnabledDays(allowedDays) {
8759		if (!mpa_empty(allowedDays, true)) {
8760		  this.datepicker.set('enable', allowedDays);
8761		} else {
8762		  // Just a date in the past to disable all days. With [] Flatpickr
8763		  // allows all dates and disables nothing
8764		  this.datepicker.set('enable', ['2000-01-01']);
8765		}
8766	  }
8767
8768	  /**
8769	   * Executed only when the new value is different from the old one.
8770	   *
8771	   * @param {String} propertyName
8772	   * @param {*} newValue
8773	   * @param {*} oldValue
8774	   *
8775	   * @access protected
8776	   *
8777	   * @since 1.0
8778	   */
8779	  afterUpdate(propertyName, newValue, oldValue) {
8780		if (propertyName == 'date') {
8781		  if (newValue == '') {
8782			this.resetTime();
8783		  } else {
8784			this.resetTimeSlots();
8785		  }
8786		}
8787	  }
8788
8789	  /**
8790	   * @access protected
8791	   *
8792	   * @since 1.0
8793	   */
8794	  react() {
8795		super.react();
8796
8797		// Toggle time slots
8798		this.$timeWrapper.toggleClass('mpa-hide', this.date == '');
8799	  }
8800
8801	  /**
8802	   * @access protected
8803	   *
8804	   * @since 1.0
8805	   */
8806	  showReady() {
8807		super.showReady();
8808
8809		// Show datepicker
8810		if (this.datepicker == null) {
8811		  this.showDatepicker();
8812
8813		  // Enable days for current month
8814		  this.resetEnabledDays();
8815		}
8816	  }
8817
8818	  /**
8819	   * @access protected
8820	   *
8821	   * @since 1.0
8822	   */
8823	  showDatepicker() {
8824		this.datepicker = mpa_flatpickr(this.$dateInput, this.getDatepickerArgs());
8825	  }
8826
8827	  /**
8828	   * @access protected
8829	   */
8830	  getDatepickerArgs() {
8831		return {
8832		  minDate: mpapp().settings().getBusinessDate(),
8833		  // Don't allow to go to
8834		  // the previous months
8835		  onMonthChange: () => this.resetEnabledDays()
8836		};
8837	  }
8838
8839	  /**
8840	   * @access protected
8841	   *
8842	   * @since 1.0
8843	   */
8844	  maybeSubmit() {
8845		let cartItem = this.cartItem;
8846		cartItem.date = mpa_parse_date(this.date);
8847		cartItem.time = new TimePeriod(this.time);
8848
8849		// Autoselect employee/location, if needed
8850		if (cartItem.employee === null || cartItem.location === null) {
8851		  let autoselectedIds = this.autoselectIds();
8852		  let autoEmployeeId = autoselectedIds[0];
8853		  let autoLocationId = autoselectedIds[1];
8854		  if (cartItem.employee === null) {
8855			cartItem.setEmployee(autoEmployeeId, 'no-reset');
8856		  }
8857		  if (cartItem.location === null) {
8858			cartItem.setLocation(autoLocationId, 'no-reset');
8859		  }
8860		}
8861		document.dispatchEvent(new CustomEvent('mpa_add_to_cart', {
8862		  detail: {
8863			cartItem: cartItem,
8864			currencyCode: mpapp().settings().getCurrency()
8865		  }
8866		}));
8867		document.dispatchEvent(new CustomEvent('mpa_view_cart', {
8868		  detail: {
8869			cart: this.cart,
8870			currencyCode: mpapp().settings().getCurrency()
8871		  }
8872		}));
8873	  }
8874
8875	  /**
8876	   * After loading time slots of month, we set the first available timeslot as the initial value of the calendar.
8877	   *
8878	   * @since 1.19.0
8879	   */
8880	  selectFirstDateTimeSlot() {
8881		let year = this.datepicker.currentYear;
8882		let month = this.datepicker.currentMonth;
8883		let monthKey = this.getMonthKey(year, month);
8884		const slotsForThisMonth = this.monthSlots[monthKey];
8885		if (slotsForThisMonth && Object.keys(slotsForThisMonth).length > 0) {
8886		  // Get the first available date and time
8887		  const firstDate = Object.keys(slotsForThisMonth)[0];
8888		  const firstTimeSlot = Object.keys(slotsForThisMonth[firstDate])[0];
8889
8890		  // Programmatically set the date value for Flatpickr and call the change handler
8891		  this.datepicker.setDate(firstDate, true);
8892		  const targetTimeElement = this.$times.children('.mpa-time-period').filter((index, elem) => {
8893			return elem.getAttribute('date-time') === firstTimeSlot;
8894		  });
8895		  targetTimeElement.trigger('click');
8896		  this.isAlreadySelectedFirstDateTimeSlotAfterFirstRenderCalendar = true;
8897		} else {
8898		  // Make scroll of calendar for first render after loading
8899		  // and skip scroll of calendar for calendar scroll by user actions
8900		  if (this.isAlreadySelectedFirstDateTimeSlotAfterFirstRenderCalendar === true) {
8901			return;
8902		  }
8903
8904		  // Prevent recursive loop
8905		  if (this.selectFirstDateTimeSlotRecursionCounter >= this.selectFirstDateTimeSlotRecursionCounterMax) {
8906			this.datepicker.changeMonth(-this.selectFirstDateTimeSlotRecursionCounter);
8907			this.isAlreadySelectedFirstDateTimeSlotAfterFirstRenderCalendar = true;
8908			return;
8909		  }
8910		  this.selectFirstDateTimeSlotRecursionCounter += 1;
8911
8912		  // If no available slots for the current month, move to the next month
8913		  this.datepicker.changeMonth(1);
8914
8915		  // Reload timeslots for new month and then again call resetEnabledDays() => selectFirstDateTimeSlot() method to try again with the next month
8916		  // To be careful. This is recursion.
8917		  this.reload();
8918		}
8919	  }
8920
8921	  /**
8922	   * @return {Number[]} [Employee ID, Location ID]
8923	   *
8924	   * @access protected
8925	   *
8926	   * @since 1.2.1
8927	   */
8928	  autoselectIds() {
8929		let ids = [0, 0];
8930		let monthKey = this.getCurrentMonthKey();
8931		if (this.monthSlots[monthKey] && this.monthSlots[monthKey][this.date]) {
8932		  let timeSlots = this.monthSlots[monthKey][this.date];
8933
8934		  // Search for the proper period
8935		  for (let timeStr in timeSlots) {
8936			if (timeStr === this.time) {
8937			  let employees = timeSlots[timeStr];
8938
8939			  // Get the first pair
8940			  ids[0] = employees[0][0]; // Copy values, not
8941			  ids[1] = employees[0][1]; // the array pointer
8942
8943			  break;
8944			}
8945		  }
8946		}
8947		return ids;
8948	  }
8949
8950	  /**
8951	   * @access protected
8952	   *
8953	   * @since 1.0
8954	   */
8955	  resetEnabledDays() {
8956		// Reset date & time
8957		this.resetDate();
8957 // This also resets the time
8958
8959		// Disable all days
8960		this.setEnabledDays([]);
8961
8962		// Add preloader
8963		this.$dateWrapper.removeClass('mpa-loaded');
8964
8965		// Get new enabled days
8966		let year = this.datepicker.currentYear;
8967		let month = this.datepicker.currentMonth;
8968		let monthKey = this.getMonthKey(year, month);
8969
8970		// Query time slots
8971		let queryPromise = null;
8972		if (!this.monthSlots[monthKey]) {
8973		  // Query slots for new month
8974		  let serviceId = this.cartItem.service.id;
8975		  let dateFrom = new Date(year, month, 1);
8976		  let dateTo = new Date(year, month + 1, 0);
8977		  queryPromise = mpa_time_slots(serviceId, dateFrom, dateTo, this.getTimeSlotsQueryArgs());
8978		} else {
8979		  // Get already loaded month
8980		  queryPromise = Promise.resolve(this.monthSlots[monthKey]);
8981		}
8982
8983		// Update enabled days
8984		queryPromise.then(daySlots => {
8985		  // Save slots for future
8986		  this.monthSlots[monthKey] = daySlots;
8987		  this.setEnabledDays(Object.keys(daySlots));
8988		  this.$dateWrapper.addClass('mpa-loaded');
8989		  this.selectFirstDateTimeSlot();
8990		});
8991	  }
8992
8993	  /**
8994	   * @access protected
8995	   */
8996	  getTimeSlotsQueryArgs() {
8997		return {
8998		  employees: this.cartItem.getAvailableEmployeeIds(),
8999		  locations: this.cartItem.getAvailableLocationIds(),
9000		  exclude: this.cart.toArray('items')
9001		};
9002	  }
9003
9004	  /**
9005	   * @access protected
9006	   *
9007	   * @since 1.0
9008	   */
9009	  resetTimeSlots() {
9010		// Clear time
9011		this.resetTime();
9012
9013		// Get time slots
9014		let timeSlots = {};
9015		let monthKey = this.getCurrentMonthKey();
9016		if (this.monthSlots[monthKey][this.date] != undefined) {
9017		  timeSlots = this.monthSlots[monthKey][this.date];
9018		}
9019
9020		// Add buttons to the $times
9021		let slotsAdded = 0;
9022		for (let timeStr in timeSlots) {
9023		  let period = new TimePeriod(timeStr); // '10:00 - 11:00'
9024
9025		  // '10:00<span class="mpa-period-end-time"> - 11:00</span>', so we
9026		  // can use CSS to hide second part in narrow columns
9027		  let periodLabel = period.toString('public', '<span class="mpa-period-end-time"> - ') + '</span>';
9028
9029		  // Add button
9030		  let periodButton = mpa_tmpl_button(periodLabel, {
9031			'class': 'button button-secondary mpa-time-period',
9032			'date-time': timeStr
9033		  });
9034		  this.$times.append(periodButton);
9035		  slotsAdded++;
9036		}
9037
9038		// Add listeners or error message
9039		if (slotsAdded > 0) {
9040		  this.$times.children('.mpa-time-period').on('click', event => this.onTime(event, event.currentTarget));
9041		} else {
9042		  this.$times.text(__('Sorry, but we were unable to allocate time slots for the date you selected.', 'motopress-appointment'));
9043		}
9044	  }
9045
9046	  /**
9047	   * @param {Number} year
9048	   * @param {Number} month
9049	   * @return {String}
9050	   *
9051	   * @access protected
9052	   *
9053	   * @since 1.2.1
9054	   */
9055	  getMonthKey(year, month) {
9056		if (month <= 8) {
9057		  // '2021-04'
9058		  return year + '-0' + (month + 1);
9059		} else {
9060		  // '2021-10'
9061		  return year + '-' + (month + 1);
9062		}
9063	  }
9064
9065	  /**
9066	   * @return {String}
9067	   *
9068	   * @access protected
9069	   *
9070	   * @since 1.2.1
9071	   */
9072	  getCurrentMonthKey() {
9073		if (this.date !== '') {
9074		  let date = mpa_parse_date(this.date);
9075		  return this.getMonthKey(date.getFullYear(), date.getMonth());
9076		} else {
9077		  return '2000-01';
9078		}
9079	  }
9080
9081	  /**
9082	   * @param {Event} event
9083	   * @param {Button} button Clicked button.
9084	   *
9085	   * @access protected
9086	   *
9087	   * @since 1.0
9088	   */
9089	  onTime(event, button) {
9090		// Toggle class ".mpa-time-period-selected"
9091		this.$times.children('.mpa-time-period-selected').removeClass('mpa-time-period-selected');
9092		button.classList.add('mpa-time-period-selected');
9093
9094		// Update time property
9095		this.setProperty('time', button.getAttribute('date-time'));
9096	  }
9097	}
9098
9099	/**
9100	 * @since 1.0
9101	 */
9102	class StepServiceForm extends AbstractStep {
9103	  /**
9104	   * @access protected
9105	   *
9106	   * @since 1.0
9107	   */
9108	  setupProperties() {
9109		super.setupProperties();
9110		this.availability = new AvailabilityService();
9111		this.category = '';
9112		this.serviceId = 0;
9113		this.employeeId = 0;
9114		this.locationId = 0;
9115		this.isHiddenStep = true;
9116		this.$form = this.$element.find('.mpa-service-form');
9117		this.$categories = this.$element.find('.mpa-service-category-wrapper');
9118		this.$services = this.$element.find('.mpa-service-wrapper');
9119		this.$employees = this.$element.find('.mpa-employee-wrapper');
9120		this.$locations = this.$element.find('.mpa-location-wrapper');
9121		this.$selects = this.$element.find('.mpa-input-wrapper select');
9122		this.$categoriesSelect = this.$selects.filter('.mpa-service-category');
9123		this.$servicesSelect = this.$selects.filter('.mpa-service');
9124		this.$employeesSelect = this.$selects.filter('.mpa-employee');
9125		this.$locationsSelect = this.$selects.filter('.mpa-location');
9126		this.unselectedServiceText = this.$servicesSelect.children('[value=""]').text();
9127		this.unselectedOptionText = this.$selects.filter('.mpa-optional-select').first().find('option:first').text();
9128	  }
9129
9130	  /**
9131	   * @return {String}
9132	   *
9133	   * @access protected
9134	   *
9135	   * @since 1.0
9136	   */
9137	  theId() {
9138		return 'service-form';
9139	  }
9140
9141	  /**
9142	   * @return {String} 'cart'|'cart item'
9143	   *
9144	   * @since 1.9.0
9145	   */
9146	  getCartContext() {
9147		return 'cart item';
9148	  }
9149
9150	  /**
9151	   * @return {Object}
9152	   *
9153	   * @access protected
9154	   *
9155	   * @since 1.0
9156	   */
9157	  propertiesSchema() {
9158		return {
9159		  category: {
9160			type: 'string',
9161			default: ''
9162		  },
9163		  serviceId: {
9164			type: 'integer',
9165			default: 0
9166		  },
9167		  employeeId: {
9168			type: 'integer',
9169			default: 0
9170		  },
9171		  locationId: {
9172			type: 'integer',
9173			default: 0
9174		  }
9175		};
9176	  }
9177
9178	  /**
9179	   * @access protected
9180	   *
9181	   * @since 1.0
9182	   */
9183	  addListeners() {
9184		super.addListeners();
9185		this.$form.on('submit', this.submitForm.bind(this));
9186		this.$categoriesSelect.on('change', event => this.setProperty('category', event.target.value));
9187		this.$servicesSelect.on('change', event => this.setProperty('serviceId', event.target.value));
9188		this.$employeesSelect.on('change', event => this.setProperty('employeeId', event.target.value));
9189		this.$locationsSelect.on('change', event => this.setProperty('locationId', event.target.value));
9190	  }
9191	  isHiddenElementByProp($element) {
9192		const isHidden = $element.attr('data-is-hidden');
9193		if (typeof isHidden !== 'undefined' && isHidden !== 'false') {
9194		  return true;
9195		}
9196		return false;
9197	  }
9198	  initCategoriesSelect() {
9199		if (this.$categoriesSelect.length == 0) {
9200		  return; // The select is disabled by parameter show_*
9201		}
9202
9203		this.updateCategorySchema();
9204		let value = this.$categoriesSelect.val();
9205		let isHiddenSelect = this.isHiddenElementByProp(this.$servicesSelect);
9206		if (this.$categoriesSelect.attr('data-default')) {
9207		  const defaultValue = this.$categoriesSelect.attr('data-default');
9208		  if (this.isValidCategoryBySchema(defaultValue)) {
9209			value = defaultValue;
9210		  } else {
9211			isHiddenSelect = false;
9212		  }
9213		}
9214		this.setProperty('category', value);
9215		this.renderCategorySelect();
9216		if (!isHiddenSelect) {
9217		  this.isHiddenStep = false;
9218		}
9219		this.$categories.toggleClass('mpa-hide', isHiddenSelect);
9220	  }
9221	  initServicesSelect() {
9222		if (this.$servicesSelect.length == 0) {
9223		  return; // The select is disabled by parameter show_*
9224		}
9225
9226		this.updateServiceSchema();
9227		let value = this.$servicesSelect.val();
9228		let isHiddenSelect = this.isHiddenElementByProp(this.$servicesSelect);
9229		if (this.$servicesSelect.attr('data-default')) {
9230		  const defaultValue = mpa_intval(this.$servicesSelect.attr('data-default'));
9231		  if (this.isValidServiceBySchema(defaultValue)) {
9232			value = defaultValue;
9233		  } else {
9234			isHiddenSelect = false;
9235		  }
9236		}
9237		this.setProperty('serviceId', value);
9238		this.renderServiceSelect();
9239		if (!isHiddenSelect) {
9240		  this.isHiddenStep = false;
9241		}
9242		this.$services.toggleClass('mpa-hide', isHiddenSelect);
9243	  }
9244	  initEmployeesSelect() {
9245		if (this.$employeesSelect.length == 0) {
9246		  return; // The select is disabled by parameter show_*
9247		}
9248
9249		this.updateEmployeeSchema();
9250		let value = this.$employeesSelect.val();
9251		let isHiddenSelect = this.isHiddenElementByProp(this.$employeesSelect);
9252		if (this.$employeesSelect.attr('data-default')) {
9253		  const defaultValue = mpa_intval(this.$employeesSelect.attr('data-default'));
9254		  if (this.isValidEmployeeBySchema(defaultValue)) {
9255			value = defaultValue;
9256		  } else {
9257			isHiddenSelect = false;
9258		  }
9259		}
9260		this.setProperty('employeeId', value);
9261		this.renderEmployeeSelect();
9262		if (!isHiddenSelect) {
9263		  this.isHiddenStep = false;
9264		}
9265		this.$employees.toggleClass('mpa-hide', isHiddenSelect);
9266	  }
9267	  initLocationsSelect() {
9268		if (this.$locationsSelect.length == 0) {
9269		  return; // The select is disabled by parameter show_*
9270		}
9271
9272		this.updateLocationSchema();
9273		let value = this.$locationsSelect.val();
9274		let isHiddenSelect = this.isHiddenElementByProp(this.$locationsSelect);
9275		if (this.$locationsSelect.attr('data-default')) {
9276		  const defaultValue = mpa_intval(this.$locationsSelect.attr('data-default'));
9277		  if (this.isValidLocationBySchema(defaultValue)) {
9278			value = defaultValue;
9279		  } else {
9280			isHiddenSelect = false;
9281		  }
9282		}
9283		this.setProperty('locationId', value);
9284		this.renderLocationSelect();
9285		if (!isHiddenSelect) {
9286		  this.isHiddenStep = false;
9287		}
9288		this.$locations.toggleClass('mpa-hide', isHiddenSelect);
9289	  }
9290
9291	  /**
9292	   * @return {Promise}
9293	   *
9294	   * @access protected
9295	   *
9296	   * @since 1.0
9297	   */
9298	  loadEntities() {
9299		return this.availability.ready().finally(() => {
9300		  // Hide other empty selects
9301		  // or hide selects with the "data-is-hidden" setting, excluding unselected and required selects.
9302		  this.initServicesSelect();
9303		  this.initCategoriesSelect(); // needs to be initialized after this.initServicesSelect(), because hiding the field depends on hiding the services select field.
9304		  this.initEmployeesSelect();
9305		  this.initLocationsSelect();
9306		  return this;
9307		});
9308	  }
9309
9310	  /**
9311	   * Reset step before a new item.
9312	   *
9313	   * @since 1.4.0
9314	   */
9315	  reset() {
9316		let $selects = {
9317		  category: this.$categoriesSelect,
9318		  serviceId: this.$servicesSelect,
9319		  employeeId: this.$employeesSelect,
9320		  locationId: this.$locationsSelect
9321		};
9322		this.preventReact = true;
9323
9324		// Set default values
9325		for (let propertyName in $selects) {
9326		  let $select = $selects[propertyName];
9327		  let defaultValue = $select.attr('data-default');
9328		  if (defaultValue) {
9329			this.setProperty(propertyName, defaultValue);
9330		  } else {
9331			this.resetProperty(propertyName);
9332		  }
9333		}
9334		this.preventReact = false;
9335
9336		// Render changes if this step is active
9337		if (this.isActive) {
9338		  this.react();
9339		}
9340	  }
9341
9342	  /**
9343	   * @return {Boolean}
9344	   *
9345	   * @since 1.0
9346	   */
9347	  isValidInput() {
9348		return this.serviceId != 0;
9349	  }
9350
9351	  /**
9352	   * @since 1.19.0
9353	   *
9354	   * Save allowed options for validateProperty()
9355	   */
9356	  updateCategorySchema() {
9357		const categories = this.availability.getAvailableServiceCategories(this.serviceId);
9358		this.schema.category.options = Object.keys(categories);
9359	  }
9360
9361	  /**
9362	   * @since 1.19.0
9363	   *
9364	   * Save allowed options for validateProperty()
9365	   */
9366	  updateServiceSchema() {
9367		const services = this.availability.getAvailableServices(this.category, this.locationId, this.employeeId);
9368		this.schema.serviceId.options = Object.keys(services).map(mpa_intval);
9369	  }
9370
9371	  /**
9372	   * @since 1.19.0
9373	   *
9374	   * Save allowed options for validateProperty()
9375	   */
9376	  updateEmployeeSchema() {
9377		const employees = this.availability.getAvailableEmployees(this.serviceId, this.locationId);
9378		this.schema.employeeId.options = Object.keys(employees).map(mpa_intval);
9379	  }
9380
9381	  /**
9382	   * @since 1.19.0
9383	   *
9384	   * Save allowed options for validateProperty()
9385	   */
9386	  updateLocationSchema() {
9387		const locations = this.availability.getAvailableLocations(this.serviceId, this.employeeId);
9388		this.schema.locationId.options = Object.keys(locations).map(mpa_intval);
9389	  }
9390
9391	  /**
9392	   * @param {String} categorySlug
9393	   * @return {Boolean}
9394	   */
9395	  isValidCategoryBySchema(categorySlug) {
9396		return this.schema.category.options.includes(categorySlug);
9397	  }
9398
9399	  /**
9400	   * @param {Number} serviceId
9401	   * @return {Boolean}
9402	   */
9403	  isValidServiceBySchema(serviceId) {
9404		return this.schema.serviceId.options.includes(serviceId);
9405	  }
9406
9407	  /**
9408	   * @param {Number} locationId
9409	   * @return {Boolean}
9410	   */
9411	  isValidLocationBySchema(locationId) {
9412		return this.schema.locationId.options.includes(locationId);
9413	  }
9414
9415	  /**
9416	   * @param {Number} locationId
9417	   * @return {Boolean}
9418	   */
9419	  isValidEmployeeBySchema(employeeId) {
9420		return this.schema.employeeId.options.includes(employeeId);
9421	  }
9422
9423	  /**
9424	   * Executed only when the new value is different from the old one.
9425	   *
9426	   * @param {String} propertyName
9427	   * @param {*} newValue
9428	   * @param {*} oldValue
9429	   *
9430	   * @access protected
9431	   *
9432	   * @since 1.0
9433	   */
9434	  afterUpdate(propertyName, newValue, oldValue) {
9435		this.updateCategorySchema();
9436		this.updateServiceSchema();
9437		this.updateEmployeeSchema();
9438		this.updateLocationSchema();
9439	  }
9440
9441	  /**
9442	   * @access protected
9443	   *
9444	   * @since 1.0
9445	   */
9446	  react() {
9447		super.react();
9448
9449		// Update dependent <select>'s
9450		this.$categoriesSelect.val(this.category || '');
9451		this.$servicesSelect.val(this.serviceId || '');
9452		this.$employeesSelect.val(this.employeeId);
9453		this.$locationsSelect.val(this.locationId);
9454		this.$categoriesSelect.toggleClass('mpa-selected', this.category != '');
9455		this.$servicesSelect.toggleClass('mpa-selected', this.serviceId != 0);
9456		this.$employeesSelect.toggleClass('mpa-selected', this.employeeId != 0);
9457		this.$locationsSelect.toggleClass('mpa-selected', this.locationId != 0);
9458
9459		// re-renden select options
9460		this.renderCategorySelect();
9461		this.renderServiceSelect();
9462		this.renderEmployeeSelect();
9463		this.renderLocationSelect();
9464
9465		// Always enable submit button to allow validation messages
9466		this.$buttonNext.prop('disabled', false);
9467	  }
9468
9469	  /**
9470	   * @since 1.19.0
9471	   * @access protected
9472	   */
9473	  renderCategorySelect() {
9474		this.preventUpdate = true;
9475		update_select_options(this.$categoriesSelect, {
9476		  '': this.unselectedOptionText
9477		}, this.availability.getAvailableServiceCategories(this.serviceId), this.category || '');
9478		this.preventUpdate = false;
9479	  }
9480
9481	  /**
9482	   * @since 1.19.0
9483	   * @access protected
9484	   */
9485	  renderServiceSelect() {
9486		this.preventUpdate = true;
9487		update_select_options(this.$servicesSelect, {
9488		  '': this.unselectedServiceText
9489		}, this.availability.getAvailableServices(this.category, this.locationId, this.employeeId), this.serviceId || '');
9490		this.preventUpdate = false;
9491	  }
9492
9493	  /**
9494	   * @since 1.19.0
9495	   * @access protected
9496	   */
9497	  renderEmployeeSelect() {
9498		this.preventUpdate = true;
9499		update_select_options(this.$employeesSelect, {
9500		  0: this.unselectedOptionText
9501		}, this.availability.getAvailableEmployees(this.serviceId, this.locationId), this.employeeId);
9502		this.preventUpdate = false;
9503	  }
9504
9505	  /**
9506	   * @since 1.19.0
9507	   * @access protected
9508	   */
9509	  renderLocationSelect() {
9510		this.preventUpdate = true;
9511		update_select_options(this.$locationsSelect, {
9512		  0: this.unselectedOptionText
9513		}, this.availability.getAvailableLocations(this.serviceId, this.employeeId), this.locationId);
9514		this.preventUpdate = false;
9515	  }
9516	  show() {
9517		this.$servicesSelect.prop('required', true);
9518		super.show();
9519	  }
9520	  hide() {
9521		super.hide();
9522		this.$servicesSelect.prop('required', false);
9523	  }
9524
9525	  /**
9526	   * @access protected
9527	   *
9528	   * @since 1.0
9529	   */
9530	  enable() {
9531		super.enable();
9532
9533		// Enable all selects
9534		this.$selects.prop('disabled', false);
9535	  }
9536
9537	  /**
9538	   * @access protected
9539	   *
9540	   * @since 1.0
9541	   */
9542	  disable() {
9543		super.disable();
9544
9545		// Disable all selects
9546		this.$selects.prop('disabled', true);
9547	  }
9548
9549	  /**
9550	   * @param {Event} event
9551	   *
9552	   * @access protected
9553	   *
9554	   * @since 1.2
9555	   */
9556	  submitForm(event) {
9557		// Never submit the form, but allow validation
9558		if (!this.isActive || this.isValidInput()) {
9559		  event.preventDefault();
9560		}
9561	  }
9562
9563	  /**
9564	   * @access protected
9565	   *
9566	   * @since 1.0
9567	   */
9568	  maybeSubmit() {
9569		let cartItem = this.cart.getActiveItem();
9570		if (cartItem === null) {
9571		  return console.error('Unable to get active cart item in StepServiceForm.maybeSubmit().');
9572		}
9573
9574		// Save service
9575		cartItem.setService(this.availability.getService(this.serviceId, true, () => {
9576		  document.dispatchEvent(new CustomEvent('mpa_view_item', {
9577			detail: {
9578			  cartItem: cartItem,
9579			  currencyCode: mpapp().settings().getCurrency()
9580			}
9581		  }));
9582		}));
9583		cartItem.setServiceCategories(this.availability.getServiceCategories(this.serviceId));
9584
9585		// Save employee(-s)
9586		if (this.employeeId !== 0) {
9587		  cartItem.setEmployee(this.availability.getEmployee(this.employeeId));
9588		} else {
9589		  cartItem.setAvailableEmployees(this.availability.filterAvailableEmployees(this.serviceId, this.locationId, 'entities'));
9590		}
9591
9592		// Save location(-s)
9593		if (this.locationId !== 0) {
9594		  cartItem.setLocation(this.availability.getLocation(this.locationId));
9595		} else {
9596		  let allowedEmployees = this.employeeId || cartItem.getAvailableEmployeeIds();
9597		  cartItem.setAvailableLocations(this.availability.filterAvailableLocations(this.serviceId, allowedEmployees, 'entities'));
9598		}
9599	  }
9600	}
9601
9602	/**
9603	 * @since 1.0
9604	 */
9605	class AppointmentFormShortcode {
9606	  /**
9607	   * @since 1.0
9608	   */
9609	  constructor($element) {
9610		this.$element = $element;
9611		this.$message = this.$element.children('.mpa-message');
9612		this.cart = new Cart();
9613		this.steps = new BookingSteps(this.cart);
9614		this.load(); // And show()
9615	  }
9616
9617	  /**
9618	   * @since 1.9.0
9619	   * @access protected
9620	   */
9621	  setupSteps() {
9622		this.steps.addStep(new StepServiceForm(this.$element.find('.mpa-booking-step-service-form'), this.cart)).addStep(new StepPeriod(this.$element.find('.mpa-booking-step-period'), this.cart)).addStep(new StepCart(this.$element.find('.mpa-booking-step-cart'), this.cart)).addStep(new StepCheckout(this.$element.find('.mpa-booking-step-checkout'), this.cart));
9623		if (mpapp().settings().isPaymentsEnabled()) {
9624		  this.steps.addStep(new StepPayment(this.$element.find('.mpa-booking-step-payment'), this.cart));
9625		}
9626		this.steps.addStep(new StepBooking(this.$element.find('.mpa-booking-step-booking'), this.cart));
9627		this.steps.mount(this.$element);
9628	  }
9629
9630	  /**
9631	   * @access protected
9632	   *
9633	   * @since 1.0
9634	   */
9635	  load() {
9636		// Create the initial cart item for cart item steps
9637		this.cart.createItem();
9638
9639		// Check if we have any service to book
9640		let availability = new AvailabilityService();
9641		Promise.all([availability.ready(), mpapp().settings().ready()]).finally(() => {
9642		  // Setup steps only when the settings are loaded
9643		  this.setupSteps();
9644		  this.show();
9645		  if (!availability.isEmpty()) {
9646			this.steps.goToNextStep(); // Show the first step
9647		  } else {
9648			this.$message.html(__('Sorry, there are no services, employees or locations to book.', 'motopress-appointment'));
9649			this.$message.removeClass('mpa-hide');
9650		  }
9651		});
9652	  }
9653
9654	  /**
9655	   * @access protected
9656	   *
9657	   * @since 1.0
9658	   */
9659	  show() {
9660		// Remove loading GIF
9661		this.$element.addClass('mpa-loaded');
9662	  }
9663	}
9664
9665	/**
9666	 * @since 1.3
9667	 */
9668	class AppointmentFormWidget extends AppointmentFormShortcode {
9669	  /**
9670	   * @since 1.6.0
9671	   */
9672	  constructor($element) {
9673		super($element.children('.widget-body').first());
9674	  }
9675	}
9676
9677	// Init shortcodes and widgets
9678	jQuery('.appointment-form-shortcode').each((i, element) => {
9679	  new AppointmentFormShortcode(jQuery(element));
9680	});
9681	jQuery('.appointment-form-widget').each((i, element) => {
9682	  new AppointmentFormWidget(jQuery(element));
9683	});
9684
9685})();

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.