PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 2.4.6
Booking for Appointments and Events Calendar – Amelia v2.4.6
2.4.7 2.4.6 2.4.5 2.4.4 2.4.3 2.4.2 2.4.1 2.4 trunk 1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.15 1.2.16 1.2.17 1.2.18 1.2.19 1.2.2 1.2.20 1.2.21 1.2.22 1.2.23 1.2.24 1.2.25 1.2.26 1.2.27 1.2.28 1.2.29 1.2.3 1.2.30 1.2.31 1.2.32 1.2.33 1.2.34 1.2.35 1.2.36 1.2.37 1.2.38 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 2.0 2.0.1 2.0.2 2.1 2.1.1 2.1.2 2.1.3 2.2 2.2.1 2.3
ameliabooking / v3 / src / views / public / StepForm / BookingStepForm.vue
ameliabooking / v3 / src / views / public / StepForm Last commit date
BringingAnyone 1 month ago CartStep 1 month ago Congratulations 1 month ago DateTimeStep 3 weeks ago EmployeeStep 1 month ago Extras 1 month ago InfoStep 3 weeks ago InitStep 3 weeks ago LocationStep 1 month ago PakagesStep 1 month ago PaymentStep 1 month ago RecurringStep 1 month ago ServiceStep 1 month ago _fields 1 month ago common 1 month ago BookingStepForm.vue 3 weeks ago
BookingStepForm.vue
1964 lines
1 <template>
2 <template v-if="!amFonts.customFontSelected">
3 <link rel="preconnect" href="https://fonts.googleapis.com" />
4 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
5 <link
6 rel="stylesheet"
7 type="text/css"
8 :href="`${baseUrls.wpAmeliaPluginURL}v3/src/assets/scss/common/fonts/font.css`"
9 media="all"
10 />
11 </template>
12 <div
13 v-if="!empty && formDialogVisibility"
14 id="amelia-container"
15 ref="ameliaContainer"
16 class="am-fs__wrapper"
17 :class="{ 'am-collapsed': sidebarCollapsed }"
18 :style="cssVars"
19 >
20 <div class="am-sr-only" role="status" aria-live="polite" aria-atomic="true">
21 {{ statusMessage }}
22 </div>
23 <div class="am-sr-only" role="alert" aria-live="assertive" aria-atomic="true">
24 {{ alertMessage }}
25 </div>
26 <SideBar
27 v-if="containerWidth > 560 && sidebarVisibility"
28 class="am-fs-sb"
29 :class="[{ 'am-collapsed': sidebarCollapsed }, { 'am-rtl': isRtl }]"
30 :style="{
31 width: !sidebarCollapsed ? '240px' : '72px',
32 paddingBottom: `${sidebarFooterHeight + 16}px`,
33 }"
34 >
35 <template #step-list>
36 <div class="am-fs-sb__step-wrapper" tabindex="0">
37 <template
38 v-if="
39 ready &&
40 (stepsArray[stepIndex] !== congratulationsStep ||
41 !amSettings.general.addToCalendar ||
42 (booked && booked.data.length === 0))
43 "
44 >
45 <div v-for="step in sidebarSteps" :key="step.key" class="am-fs-sb__step">
46 <div class="am-fs-sb__step-inner">
47 <div class="am-fs-sb__step-icon">
48 <span :class="`am-icon-${step.icon}`"></span>
49 <span
50 v-if="step.key === 'cartStep' && useCartHasItems(store) !== 0"
51 class="am-fs-sb__step-icon__number"
52 :class="[{ 'am-rtl': isRtl }, sidebarCollapseItemsClass]"
53 >
54 {{ useCartHasItems(store) }}
55 </span>
56 </div>
57 <transition name="fade">
58 <p
59 v-if="!sidebarCollapsed"
60 class="am-fs-sb__step-heading"
61 :class="sidebarCollapseItemsClass"
62 >
63 {{ step.label }}
64 </p>
65 </transition>
66 <div
67 class="am-fs-sb__step-checker"
68 :class="[
69 { 'am-fs-sb__step-checker-selected': step.selected },
70 { 'am-rtl': isRtl },
71 sidebarCollapseItemsClass,
72 ]"
73 >
74 <transition name="fade">
75 <span v-if="step.finished" class="am-icon-check"></span>
76 </transition>
77 </div>
78 </div>
79 <TransitionGroup
80 tag="span"
81 name="fade"
82 appear
83 class="am-fs-sb__step-selection__wrapper"
84 :class="{
85 'am-fs-sb__step-selection-packages':
86 step.key === packageAppointmentsStep.key && step.stepSelectedData.length > 3,
87 }"
88 >
89 <template
90 v-if="
91 !sidebarCollapsed && (useCart(store).length === 1 || step.key === 'cartStep')
92 "
93 >
94 <p
95 v-for="(itemSelected, index) in step.stepSelectedData"
96 :key="itemSelected.position"
97 class="am-fs-sb__step-selection"
98 >
99 <span v-if="step.key !== packageAppointmentsStep.key || index < 3">
100 {{ itemSelected.value }}
101 </span>
102 <span v-else>
103 {{ amLabels.plus_more }}
104 </span>
105 </p>
106 </template>
107 </TransitionGroup>
108 </div>
109 </template>
110 <template v-else-if="ready">
111 <div>
112 <transition name="fade">
113 <AddToCalendar v-if="!sidebarCollapsed"></AddToCalendar>
114 </transition>
115 <div v-if="sidebarCollapsed" class="am-fs-sb__step">
116 <div class="am-fs-sb__step-inner">
117 <div class="am-fs-sb__step-icon">
118 <span :class="`am-icon-${stepsArray[stepIndex].sidebarData.icon}`"></span>
119 </div>
120 <div
121 class="am-fs-sb__step-checker"
122 :class="[
123 {
124 'am-fs-sb__step-checker-selected':
125 stepsArray[stepIndex].sidebarData.selected,
126 },
127 sidebarCollapseItemsClass,
128 ]"
129 >
130 <transition name="fade">
131 <span
132 v-if="stepsArray[stepIndex].sidebarData.finished"
133 class="am-icon-check"
134 ></span>
135 </transition>
136 </div>
137 </div>
138 </div>
139 </div>
140 </template>
141 <template v-else>
142 <!-- Skeleton -->
143 <el-skeleton animated>
144 <template #template>
145 <div v-for="item in new Array(3)" :key="item">
146 <div class="el-skeleton-item-wrapper">
147 <el-skeleton-item variant="text" />
148 </div>
149 </div>
150 </template>
151 </el-skeleton>
152 <!-- /Skeleton -->
153 </template>
154 </div>
155 </template>
156 <template #support-info>
157 <div ref="sidebarFooterRef" class="am-fs-sb__footer">
158 <div
159 v-if="
160 (amSettings.company.email || amSettings.company.phone) &&
161 (footerCustomizeOptions.heading ||
162 footerCustomizeOptions.phone ||
163 footerCustomizeOptions.email)
164 "
165 class="am-fs-sb__support"
166 >
167 <transition name="fade">
168 <div
169 v-if="!sidebarCollapsed && footerCustomizeOptions.heading"
170 class="am-fs-sb__support-heading"
171 :class="sidebarCollapseItemsClass"
172 >
173 {{ amLabels.get_in_touch }}
174 </div>
175 </transition>
176 <a
177 v-if="amSettings.company.phone && footerCustomizeOptions.phone"
178 class="am-fs-sb__support-email"
179 :aria-label="`Company phone: ${amSettings.company.phone}`"
180 :href="`tel:${amSettings.company.phone}`"
181 >
182 <template v-if="!sidebarCollapsed">
183 {{ amSettings.company.phone }}
184 </template>
185 <template v-else>
186 <span class="am-icon-phone"></span>
187 </template>
188 </a>
189 <a
190 v-if="amSettings.company.email && footerCustomizeOptions.email"
191 class="am-fs-sb__support-email"
192 :aria-label="`Company email: ${amSettings.company.email}`"
193 :href="`mailto:${amSettings.company.email}`"
194 >
195 <template v-if="!sidebarCollapsed">
196 {{ amSettings.company.email }}
197 </template>
198 <template v-else>
199 <span class="am-icon-email"></span>
200 </template>
201 </a>
202 </div>
203 <div
204 class="am-fs-sb__menu"
205 tabindex="0"
206 :aria-label="amLabels.collapse_menu"
207 :class="sidebarCollapseItemsClass"
208 @click="sidebarCollapsed = !sidebarCollapsed"
209 @keydown.enter="sidebarCollapsed = !sidebarCollapsed"
210 @keydown.space.prevent="sidebarCollapsed = !sidebarCollapsed"
211 >
212 <Transition name="fade">
213 <span v-if="!sidebarCollapsed" class="am-fs-sb__menu-text">
214 {{ amLabels.collapse_menu }}
215 </span>
216 </Transition>
217 <span :class="`am-icon-arrow-circle-${sidebarCollapsed ? 'left' : 'right'}`"></span>
218 </div>
219 </div>
220 </template>
221 </SideBar>
222 <MainContent>
223 <template v-if="stepsArray[stepIndex] !== congratulationsStep" #header>
224 <MainContentHeader :sidebar-visible="sidebarVisibility" :ready="ready"></MainContentHeader>
225 </template>
226 <template #step>
227 <component :is="stepsArray[stepIndex]" global-class="am-fs__main-content"></component>
228 </template>
229 <template #footer>
230 <MainContentFooter
231 :second-button-show="
232 stepsArray[stepIndex] === congratulationsStep &&
233 !!amSettings.roles.customerCabinet.pageUrl
234 "
235 :add-to-cart-button-show="useCartStep(store) && stepsArray[stepIndex] === cartStep"
236 :back-to-cart-button-show="
237 useCartStep(store) &&
238 cart.length > 1 &&
239 stepsArray[stepIndex] !== cartStep &&
240 stepsArray[stepIndex] !== infoStep &&
241 stepsArray[stepIndex] !== paymentStep &&
242 stepsArray[stepIndex] !== congratulationsStep
243 "
244 :booked="booked"
245 :loading="loading"
246 :payment-gateway="paymentGateway"
247 :customized-labels="footerLabels"
248 :primary-footer-button-type="primFooterBtnType"
249 :secondary-footer-button-type="secFooterBtnType"
250 :add-to-cart-button-type="addToCartBtnType"
251 :back-to-cart-button-type="backToCartBtnType"
252 :back-to-cart-label="dedicatedStepLabel('cancel', 'cartStep')"
253 :ready="ready"
254 @add-to-cart="addToCart"
255 @back-to-cart="backToCart"
256 ></MainContentFooter>
257 </template>
258 </MainContent>
259 </div>
260 <template v-else>
261 <div
262 v-if="shortcodeData.show !== 'packages' && !shortcodeData.package"
263 ref="ameliaContainer"
264 class="am-no-services"
265 :style="cssVars"
266 >
267 <img
268 :src="baseUrls.wpAmeliaPluginURL + '/v3/src/assets/img/am-empty-booking.svg'"
269 style="margin-top: 10px"
270 :alt="amLabels.no_services_employees"
271 />
272 <div class="am-no-services-oops">{{ amLabels.oops }}</div>
273 <div class="am-no-services-text">
274 {{ amLabels.no_services_employees }}
275 </div>
276 <div class="am-no-services-text-2">
277 <p>{{ amLabels.add_services_employees }}</p>
278 <a href="https://wpamelia.com/services-and-categories/" rel="nofollow">
279 {{ amLabels.add_services_url }}&nbsp;
280 </a>
281 <span style="font-size: 14px">{{ amLabels.and }}&nbsp;</span>
282 <a href="https://wpamelia.com/employees/" rel="nofollow">
283 {{ amLabels.add_employees_url }}
284 </a>
285 </div>
286 </div>
287 <div
288 v-else
289 ref="ameliaContainer"
290 class="am-no-services"
291 :style="{ height: '100%', ...cssVars }"
292 >
293 <img
294 :src="baseUrls.wpAmeliaPluginURL + '/v3/src/assets/img/am-empty-booking.svg'"
295 style="margin-top: 10px"
296 :alt="amLabels.no_package_services"
297 />
298 <div>{{ amLabels.oops }}</div>
299 <div>{{ amLabels.no_package_services }}</div>
300 <a href="https://wpamelia.com/services-and-categories/" rel="nofollow">
301 {{ amLabels.add_services_url }}&nbsp;
302 </a>
303 </div>
304 </template>
305 <BackLink />
306 </template>
307
308 <script setup>
309 // * Form construction
310 import MainContent from '../../common/SbsFormConstruction/MainContent/MainContent.vue'
311 import MainContentHeader from '../../common/SbsFormConstruction/MainContent/parts/MainContentHeader.vue'
312 import MainContentFooter from '../../common/SbsFormConstruction/MainContent/parts/MainContentFooter'
313 import SideBar from '../../common/SbsFormConstruction/SideBar/SideBar.vue'
314 import BackLink from '../Parts/BackLink'
315
316 // * Step components
317 import InitStep from './InitStep/InitStep.vue'
318 import BringingAnyone from './BringingAnyone/BringingAnyone'
319 import Extras from './Extras/Extras.vue'
320 import PackageInfo from './PakagesStep/PackageInfoStep'
321 import DateTimeStep from './DateTimeStep/DateTimeStep.vue'
322 import PackageAppointmentsStep from './PakagesStep/PackageAppointmentsStep.vue'
323 import PackageAppointmentsListStep from './PakagesStep/PackageAppointmentsListStep.vue'
324 import CartStep from './CartStep/CartStep.vue'
325 import InfoStep from './InfoStep/InfoStep.vue'
326 import Congratulations from './Congratulations/Congratulations.vue'
327 import AddToCalendar from './Congratulations/AddToCalendar.vue'
328 import PaymentStep from './PaymentStep/PaymentStep.vue'
329 import PackagesStep from './PakagesStep/PackageStep'
330 import RecurringStep from './RecurringStep/RecurringStep'
331 import RecurringSummary from './RecurringStep/RecurringSummary'
332 import ServiceStep from './ServiceStep/ServiceStep.vue'
333 import EmployeeStep from './EmployeeStep/EmployeeStep.vue'
334 import LocationStep from './LocationStep/LocationStep.vue'
335
336 // * import from Vue
337 import {
338 ref,
339 reactive,
340 watch,
341 provide,
342 inject,
343 markRaw,
344 computed,
345 onMounted,
346 watchEffect,
347 nextTick,
348 } from 'vue'
349 // * import from Vuex
350 import { useStore } from 'vuex'
351 // * import composable
352 import useRestore from '../../../assets/js/public/restore'
353 import { useBuildPackage } from '../../../assets/js/public/package.js'
354 import { defaultCustomizeSettings } from '../../../assets/js/common/defaultCustomize.js'
355 import useAction from '../../../assets/js/public/actions'
356 import { useRenderAction } from '../../../assets/js/public/renderActions.js'
357 import { useColorTransparency } from '../../../assets/js/common/colorManipulation'
358 import {
359 usePrepaidPrice,
360 useCapacity,
361 useCheckingIfAllNotFree,
362 } from '../../../assets/js/common/appointments'
363 import {
364 useCart,
365 useCartStep,
366 useInitCartItem,
367 useCartHasItems,
368 useInitSelection,
369 useAddToCart,
370 useGoToCartStep,
371 } from '../../../assets/js/public/cart'
372 import { provideLiveAnnouncer } from '../../../assets/js/common/liveAnnouncer'
373
374 let formDialogVisibility = inject('formDialogVisibility', ref(true))
375
376 const { statusMessage, alertMessage, announceStatus } = provideLiveAnnouncer()
377
378 // * Shortcode data
379 const shortcodeData = inject('shortcodeData')
380
381 // * Plugin Licence
382 let licence = inject('licence')
383
384 // * Component reference
385 let ameliaContainer = ref(null)
386 // * Plugin wrapper width
387 let containerWidth = ref()
388 provide('containerWidth', containerWidth)
389
390 let empty = ref(false)
391
392 // * window resize listener
393 window.addEventListener('resize', resize)
394 // * resize function
395 function resize() {
396 if (ameliaContainer.value) {
397 containerWidth.value = ameliaContainer.value.offsetWidth
398 }
399 }
400
401 onMounted(() => {
402 store.commit('shortcodeParams/setForm', 'stepForm')
403
404 document
405 .getElementById('amelia-v2-booking-' + shortcodeData.value.counter)
406 .classList.add('amelia-v2-booking-' + shortcodeData.value.counter + '-loaded')
407
408 useAction(store, {}, 'ViewContent', 'appointment', null, null)
409
410 nextTick(() => {
411 containerWidth.value = ameliaContainer.value.offsetWidth
412 })
413
414 useAction(store, { containerWidth }, 'ContainerWidth', 'appointment', null, null)
415 })
416
417 // * Root Settings
418 const amSettings = inject('settings')
419
420 // * Customize
421 const amCustomize =
422 amSettings.customizedData && amSettings.customizedData.sbsNew
423 ? amSettings.customizedData.sbsNew
424 : defaultCustomizeSettings.sbsNew
425 if (amCustomize) {
426 provide('amCustomize', amCustomize)
427 }
428
429 let footerCustomizeOptions = computed(() => {
430 let obj = {
431 heading: true,
432 phone: true,
433 email: true,
434 }
435
436 if ('supportHeading' in amCustomize.sidebar.options) {
437 obj.heading = amCustomize.sidebar.options.supportHeading.visibility
438 }
439
440 if ('companyPhone' in amCustomize.sidebar.options) {
441 obj.phone = amCustomize.sidebar.options.companyPhone.visibility
442 }
443
444 if ('companyEmail' in amCustomize.sidebar.options) {
445 obj.email = amCustomize.sidebar.options.companyEmail.visibility
446 }
447
448 return obj
449 })
450
451 // * Fonts
452 const amFonts = ref(
453 amSettings.customizedData ? amSettings.customizedData.fonts : defaultCustomizeSettings.fonts,
454 )
455 provide('amFonts', amFonts)
456
457 // * Dialog width
458 let dialogWrapperWidth = inject('dialogWrapperWidth', ref(''))
459
460 // * Form Sidebar Collapse
461 let sidebarCollapsed = ref(false)
462 provide('sidebarCollapsed', sidebarCollapsed)
463
464 let sidebarCollapseItemsClass = ref('')
465
466 watch(sidebarCollapsed, (current) => {
467 if (current) {
468 setTimeout(() => {
469 sidebarCollapseItemsClass.value = 'am-collapsed'
470 dialogWrapperWidth.value = '592px'
471 }, 1000)
472 } else {
473 sidebarCollapseItemsClass.value = ''
474 dialogWrapperWidth.value = '760px'
475 }
476 })
477
478 let sidebarFooterRef = ref(null)
479 let sidebarFooterHeight = ref(0)
480
481 // * Form Sidebar Visibility
482 let sidebarVisibility = ref(
483 amCustomize.sidebar ? amCustomize.sidebar.options.self.visibility : true,
484 )
485
486 // * Root Urls
487 const baseUrls = inject('baseUrls')
488
489 // * Define store
490 const store = useStore()
491
492 let isRtl = computed(() => store.getters['getIsRtl'])
493
494 let ready = computed(() => store.getters['entities/getReady'])
495
496 let isRestored = ref(false)
497
498 watch(ready, (current) => {
499 if (current) {
500 nextTick(() => {
501 if (sidebarFooterRef.value) {
502 setTimeout(() => {
503 sidebarFooterHeight.value = sidebarFooterRef.value.offsetHeight
504 }, 200)
505 }
506 })
507
508 setShortcodeParams()
509
510 let preselected = store.getters['entities/getPreselected']
511
512 let selectionPackages = store.getters['entities/filteredPackages'](
513 store.getters['booking/getSelection'],
514 )
515
516 empty.value =
517 store.getters['entities/getServices'].length === 0 ||
518 store.getters['entities/getEmployees'].length === 0 ||
519 (store.getters['entities/getPackages'].length === 0 &&
520 (shortcodeData.value.show === 'packages' || preselected.package.length > 0)) ||
521 ((shortcodeData.value.show === 'packages' ||
522 preselected.show === 'packages' ||
523 preselected.package.length > 1) &&
524 selectionPackages.length === 0)
525
526 let restore = useRestore(store, shortcodeData.value)
527
528 if (restore) {
529 stepsArray.value.splice(0, stepsArray.value.length)
530
531 sidebarSteps.value.splice(0, sidebarSteps.value.length)
532
533 stepIndex.value = 0
534
535 restore.steps.forEach((key) => {
536 switch (key) {
537 case 'packageStep':
538 stepsArray.value.push(packagesStep)
539 break
540
541 case 'bringingAnyone':
542 stepsArray.value.push(bringingAnyone)
543 break
544
545 case 'initStep':
546 stepsArray.value.push(initStep)
547 break
548
549 case 'serviceStep':
550 stepsArray.value.push(serviceStep)
551 break
552
553 case 'employeeStep':
554 stepsArray.value.push(employeeStep)
555 break
556
557 case 'locationStep':
558 stepsArray.value.push(locationStep)
559 break
560
561 case 'packageInfoStep':
562 stepsArray.value.push(packageInfoStep)
563 break
564
565 case 'packageAppointmentsStep':
566 stepsArray.value.push(packageAppointmentsStep)
567 break
568
569 case 'packageAppointmentsListStep':
570 stepsArray.value.push(packageAppointmentsListStep)
571 break
572
573 case 'extrasStep':
574 stepsArray.value.push(extras)
575 break
576
577 case 'dateTimeStep':
578 stepsArray.value.push(dateTimeStep)
579 break
580
581 case 'recurringStep':
582 stepsArray.value.push(recurringStep)
583 break
584
585 case 'recurringSummary':
586 stepsArray.value.push(recurringSummary)
587 break
588
589 case 'cartStep':
590 stepsArray.value.push(cartStep)
591 break
592
593 case 'infoStep':
594 stepsArray.value.push(infoStep)
595 break
596
597 case 'paymentStep':
598 stepsArray.value.push(paymentStep)
599 break
600
601 case 'congratulations':
602 stepsArray.value.push(congratulationsStep)
603 break
604 }
605
606 let sideBarItem = restore.sidebar.find((i) => i.key === key)
607
608 sidebarDataUpdate()
609
610 if (typeof sideBarItem !== 'undefined') {
611 if (sideBarItem.data) {
612 sideBarItem.data.forEach((i) => {
613 sidebarDataCollector({
614 reference: i.reference,
615 position: i.position,
616 value: i.value,
617 })
618 })
619 }
620 }
621
622 stepIndex.value++
623 })
624
625 store.commit('booking/setLoading', false)
626
627 let index = -1
628
629 if (restore.result === 'success') {
630 index = stepsArray.value.length - 1
631 } else if (restore.result === 'error' || restore.result === 'canceled') {
632 index = stepsArray.value.length - 2
633 }
634
635 for (let i = 0; i <= index; i++) {
636 stepsArray.value[i].finished = true
637 }
638
639 for (let i = 0; i <= index - 1; i++) {
640 sidebarSteps.value[i].finished = true
641 sidebarSteps.value[i].selected = false
642 }
643 if (restore.result === 'error' || restore.result === 'canceled') {
644 sidebarSteps.value[index].finished = false
645 sidebarSteps.value[index].selected = true
646 }
647
648 isRestored.value = true
649 stepIndex.value = index
650 }
651 }
652 })
653
654 store.commit('entities/setPreselected', shortcodeData.value)
655
656 // * Get Entities from server
657 store.dispatch('entities/getEntities', {
658 types: [
659 'employees',
660 'categories',
661 'locations',
662 'packages',
663 'entitiesRelations',
664 'customFields',
665 'taxes',
666 ],
667 licence: licence,
668 loadEntities: shortcodeData.value.hasApiCall || shortcodeData.value.trigger,
669 showHidden: false,
670 isPanel: false,
671 })
672
673 // * Form Component Collection
674 const initStep = markRaw(InitStep)
675 const extras = markRaw(Extras)
676 const packageInfoStep = markRaw(PackageInfo)
677 const recurringStep = markRaw(RecurringStep)
678 const recurringSummary = markRaw(RecurringSummary)
679 const dateTimeStep = markRaw(DateTimeStep)
680 const packageAppointmentsStep = markRaw(PackageAppointmentsStep)
681 const packageAppointmentsListStep = markRaw(PackageAppointmentsListStep)
682 const congratulationsStep = markRaw(Congratulations)
683 const cartStep = markRaw(CartStep)
684 const infoStep = markRaw(InfoStep)
685 const paymentStep = markRaw(PaymentStep)
686 const packagesStep = markRaw(PackagesStep)
687 const bringingAnyone = markRaw(BringingAnyone)
688
689 // * List Layout Components
690 const serviceStep = markRaw(ServiceStep)
691 const employeeStep = markRaw(EmployeeStep)
692 const locationStep = markRaw(LocationStep)
693
694 // * Array of step components
695 const stepsArray = ref(calculateInitStepsArray())
696
697 function calculateInitStepsArray() {
698 let steps
699 if (useCartStep(store)) {
700 steps = [initStep, dateTimeStep, cartStep, infoStep, congratulationsStep]
701 } else {
702 steps = [initStep, dateTimeStep, infoStep, congratulationsStep]
703 }
704
705 if (shortcodeData.value.layout === '2') {
706 steps.splice(0, 1)
707 let stepsOrder =
708 'order' in amCustomize
709 ? amCustomize.order
710 : [{ id: 'ServiceStep' }, { id: 'EmployeeStep' }, { id: 'LocationStep' }]
711 stepsOrder.forEach((stepKey, index) => {
712 if (stepKey.id === 'ServiceStep') {
713 steps.splice(index, 0, serviceStep)
714 }
715
716 if (stepKey.id === 'EmployeeStep') {
717 steps.splice(index, 0, employeeStep)
718 }
719
720 if (stepKey.id === 'LocationStep') {
721 steps.splice(index, 0, locationStep)
722 }
723 })
724 }
725
726 return steps
727 }
728
729 provide('stepsArray', stepsArray)
730
731 // * labels
732 const labels = inject('labels')
733
734 // * local language short code
735 const localLanguage = inject('localLanguage')
736
737 // * if local lang is in settings lang
738 let langDetection = computed(() => amSettings.general.usedLanguages.includes(localLanguage.value))
739
740 let cart = useCart(store)
741
742 // * Steps flags and boolean values - we use these flags and booleans to achieve certain functionalities on pages
743 // * preventRebuildRecurringBookingData flag prevents the recurring booking data from being rebuilt when navigating back to the recurring summary step
744 const preventRebuildRecurringBookingData = ref(false)
745 provide('stepsFlagsAndBooleans', {
746 preventRebuildRecurringBookingData,
747 })
748
749 // * Computed labels
750 let amLabels = computed(() => {
751 let computedLabels = reactive({ ...labels })
752
753 if (amSettings.customizedData && amSettings.customizedData.sbsNew) {
754 Object.keys(amSettings.customizedData.sbsNew).forEach((stepKey) => {
755 if (stepKey !== 'colors' && amSettings.customizedData.sbsNew[stepKey].translations) {
756 let customizedLabels = amSettings.customizedData.sbsNew[stepKey].translations
757 Object.keys(customizedLabels).forEach((labelKey) => {
758 if (customizedLabels[labelKey][localLanguage.value] && langDetection.value) {
759 computedLabels[labelKey] = customizedLabels[labelKey][localLanguage.value]
760 } else if (customizedLabels[labelKey].default) {
761 computedLabels[labelKey] = customizedLabels[labelKey].default
762 }
763 })
764 }
765 })
766 }
767 return computedLabels
768 })
769
770 provide('amLabels', amLabels)
771
772 let footerLabels = computed(() => {
773 let customLabels = {}
774 if (amSettings.customizedData && amSettings.customizedData.sbsNew) {
775 let customizedLabels = amSettings.customizedData.sbsNew[stepsArray.value[stepIndex.value].key]
776 ? amSettings.customizedData.sbsNew[stepsArray.value[stepIndex.value].key].translations
777 : null
778 if (amSettings.customizedData && customizedLabels) {
779 Object.keys(customizedLabels).forEach((labelKey) => {
780 if (customizedLabels[labelKey][localLanguage.value] && langDetection.value) {
781 customLabels[labelKey] = customizedLabels[labelKey][localLanguage.value]
782 } else if (customizedLabels[labelKey].default) {
783 customLabels[labelKey] = customizedLabels[labelKey].default
784 }
785 })
786 }
787 }
788
789 return Object.keys(customLabels).length ? customLabels : labels
790 })
791
792 let primFooterBtnType = computed(() => {
793 let btnType = 'filled'
794 if (
795 amSettings.customizedData &&
796 amSettings.customizedData.sbsNew &&
797 amSettings.customizedData.sbsNew[stepsArray.value[stepIndex.value].key]
798 ) {
799 btnType =
800 amSettings.customizedData.sbsNew[stepsArray.value[stepIndex.value].key].options
801 .primaryFooterButton.buttonType
802 }
803
804 return btnType
805 })
806
807 provide('primFooterBtnType', primFooterBtnType)
808
809 provide('primDescBtnType', primFooterBtnType)
810
811 let secFooterBtnType = computed(() => {
812 let btnType = 'text'
813 if (
814 amSettings.customizedData &&
815 amSettings.customizedData.sbsNew &&
816 amSettings.customizedData.sbsNew[stepsArray.value[stepIndex.value].key] &&
817 amSettings.customizedData.sbsNew[stepsArray.value[stepIndex.value].key].options
818 .secondaryFooterButton
819 ) {
820 btnType =
821 amSettings.customizedData.sbsNew[stepsArray.value[stepIndex.value].key].options
822 .secondaryFooterButton.buttonType
823 }
824
825 return btnType
826 })
827
828 let addToCartBtnType = computed(() => {
829 if (
830 amSettings.customizedData &&
831 amSettings.customizedData.sbsNew &&
832 amSettings.customizedData.sbsNew['cartStep']
833 ) {
834 return amSettings.customizedData.sbsNew['cartStep'].options['addToCart'].buttonType
835 }
836
837 return 'text'
838 })
839
840 let backToCartBtnType = computed(() => {
841 if (
842 amSettings.customizedData &&
843 amSettings.customizedData.sbsNew &&
844 amSettings.customizedData.sbsNew['cartStep']
845 ) {
846 return amSettings.customizedData.sbsNew['cartStep'].options['backToCart'].buttonType
847 }
848
849 return 'text'
850 })
851
852 function dedicatedStepLabel(labelKey, stepKey) {
853 let customLabel = ''
854 if (amSettings.customizedData && amSettings.customizedData.sbsNew) {
855 let customizedLabels = amSettings.customizedData.sbsNew[stepKey]
856 ? amSettings.customizedData.sbsNew[stepKey].translations
857 : null
858 if (amSettings.customizedData && customizedLabels && customizedLabels[labelKey]) {
859 if (
860 customLabel === '' &&
861 customizedLabels[labelKey][localLanguage.value] &&
862 langDetection.value
863 ) {
864 customLabel = customizedLabels[labelKey][localLanguage.value]
865 } else if (customLabel === '' && customizedLabels[labelKey].default) {
866 customLabel = customizedLabels[labelKey].default
867 }
868 }
869 }
870
871 return customLabel ? customLabel : labels[labelKey]
872 }
873
874 let bringingAnyoneOptions = computed(() => {
875 return useCapacity(
876 store.getters['entities/getEmployeeServices'](store.getters['booking/getSelection']),
877 )
878 })
879
880 function setShortcodeParams() {
881 let preselected = store.getters['entities/getPreselected']
882
883 // * When single category selected
884 if (preselected.category.length === 1) {
885 store.commit('booking/setCategoryId', parseInt(preselected.category[0]))
886 }
887
888 // * When single service selected
889 if (preselected.service.length === 1) {
890 let service = store.getters['entities/getService'](parseInt(preselected.service[0]))
891
892 if (service) {
893 store.commit('booking/setServiceId', parseInt(preselected.service[0]))
894 }
895 // * Set category id from service if it exists
896 store.commit('booking/setCategoryId', service ? parseInt(service.categoryId) : null)
897 }
898
899 // * When single employee selected
900 if (preselected.employee.length === 1) {
901 store.commit('booking/setEmployeeId', parseInt(preselected.employee[0]))
902 }
903
904 // * When single location selected
905 if (preselected.location.length === 1) {
906 store.commit('booking/setLocationId', parseInt(preselected.location[0]))
907 }
908
909 // * When single package selected
910 if (preselected.package.length === 1) {
911 store.commit('booking/setPackageId', parseInt(preselected.package[0]))
912 }
913
914 if (preselected.package.length === 1) {
915 if (shortcodeData.value.layout === '2') {
916 stepsArray.value.splice(0, 3, packageInfoStep)
917 sidebarSteps.value.splice(0, 3)
918 } else {
919 stepsArray.value.splice(0, 1, packageInfoStep)
920 sidebarSteps.value.splice(0, 1)
921 }
922
923 sidebarDataUpdate()
924 let selectedPackage = store.getters['entities/filteredPackages'](
925 store.getters['booking/getSelection'],
926 )[0]
927 if (selectedPackage) {
928 goToPackageStep(selectedPackage, false)
929 }
930 } else if (preselected.show === 'packages' || preselected.package.length > 1) {
931 if (shortcodeData.value.layout === '2') {
932 stepsArray.value.splice(0, 3, packagesStep)
933 sidebarSteps.value.splice(0, 3)
934 } else {
935 stepsArray.value.splice(0, 1, packagesStep)
936 sidebarSteps.value.splice(0, 1)
937 }
938
939 // * Set bookable type to package
940 store.commit('booking/setBookableType', 'package')
941 sidebarDataUpdate()
942 } else {
943 changeInitStepDataService()
944 store.commit('booking/setBookableType', 'appointment')
945 let employeeOptions =
946 store.getters['entities/filteredEmployees'](store.getters['booking/getSelection']).length <= 1
947
948 let employeeStepVisibility = amCustomize?.employeeStep
949 ? !amCustomize.employeeStep.options.employee.visibility
950 : !defaultCustomizeSettings.sbsNew.employeeStep.options.employee.visibility
951 let employeeVisibility =
952 shortcodeData.value.layout === '2'
953 ? employeeStepVisibility
954 : !amCustomize.initStep.options.employee.visibility
955 let locationOptions =
956 store.getters['entities/filteredLocations'](store.getters['booking/getSelection']).length <= 1
957 let locationStepVisibility = amCustomize?.locationStep
958 ? !amCustomize.locationStep.options.location.visibility
959 : !defaultCustomizeSettings.sbsNew.locationStep.options.location.visibility
960 let locationVisibility =
961 shortcodeData.value.layout === '2'
962 ? locationStepVisibility
963 : !amCustomize.initStep.options.location.visibility
964
965 if (shortcodeData.value.layout === '2') {
966 // * SeviceStep index in stepsArray
967 let serviceStepIndex = stepsArray.value.findIndex((item) => item.name === 'ServiceStep')
968
969 // * If sevice is preslected
970 if (preselected.service.length === 1 && serviceStepIndex !== -1) {
971 stepsArray.value.splice(serviceStepIndex, 1)
972 sidebarSteps.value.splice(serviceStepIndex, 1)
973 }
974
975 // * EmployeeStep index in stepsArray
976 let employeeStepIndex = stepsArray.value.findIndex((item) => item.name === 'EmployeeStep')
977
978 // * If employee is preslected
979 if (
980 (preselected.employee.length === 1 && employeeStepIndex !== -1 && employeeOptions) ||
981 employeeVisibility
982 ) {
983 stepsArray.value.splice(employeeStepIndex, 1)
984 sidebarSteps.value.splice(employeeStepIndex, 1)
985 }
986
987 // * LocationStep index in stepsArray
988 let locationStepIndex = stepsArray.value.findIndex((item) => item.name === 'LocationStep')
989
990 // * If location is preslected
991 if (
992 ((preselected.location.length === 1 ||
993 store.getters['entities/getLocations'].length === 0) &&
994 locationStepIndex !== -1 &&
995 locationOptions) ||
996 locationVisibility
997 ) {
998 stepsArray.value.splice(locationStepIndex, 1)
999 sidebarSteps.value.splice(locationStepIndex, 1)
1000 }
1001
1002 if (
1003 preselected.service.length === 1 &&
1004 (preselected.employee.length === 1 || employeeOptions || employeeVisibility) &&
1005 (preselected.location.length === 1 || locationOptions || locationVisibility)
1006 ) {
1007 if (bringingAnyoneOptions.value.availability) {
1008 stepsArray.value.splice(0, 0, bringingAnyone)
1009 }
1010 sidebarDataUpdate()
1011 }
1012 } else {
1013 if (
1014 preselected.service.length === 1 &&
1015 (preselected.employee.length === 1 || employeeOptions || employeeVisibility) &&
1016 (preselected.location.length === 1 || locationOptions || locationVisibility)
1017 ) {
1018 if (bringingAnyoneOptions.value.availability) {
1019 stepsArray.value.splice(0, 1, bringingAnyone)
1020 sidebarSteps.value.splice(0, 1)
1021 } else {
1022 stepsArray.value.splice(0, 1)
1023 sidebarSteps.value.splice(0, 1)
1024 }
1025 sidebarDataUpdate()
1026 }
1027 }
1028 }
1029 }
1030
1031 function stepChanger(steps, stepsNamesToRemove, stepsToAdd, startIndex) {
1032 let removeIndexes = steps.value
1033 .map((item, index) => (stepsNamesToRemove.includes(item.name) ? index : null))
1034 .filter((item) => item !== null)
1035 .reverse()
1036
1037 removeIndexes.forEach((item) => {
1038 steps.value.splice(item, 1)
1039 })
1040
1041 stepsToAdd.forEach((item) => {
1042 if (steps.value.indexOf(item) === -1) {
1043 steps.value.splice(startIndex + 1, 0, item)
1044 }
1045 })
1046 }
1047
1048 provide('goToPackageStep', { goToPackageStep })
1049
1050 // * Those variables are used to store initial steps array if layout is 2
1051 let initStepsArray = ref([])
1052 let lowestInitStepsIndex = ref(0)
1053
1054 function goToPackageStep(pack, goToNextStep = true) {
1055 store.commit('booking/setPackageId', pack.id)
1056 store.commit('booking/setBookableType', 'package')
1057 store.commit('booking/setMultipleAppointments', useBuildPackage(0, pack))
1058 store.commit('booking/setMultipleAppointmentsIndex', 0)
1059
1060 let bookingCount = pack.bookable.reduce(
1061 (partialSum, book) => partialSum + book.maximumScheduled,
1062 0,
1063 )
1064
1065 let removeSteps = []
1066 let addSteps = []
1067
1068 if (shortcodeData.value.layout === '2') {
1069 stepsArray.value.forEach((step, index) => {
1070 if (
1071 step.name === 'ServiceStep' ||
1072 step.name === 'EmployeeStep' ||
1073 step.name === 'LocationStep'
1074 ) {
1075 initStepsArray.value.push({
1076 index: index,
1077 component: step,
1078 })
1079 }
1080 })
1081
1082 lowestInitStepsIndex.value = Math.min(...initStepsArray.value.map((step) => step.index))
1083
1084 // * If ServiceStep is present in initial steps
1085 if (initStepsArray.value.find((step) => step.component.name === 'ServiceStep')) {
1086 initStepsArray.value.forEach((step) => {
1087 if (step.component.name !== 'ServiceStep') {
1088 removeSteps.push(step.component.name)
1089 }
1090 })
1091 } else {
1092 // * If ServiceStep is not present in initial steps
1093 initStepsArray.value.forEach((step) => {
1094 if (step.index !== lowestInitStepsIndex.value) {
1095 removeSteps.push(step.component.name)
1096 }
1097 })
1098 }
1099 }
1100
1101 if (stepsArray.value[0] !== extras) {
1102 removeSteps.push('ExtrasStep')
1103 }
1104
1105 if (stepsArray.value[0] !== dateTimeStep) {
1106 removeSteps.push('DateTimeStep')
1107 }
1108
1109 if (stepsArray.value[0] !== cartStep) {
1110 removeSteps.push('CartStep')
1111 }
1112
1113 if (bookingCount > 0) {
1114 addSteps = addSteps.concat([
1115 packageAppointmentsListStep,
1116 packageAppointmentsStep,
1117 packageInfoStep,
1118 ])
1119 } else {
1120 addSteps.push(packageInfoStep)
1121 }
1122
1123 if (stepIndex.value > 0) {
1124 stepIndex.value = 0
1125 }
1126 stepChanger(stepsArray, removeSteps, addSteps, stepIndex.value)
1127
1128 stepChanger(sidebarSteps, removeSteps, [], stepIndex.value)
1129
1130 sidebarDataUpdate()
1131
1132 if (goToNextStep) {
1133 nextStep()
1134 }
1135 }
1136
1137 function removePackageStep() {
1138 store.commit('booking/setBookableType', 'appointment')
1139
1140 store.commit('booking/setMultipleAppointments', [
1141 {
1142 packageId: null,
1143 serviceId: null,
1144 index: 0,
1145 services: {},
1146 },
1147 ])
1148
1149 store.commit('booking/setPackageId', null)
1150
1151 store.commit('booking/setMultipleAppointmentsIndex', 0)
1152
1153 useInitCartItem(store)
1154
1155 let addSteps = []
1156
1157 if (stepsArray.value[0] !== dateTimeStep) {
1158 if (useCartStep(store)) {
1159 addSteps.push(cartStep)
1160 }
1161
1162 addSteps.push(dateTimeStep)
1163 }
1164
1165 if (
1166 stepsArray.value[0] !== extras &&
1167 selectedServiceExtras.value &&
1168 selectedServiceExtras.value.length
1169 ) {
1170 addSteps.push(extras)
1171 }
1172
1173 let addingStartIndex = stepIndex.value - 1
1174
1175 if (shortcodeData.value.layout === '2' && initStepsArray.value.length) {
1176 initStepsArray.value.forEach((step) => {
1177 if (!stepsArray.value.some((s) => s.name === step.component.name)) {
1178 stepsArray.value.splice(step.index, 0, step.component)
1179 }
1180 })
1181
1182 addingStartIndex = initStepsArray.value.length - 1
1183 }
1184
1185 stepChanger(
1186 stepsArray,
1187 ['PackageInfoStep', 'PackageAppointmentsStep', 'PackageAppointmentsListStep'],
1188 addSteps,
1189 addingStartIndex,
1190 )
1191
1192 stepChanger(
1193 sidebarSteps,
1194 ['PackageInfoStep', 'PackageAppointmentsStep', 'PackageAppointmentsListStep'],
1195 [],
1196 stepIndex.value,
1197 )
1198
1199 sidebarDataUpdate()
1200 }
1201
1202 provide('goToRecurringStep', { goToRecurringStep })
1203
1204 provide('removeRecurringStep', { removeRecurringStep })
1205
1206 function goToRecurringStep() {
1207 if (useCartStep(store)) {
1208 removeCartStep()
1209 }
1210
1211 let startIndex = 0
1212
1213 for (let i = 0; i < stepsArray.value.length; i++) {
1214 if (stepsArray.value[i].name === 'DateTimeStep') {
1215 startIndex = i
1216
1217 break
1218 }
1219 }
1220
1221 stepsArray.value.splice(startIndex + 1, 0, recurringStep)
1222 stepsArray.value.splice(startIndex + 2, 0, recurringSummary)
1223
1224 sidebarDataUpdate()
1225
1226 nextStep()
1227 }
1228
1229 function removeRecurringStep() {
1230 for (let i = stepsArray.value.length - 1; i >= 0; i--) {
1231 if (
1232 stepsArray.value[i].name === 'RecurringStep' ||
1233 stepsArray.value[i].name === 'RecurringSummary'
1234 ) {
1235 stepsArray.value.splice(i + 1, 1)
1236 }
1237 }
1238
1239 if (useCartStep(store)) {
1240 addCartStep(stepIndex.value)
1241 }
1242
1243 sidebarDataUpdate()
1244 }
1245
1246 provide('addPaymentsStep', { addPaymentsStep })
1247
1248 provide('removePaymentsStep', { removePaymentsStep })
1249
1250 // Provide cart step controls for child components (e.g., Calendar) so they can hide cart during waiting list selection
1251 provide('addCartStep', { addCartStep: () => addCartStep(stepIndex.value) })
1252
1253 provide('removeCartStep', { removeCartStep })
1254
1255 function addPaymentsStep() {
1256 stepsArray.value.splice(stepsArray.value.length - 1, 0, paymentStep)
1257
1258 sidebarDataUpdate()
1259 }
1260
1261 function removePaymentsStep() {
1262 let removeSteps = ['PaymentStep']
1263
1264 stepChanger(stepsArray, removeSteps, [], stepIndex.value)
1265
1266 stepChanger(sidebarSteps, removeSteps, [], stepIndex.value)
1267 }
1268
1269 function addCartStep(index) {
1270 stepChanger(stepsArray, [], [cartStep], index)
1271
1272 stepChanger(sidebarSteps, [], [], index)
1273
1274 sidebarDataUpdate()
1275 }
1276
1277 function removeCartStep() {
1278 stepChanger(stepsArray, ['CartStep'], [], stepIndex.value)
1279
1280 stepChanger(sidebarSteps, ['CartStep'], [], stepIndex.value)
1281
1282 sidebarDataUpdate()
1283 }
1284
1285 /**
1286 * Detect if Selected Service has Extras
1287 * @type {any}
1288 */
1289 let selectedServiceExtras = computed(() => {
1290 let service = store.getters['entities/getService'](store.getters['booking/getServiceId'])
1291
1292 return service ? service.extras : []
1293 })
1294
1295 // * Appointment Waiting List: populate module on service selection
1296 watch(
1297 () => store.getters['booking/getServiceId'],
1298 (newServiceId) => {
1299 // Reset first
1300 store.dispatch('appointmentWaitingListOptions/resetWaitingOptions')
1301
1302 if (!newServiceId) return
1303
1304 let globalEnabled = !!amSettings.featuresIntegrations?.waitingListAppointments?.enabled
1305 if (!globalEnabled) return
1306
1307 let service = store.getters['entities/getService'](newServiceId)
1308 if (!service || !service.settings) return
1309
1310 let serviceSettings = null
1311 try {
1312 serviceSettings = JSON.parse(service.settings)
1313 } catch (e) {
1314 serviceSettings = null
1315 }
1316 if (!serviceSettings || !serviceSettings.waitingList || !serviceSettings.waitingList.enabled)
1317 return
1318
1319 const wl = serviceSettings.waitingList
1320 // Backend may not yet supply peopleWaiting for appointments; default 0
1321 let waitingPayload = {
1322 enabled: !!wl.enabled,
1323 maxCapacity: wl.maxCapacity || 0,
1324 maxExtraPeople: wl.maxExtraPeople || 0,
1325 maxExtraPeopleEnabled: !!wl.maxExtraPeopleEnabled,
1326 peopleWaiting: wl.peopleWaiting || 0,
1327 isWaitingListSlot: false,
1328 }
1329 store.commit('appointmentWaitingListOptions/setAllData', waitingPayload)
1330 },
1331 { immediate: true },
1332 )
1333
1334 /**
1335 * Add or Remove steps from Steps Array
1336 */
1337 function changeInitStepDataService() {
1338 // * Adding Extras Step
1339 if (
1340 selectedServiceExtras.value.length &&
1341 !stepsArray.value.find((step) => step.name === 'ExtrasStep')
1342 ) {
1343 if (shortcodeData.value.layout === '2') {
1344 let initStepsIndex = Math.max(
1345 stepsArray.value.findIndex((step) => step.name === 'ServiceStep'),
1346 stepsArray.value.findIndex((step) => step.name === 'EmployeeStep'),
1347 stepsArray.value.findIndex((step) => step.name === 'LocationStep'),
1348 )
1349 stepsArray.value.splice(initStepsIndex + 1, 0, extras)
1350 } else {
1351 stepsArray.value.splice(stepIndex.value + 1, 0, extras)
1352 }
1353
1354 sidebarDataUpdate()
1355 }
1356
1357 // * Removing Extras Step
1358 if (
1359 sidebarSteps.value.find((step) => step.name === 'ExtrasStep') &&
1360 !Object.keys(selectedServiceExtras.value).length
1361 ) {
1362 let extrasIndex = stepsArray.value.findIndex((step) => step.name === 'ExtrasStep')
1363 stepsArray.value.splice(extrasIndex, 1)
1364 sidebarSteps.value.splice(extrasIndex, 1)
1365 }
1366
1367 useInitCartItem(store)
1368 }
1369
1370 provide('initDataChanges', {
1371 changeInitStepDataService,
1372 })
1373
1374 // * Step index
1375 const stepIndex = ref(0)
1376 // * StepIndex provide
1377 provide('stepIndex', stepIndex)
1378
1379 // * Monitoring step index for step selection
1380 watch(stepIndex, (currStepIndex, prevStepIndex) => {
1381 if (currStepIndex < sidebarSteps.value.length) {
1382 sidebarSteps.value[prevStepIndex].selected = false
1383 sidebarSteps.value[currStepIndex].selected = true
1384 }
1385
1386 if (currStepIndex === sidebarSteps.value.length) {
1387 sidebarSteps.value[prevStepIndex].selected = false
1388 }
1389
1390 const step = sidebarSteps.value[currStepIndex]
1391 if (step?.label) {
1392 announceStatus(`${step.label}, step ${currStepIndex + 1} of ${sidebarSteps.value.length}`)
1393 }
1394 })
1395
1396 let goBackToPackageBooking = ref(false)
1397 provide('goBackToPackageBooking', goBackToPackageBooking)
1398
1399 /**
1400 * Move to previous Form Step
1401 */
1402 function previousStep() {
1403 footerBtnDisabledUpdater(false)
1404
1405 if (!navigateInsideStep.value) {
1406 if (stepsArray.value[stepIndex.value].name === packageInfoStep.name) {
1407 removePackageStep()
1408 }
1409
1410 if (stepsArray.value[stepIndex.value].name === recurringStep.name) {
1411 stepsArray.value.splice(stepIndex.value, 2)
1412 sidebarSteps.value.splice(stepIndex.value, 2)
1413
1414 if (useCartStep(store)) {
1415 stepsArray.value.splice(stepIndex.value, 0, cartStep)
1416
1417 stepChanger(sidebarSteps, [], [], stepIndex.value)
1418
1419 sidebarDataUpdate()
1420 }
1421 }
1422
1423 if (
1424 stepIndex.value > 0 &&
1425 stepsArray.value[stepIndex.value - 1]?.name === recurringSummary.name
1426 ) {
1427 preventRebuildRecurringBookingData.value = true
1428 }
1429
1430 if (stepsArray.value[stepIndex.value].name === packageAppointmentsListStep.name) {
1431 goBackToPackageBooking.value = true
1432 }
1433
1434 stepIndex.value = stepIndex.value - 1
1435 }
1436
1437 headerButtonPreviousClicked.value = !headerButtonPreviousClicked.value
1438 }
1439
1440 /**
1441 * Move to next Form Step
1442 */
1443 function nextStep() {
1444 if (!navigateInsideStep.value) {
1445 sidebarSteps.value[stepIndex.value].finished = true
1446 stepIndex.value = stepIndex.value + 1
1447 }
1448 }
1449
1450 // * Footer states
1451 let loading = computed(() => store.getters['booking/getLoading'])
1452 let booked = computed(() => store.getters['booking/getBooked'])
1453 let paymentGateway = computed(() => store.getters['booking/getPaymentGateway'])
1454
1455 // * Footer btn flag
1456 let footerButtonClicked = ref(false)
1457 let footerBtnDisabled = ref(false)
1458
1459 let headerButtonPreviousClicked = ref(false)
1460
1461 /**
1462 * Footer btn clicked
1463 */
1464 function footerButtonClick() {
1465 footerButtonClicked.value = true
1466 }
1467
1468 /**
1469 * Footer btn reset
1470 */
1471 function footerButtonReset() {
1472 footerButtonClicked.value = false
1473 }
1474
1475 /**
1476 * Header btn previous clicked
1477 */
1478 function headerButtonPreviousClick() {
1479 headerButtonPreviousClicked.value = true
1480 }
1481
1482 /**
1483 * Header btn previous reset
1484 */
1485 function headerButtonPreviousReset() {
1486 headerButtonPreviousClicked.value = false
1487 }
1488
1489 function footerBtnDisabledUpdater(data) {
1490 footerBtnDisabled.value = data
1491 }
1492
1493 function secondButtonClick() {
1494 if (booked.value) {
1495 if (booked.value.customerCabinetUrl) {
1496 window.location.href = booked.value.customerCabinetUrl
1497 } else {
1498 window.location.href = amSettings.roles.customerCabinet.pageUrl
1499 }
1500 }
1501 }
1502
1503 provide('secondButton', {
1504 secondButtonClick,
1505 })
1506
1507 let navigateInsideStep = ref(false)
1508
1509 provide('changingStepsFunctions', {
1510 nextStep,
1511 previousStep,
1512 footerButtonClick,
1513 footerButtonReset,
1514 footerBtnDisabledUpdater,
1515 headerButtonPreviousClick,
1516 headerButtonPreviousReset,
1517 footerBtnDisabled,
1518 footerButtonClicked,
1519 headerButtonPreviousClicked,
1520 navigateInsideStep,
1521 })
1522
1523 // * Array of Sidebar steps
1524 const sidebarSteps = ref([])
1525 provide('sidebarSteps', sidebarSteps)
1526
1527 let keepPaymentStep = computed(() =>
1528 !empty.value
1529 ? useCart(store).length
1530 ? usePrepaidPrice(store) !== 0
1531 : useCheckingIfAllNotFree(store)
1532 : true,
1533 )
1534
1535 // Appointment waiting list flag
1536 const isWaitingListBooking = computed(
1537 () => store.getters['appointmentWaitingListOptions/getIsWaitingListSlot'],
1538 )
1539
1540 watchEffect(() => {
1541 // If waiting list, remove payment step
1542 if (isWaitingListBooking.value) {
1543 removePaymentsStep()
1544 return
1545 }
1546 if (!isRestored.value && !keepPaymentStep.value) {
1547 removePaymentsStep()
1548 } else if (!isRestored.value) {
1549 if (stepsArray.value.indexOf(paymentStep) === -1) {
1550 addPaymentsStep()
1551 }
1552 }
1553 })
1554
1555 /**
1556 * Collecting and rearranging selected data in form sidebar based on item position in form
1557 * @param data
1558 */
1559 function sidebarDataCollector(data) {
1560 // Determines are selected data already exists
1561 if (
1562 sidebarSteps.value[stepIndex.value].stepSelectedData.filter(
1563 (item) => item.reference === data.reference,
1564 ).length
1565 ) {
1566 sidebarSteps.value[stepIndex.value].stepSelectedData.forEach((item, index, array) => {
1567 // Handles changing value of existing data
1568 if (item.reference === data.reference && data.value && data.value !== item.value) {
1569 item.value = data.value
1570 }
1571
1572 // Removes un-selected form data from sidebar array
1573 if (item.reference === data.reference && !data.value) {
1574 array.splice(index, 1)
1575 }
1576 })
1577 } else {
1578 if (data.value) {
1579 sidebarSteps.value[stepIndex.value].stepSelectedData.push(data)
1580 }
1581 }
1582
1583 // Rearranges data in sidebar based on item position in the form
1584 sidebarSteps.value[stepIndex.value].stepSelectedData.sort((a, b) => {
1585 return a.position - b.position
1586 })
1587
1588 const currentStep = sidebarSteps.value[stepIndex.value]
1589 const selections = currentStep.stepSelectedData
1590 .map((item) => item.value)
1591 .filter(Boolean)
1592 .join(', ')
1593
1594 if (selections) {
1595 announceStatus(`${currentStep.label}: ${selections}`)
1596 }
1597 }
1598
1599 provide('sidebarStepsFunctions', {
1600 sidebarDataCollector,
1601 })
1602
1603 /**
1604 * Function that update SideBar Data Array when some component are dynamically added
1605 */
1606 function sidebarDataUpdate() {
1607 stepsArray.value.forEach((item, index) => {
1608 if (item.name === 'CongratulationsStep') {
1609 return
1610 }
1611 if (!sidebarSteps.value.find((block) => block.labelKey === item.sidebarData.label)) {
1612 let labelKey = item.sidebarData.label
1613 let step = {
1614 name: item.name,
1615 key: item.key,
1616 label: amLabels.value[labelKey],
1617 labelKey: labelKey,
1618 icon: item.sidebarData.icon,
1619 stepSelectedData: [],
1620 finished: false,
1621 selected: index === stepIndex.value,
1622 }
1623 sidebarSteps.value.splice(index, 0, step)
1624 }
1625 })
1626 }
1627
1628 // * Cart
1629 function addToCart() {
1630 sidebarSteps.value.forEach((item) => {
1631 item.finished = false
1632 item.selected = false
1633 })
1634
1635 stepsArray.value.forEach((item) => {
1636 item.finished = false
1637 item.selected = false
1638 })
1639
1640 stepIndex.value = 0
1641
1642 useInitSelection(store, true)
1643
1644 useAddToCart(store)
1645
1646 useInitCartItem(store)
1647
1648 const cartCount = useCartHasItems(store)
1649 announceStatus(
1650 `${amLabels.value.added_to_cart || 'Added to cart'}. ${cartCount} ${cartCount === 1 ? amLabels.value.appointment : amLabels.value.appointments} ${amLabels.value.in_cart}.`,
1651 )
1652 }
1653
1654 function backToCart() {
1655 let currentCartItem = store.getters['booking/getCurrentCartItem']
1656
1657 let items = store.getters['booking/getAllMultipleAppointments']
1658
1659 if (currentCartItem) {
1660 store.commit('booking/setCartItem', currentCartItem)
1661
1662 store.commit('booking/setCartItemIndex', items.length - 1)
1663
1664 store.commit('booking/setCurrentCartItem', null)
1665 } else {
1666 items.pop()
1667
1668 store.commit('booking/setCartItemIndex', items.length - 1)
1669
1670 store.commit('booking/setServiceId', items[items.length - 1].serviceId)
1671
1672 store.commit(
1673 'booking/setEmployeeId',
1674 items[items.length - 1].services[items[items.length - 1].serviceId].providerId,
1675 )
1676
1677 store.commit(
1678 'booking/setLocationId',
1679 items[items.length - 1].services[items[items.length - 1].serviceId].locationId,
1680 )
1681 }
1682
1683 useGoToCartStep(stepsArray, stepIndex)
1684 }
1685
1686 // * Colors block
1687 let amColors = computed(() => {
1688 return amSettings.customizedData && amSettings.customizedData.sbsNew
1689 ? amSettings.customizedData.sbsNew.colors
1690 : defaultCustomizeSettings.sbsNew.colors
1691 })
1692 provide('amColors', amColors)
1693
1694 let borderCalculation = computed(() =>
1695 containerWidth.value !== 0 && containerWidth.value > 560
1696 ? sidebarVisibility.value || sidebarCollapsed.value
1697 : false,
1698 )
1699
1700 let cssVars = computed(() => {
1701 return {
1702 '--am-c-primary': amColors.value.colorPrimary,
1703 '--am-c-success': amColors.value.colorSuccess,
1704 '--am-c-error': amColors.value.colorError,
1705 '--am-c-warning': amColors.value.colorWarning,
1706 '--am-c-main-bgr': amColors.value.colorMainBgr,
1707 '--am-c-main-heading-text': amColors.value.colorMainHeadingText,
1708 '--am-c-main-text': amColors.value.colorMainText,
1709 '--am-c-sb-bgr': amColors.value.colorSbBgr,
1710 '--am-c-sb-text': amColors.value.colorSbText,
1711 '--am-c-inp-bgr': amColors.value.colorInpBgr,
1712 '--am-c-inp-border': amColors.value.colorInpBorder,
1713 '--am-c-inp-text': amColors.value.colorInpText,
1714 '--am-c-inp-placeholder': amColors.value.colorInpPlaceHolder,
1715 '--am-c-drop-bgr': amColors.value.colorDropBgr,
1716 '--am-c-drop-text': amColors.value.colorDropText,
1717 '--am-c-btn-prim': amColors.value.colorBtnPrim,
1718 '--am-c-btn-prim-text': amColors.value.colorBtnPrimText,
1719 '--am-c-btn-sec': amColors.value.colorBtnSec,
1720 '--am-c-btn-sec-text': amColors.value.colorBtnSecText,
1721 '--am-c-skeleton-op20': useColorTransparency(amColors.value.colorMainText, 0.2),
1722 '--am-c-skeleton-op60': useColorTransparency(amColors.value.colorMainText, 0.6),
1723 '--am-c-skeleton-sb-op20': useColorTransparency(amColors.value.colorSbText, 0.2),
1724 '--am-c-skeleton-sb-op60': useColorTransparency(amColors.value.colorSbText, 0.6),
1725 '--am-font-family': amFonts.value.fontFamily,
1726
1727 // css properties
1728 // -mw- max width
1729 // -brad- border-radius
1730 '--am-mw-main': sidebarVisibility.value
1731 ? sidebarCollapsed.value
1732 ? '592px'
1733 : '760px'
1734 : '520px',
1735 '--am-brad-main': borderCalculation.value
1736 ? isRtl.value
1737 ? '0.5rem 0 0 0.5rem'
1738 : '0 0.5rem 0.5rem 0'
1739 : '0.5rem',
1740 }
1741 })
1742
1743 function activateCustomFontStyles() {
1744 let head = document.head || document.getElementsByTagName('head')[0]
1745 if (head.querySelector('#amCustomFont')) {
1746 head.querySelector('#amCustomFont').remove()
1747 }
1748
1749 let css = `@font-face {font-family: '${amFonts.value.fontFamily}'; src: url(${amFonts.value.fontUrl});}`
1750 let style = document.createElement('style')
1751 head.appendChild(style)
1752 style.setAttribute('type', 'text/css')
1753 style.setAttribute('id', 'amCustomFont')
1754 style.appendChild(document.createTextNode(css))
1755 }
1756
1757 if (amFonts.value.customFontSelected) activateCustomFontStyles()
1758
1759 // * Design Properties
1760 let amDesignProperties = computed(() => {
1761 return {
1762 colorInputBorderRadius: '6px',
1763 }
1764 })
1765 provide('amDesignProperties', amDesignProperties)
1766
1767 onMounted(() => {
1768 useRenderAction('renderForm', {
1769 ameliaContainer,
1770 containerWidth,
1771 empty,
1772 amCustomize,
1773 amFonts,
1774 sidebarCollapsed,
1775 sidebarCollapseItemsClass,
1776 sidebarFooterRef,
1777 sidebarFooterHeight,
1778 sidebarVisibility,
1779 })
1780 })
1781 </script>
1782
1783 <style lang="scss">
1784 @import '../../../assets/scss/public/overides/overides';
1785 @import '../../../assets/scss/common/reset/reset';
1786 @import '../../../assets/scss/common/icon-fonts/style';
1787 @import '../../../assets/scss/common/skeleton/skeleton.scss';
1788
1789 :root {
1790 // Colors
1791 // shortcuts
1792 // -c- color
1793 // -bgr- background
1794 // -prim- primary
1795 // -sec- secondary
1796 // primitive colors
1797 --am-c-primary: #{$blue-1000};
1798 --am-c-success: #{$green-1000};
1799 --am-c-error: #{$red-900};
1800 --am-c-warning: #{$yellow-1000};
1801 // main container colors - right part of the form
1802 --am-c-main-bgr: #{$am-white};
1803 --am-c-main-heading-text: #{$shade-800};
1804 --am-c-main-text: #{$shade-900};
1805 // sidebar container colors - left part of the form
1806 --am-c-sb-bgr: #17295a;
1807 --am-c-sb-text: #{$am-white};
1808 // input global colors - usage input, textarea, checkbox, radio button, select input, adv select input
1809 --am-c-inp-bgr: #{$am-white};
1810 --am-c-inp-border: #{$shade-250};
1811 --am-c-inp-text: #{$shade-900};
1812 --am-c-inp-placeholder: #{$shade-500};
1813 // dropdown global colors - usage select dropdown, adv select dropdown
1814 --am-c-drop-bgr: #{$am-white};
1815 --am-c-drop-text: #{$shade-1000};
1816 // button global colors
1817 --am-c-btn-prim: #{$blue-900};
1818 --am-c-btn-prim-text: #{$am-white};
1819 --am-c-btn-sec: #{$am-white};
1820 --am-c-btn-sec-text: #{$shade-900};
1821
1822 // Properties
1823 // shortcuts
1824 // -h- height
1825 // -fs- font size
1826 // -rad- border radius
1827 --am-h-inp: 40px;
1828 --am-fs-inp: 15px;
1829 --am-rad-inp: 6px;
1830 --am-fs-label: 15px;
1831 --am-fs-btn: 15px;
1832
1833 // Font
1834 --am-font-family: 'Amelia Roboto', sans-serif;
1835 }
1836
1837 // am -- amelia
1838 // fs -- form steps
1839 // sb -- sidebar
1840 .amelia-v2-booking {
1841 background-color: transparent;
1842
1843 #amelia-container {
1844 background-color: transparent;
1845 * {
1846 font-family: var(--am-font-family);
1847 font-style: initial;
1848 box-sizing: border-box;
1849 word-break: break-word;
1850 letter-spacing: normal;
1851 }
1852
1853 &.am-fs {
1854 // Container Wrapper
1855 &__wrapper {
1856 display: flex;
1857 justify-content: center;
1858 max-width: var(--am-mw-main);
1859 width: 100%;
1860 height: 560px;
1861 margin: 100px auto;
1862 border-radius: 8px;
1863 box-shadow: 0 30px 40px rgba(0, 0, 0, 0.12);
1864 transition: max-width 0.3s ease-in-out;
1865
1866 &.am-collapsed {
1867 transition-delay: 1s;
1868 }
1869
1870 .el-form {
1871 &-item {
1872 display: block;
1873 font-family: var(--am-font-family);
1874 font-size: var(--am-fs-label);
1875 margin-bottom: 24px;
1876
1877 &__label {
1878 flex: 0 0 auto;
1879 text-align: left;
1880 font-size: var(--am-fs-label);
1881 line-height: 1.3;
1882 color: var(--am-c-main-text);
1883 box-sizing: border-box;
1884 margin: 0;
1885
1886 &:before {
1887 color: var(--am-c-error);
1888 }
1889 }
1890
1891 &__content {
1892 display: flex;
1893 flex-wrap: wrap;
1894 align-items: center;
1895 flex: 1;
1896 position: relative;
1897 font-size: var(--am-fs-inp);
1898 min-width: 0;
1899 color: var(--am-c-main-text);
1900 }
1901
1902 &__error {
1903 font-size: 12px;
1904 color: var(--am-c-error);
1905 padding-top: 4px;
1906 }
1907 }
1908 }
1909
1910 * {
1911 font-family: var(--am-font-family);
1912 box-sizing: border-box;
1913 }
1914 }
1915 }
1916 }
1917
1918 .am-no-services {
1919 box-shadow: 0 30px 40px rgba(0, 0, 0, 0.12);
1920 margin: 100px auto;
1921 text-align: center;
1922 padding: 56px;
1923 max-width: 760px;
1924 height: 460px;
1925 width: 100%;
1926 background: var(--am-c-main-bgr);
1927 border-radius: 8px;
1928
1929 p {
1930 margin-bottom: 8px;
1931 padding: 0;
1932 }
1933
1934 &-oops {
1935 font-size: 24px;
1936 line-height: 1.5;
1937 font-weight: 400;
1938 color: #354052;
1939 margin: 0;
1940 }
1941
1942 &-text {
1943 font-size: 16px;
1944 line-height: 1.5;
1945 font-weight: 400;
1946 color: #354052;
1947 &-2 {
1948 color: #354052;
1949 font-size: 14px;
1950 }
1951 }
1952
1953 a {
1954 font-size: 14px;
1955 }
1956
1957 * {
1958 font-family: 'Amelia Roboto', sans-serif;
1959 box-sizing: border-box;
1960 }
1961 }
1962 }
1963 </style>
1964