PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 2.4.6
Booking for Appointments and Events Calendar – Amelia v2.4.6
2.4.8 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 / EventForm / Common / EventBookingDialog / EventBookingDialog.vue
ameliabooking / v3 / src / views / public / EventForm / Common / EventBookingDialog Last commit date
EventBookingDialog.vue 1 month ago
EventBookingDialog.vue
613 lines
1 <template>
2 <AmDialog
3 v-model="popupVisible"
4 width="650px"
5 :append-to-body="true"
6 :destroy-on-close="true"
7 :show-close="props.ready && !props.loading && !props.loadingUpcoming"
8 :close-on-click-modal="false"
9 :close-on-press-escape="false"
10 modal-class="amelia-v2-booking am-dialog-el"
11 :custom-styles="props.cssVars"
12 :aria-labelledby="dialogTitleId"
13 @close="onCloseEventDialog"
14 @closed="onClosedEventDialog"
15 @opened="onOpenedEventDialog"
16 >
17 <template #header>
18 <!-- Visually-hidden dialog title for screen readers -->
19 <div :id="dialogTitleId" class="am-sr-only">
20 {{ customizedStepLabels.event_booking || labels.event_booking || 'Event Booking' }}
21 </div>
22
23 <EventListHeader
24 :ready="props.ready"
25 :loading="props.loading"
26 :loading-upcoming="props.loadingUpcoming"
27 :customized-labels="customizedStepLabels"
28 />
29 </template>
30
31 <template #default>
32 <div
33 id="amelia-container"
34 ref="dialogContainer"
35 :class="'am-' + props.formKey"
36 :style="props.cssVars"
37 >
38 <component
39 :is="currentStep"
40 v-if="currentStep"
41 :in-dialog="true"
42 global-class="am-dialog-el__main-container"
43 />
44 </div>
45 </template>
46
47 <template #footer>
48 <EventListFooter
49 :ready="props.ready"
50 :loading="props.loading"
51 :loading-upcoming="props.loadingUpcoming"
52 :second-button-show="secBtnShow && customerCabinetButton"
53 :payment-gateway="store.getters['payment/getPaymentGateway']"
54 :customized-labels="customizedStepLabels"
55 :primary-footer-button-type="bookingBtnType"
56 :secondary-footer-button-type="customizedStepOptions.secBtn.buttonType"
57 :is-waiting-list="isWaitingList"
58 />
59 </template>
60 </AmDialog>
61 </template>
62
63 <script setup>
64 import { computed, inject, markRaw, nextTick, provide, ref, watch, onMounted } from 'vue'
65 import { useStore } from 'vuex'
66
67 import { defaultCustomizeSettings } from '@/assets/js/common/defaultCustomize'
68 import useRestore from '@/assets/js/public/restore'
69 import { useScrollTo } from '@/assets/js/common/scrollElements'
70 import { useWaitingListAvailability } from '@/assets/js/public/events'
71
72 import AmDialog from '@/views/_components/dialog/AmDialog.vue'
73 import EventListHeader from '@/views/common/EventListFormConstruction/Header/Header.vue'
74 import EventListFooter from '@/views/common/EventListFormConstruction/Footer/Footer.vue'
75
76 import EventInfo from '../EventInfo/EventInfo.vue'
77 import EventTickets from '../EventTickets/EventTickets.vue'
78 import EventCustomerInfo from '../EventCustomerInfo/EventCustomerInfo.vue'
79 import EventPayment from '../EventPayment/EventPayment.vue'
80 import EventCongratulations from '../Congratulations/Congratulations.vue'
81
82 const props = defineProps({
83 modelValue: {
84 type: Boolean,
85 default: false,
86 },
87 formKey: {
88 type: String,
89 required: true,
90 },
91 ready: {
92 type: Boolean,
93 required: true,
94 },
95 loading: {
96 type: Boolean,
97 required: true,
98 },
99 loadingUpcoming: {
100 type: Boolean,
101 default: false,
102 },
103 cssVars: {
104 type: Object,
105 required: true,
106 },
107 resetEventIdOnClose: {
108 type: Boolean,
109 default: true,
110 },
111 skipEventTicketsStep: {
112 type: Boolean,
113 default: false,
114 },
115 })
116
117 const emit = defineEmits(['update:modelValue'])
118
119 const uid = Math.random().toString(36).slice(2, 8)
120 const dialogTitleId = `am-elf-dialog-title-${uid}`
121
122 const popupVisible = computed({
123 get: () => props.modelValue,
124 set: (value) => emit('update:modelValue', value),
125 })
126
127 const store = useStore()
128 const amSettings = inject('settings')
129 const labels = inject('labels')
130 const localLanguage = inject('localLanguage')
131 const shortcodeData = inject('shortcodeData')
132
133 const eventInfo = markRaw(EventInfo)
134 const eventTickets = markRaw(EventTickets)
135 const eventCustomerInfo = markRaw(EventCustomerInfo)
136 const eventPayment = markRaw(EventPayment)
137 const eventCongratulations = markRaw(EventCongratulations)
138
139 const stepIndex = ref(0)
140 provide('stepIndex', stepIndex)
141
142 const baseSteps = !props.skipEventTicketsStep
143 ? [eventInfo, eventCustomerInfo, eventCongratulations]
144 : [eventCustomerInfo, eventCongratulations]
145 const stepsArray = ref(baseSteps)
146 provide('stepsArray', stepsArray)
147
148 const currentStep = computed(() => stepsArray.value[stepIndex.value] ?? null)
149
150 const eventFluidStepKey = ref([])
151 provide('eventFluidStepKey', eventFluidStepKey)
152
153 function removeStepComponent(component) {
154 const arr = stepsArray.value
155 const index = arr.indexOf(component)
156 if (index > -1) {
157 arr.splice(index, 1)
158 }
159 }
160
161 watch(
162 eventFluidStepKey,
163 (keys) => {
164 const wantTickets = !props.skipEventTicketsStep && keys.indexOf('eventTickets') > -1
165 const wantPayment = keys.indexOf('eventPayment') > -1
166
167 if (!wantTickets) {
168 removeStepComponent(eventTickets)
169 }
170 if (!wantPayment) {
171 removeStepComponent(eventPayment)
172 }
173
174 const arr = stepsArray.value
175
176 if (wantTickets && arr.indexOf(eventTickets) === -1) {
177 const infoIdx = arr.findIndex((step) => step.name === 'EventInfo')
178 const insertAt = infoIdx > -1 ? infoIdx + 1 : Math.min(stepIndex.value + 1, arr.length)
179 arr.splice(insertAt, 0, eventTickets)
180 }
181
182 if (wantPayment && arr.indexOf(eventPayment) === -1) {
183 const customerIdx = arr.findIndex((step) => step.name === 'EventCustomerInfo')
184 const insertAt =
185 customerIdx > -1 ? customerIdx + 1 : Math.min(stepIndex.value + 1, arr.length)
186 arr.splice(insertAt, 0, eventPayment)
187 }
188
189 const len = stepsArray.value.length
190 if (len === 0) {
191 stepIndex.value = 0
192 } else {
193 stepIndex.value = Math.max(0, Math.min(stepIndex.value, len - 1))
194 }
195 },
196 { deep: true },
197 )
198
199 // * Waiting list: vuex options aligned with selected event (parity with Events Calendar dialog flow).
200 const selectedEvent = computed(() =>
201 store.getters['eventEntities/getEvent'](store.getters['eventBooking/getSelectedEventId']),
202 )
203
204 let isWaitingList = computed(() => useWaitingListAvailability(selectedEvent.value))
205
206 function syncEventWaitingListOptionsForSelectedEvent() {
207 const event = selectedEvent.value
208 if (event && useWaitingListAvailability(event)) {
209 const waitingOptions =
210 event.waitingList != null ? JSON.parse(JSON.stringify(event.waitingList)) : {}
211 store.commit('eventWaitingListOptions/setAllData', { ...waitingOptions, isAvailable: true })
212 } else {
213 store.dispatch('eventWaitingListOptions/resetWaitingOptions')
214 }
215 }
216
217 watch(selectedEvent, (event) => {
218 if (event == null) {
219 return
220 }
221 syncEventWaitingListOptionsForSelectedEvent()
222 })
223
224 const bringingAnyoneVisibility = ref(false)
225 provide('bringingAnyoneVisibility', bringingAnyoneVisibility)
226
227 const booked = computed(() => store.getters['eventBooking/getBooked'])
228 const footerButtonClicked = ref(false)
229 const footerBtnDisabled = ref(false)
230 const headerButtonPreviousClicked = ref(false)
231
232 const ifBookedCustomerCabinetUrl = computed(() => {
233 if (booked.value !== null) {
234 return booked.value.customerCabinetUrl.length > 0
235 }
236
237 return true
238 })
239
240 const customerCabinetButton = computed(() => {
241 const step = currentStep.value
242 if (!step) {
243 return true
244 }
245 if (step.name === 'CongratulationsStep') {
246 return !!amSettings.roles.customerCabinet.pageUrl && ifBookedCustomerCabinetUrl.value
247 }
248
249 return true
250 })
251
252 function nextStep() {
253 store.commit('setLoading', true)
254 stepIndex.value =
255 stepIndex.value === stepsArray.value.length - 1 ? stepIndex.value : stepIndex.value + 1
256 }
257
258 function previousStep() {
259 store.commit('setLoading', true)
260 stepIndex.value = stepIndex.value - 1
261 headerButtonPreviousClicked.value = !headerButtonPreviousClicked.value
262 }
263
264 function footerButtonClick() {
265 footerButtonClicked.value = true
266
267 const dialogEl = document.getElementsByClassName('am-dialog-el')[0]
268 if (dialogEl && dialogEl.children.length) {
269 useScrollTo(dialogEl.children[0], dialogEl.children[0].children[0], 50, 300)
270 }
271 }
272
273 function footerButtonReset() {
274 footerButtonClicked.value = false
275 }
276
277 function footerBtnDisabledUpdater(data) {
278 footerBtnDisabled.value = data
279 }
280
281 function headerButtonPreviousClick() {
282 headerButtonPreviousClicked.value = true
283 }
284
285 function headerButtonPreviousReset() {
286 headerButtonPreviousClicked.value = false
287 }
288
289 function secondButtonClick() {
290 const step = currentStep.value
291 if (step?.name === 'CongratulationsStep' && booked.value) {
292 window.location.href = booked.value.customerCabinetUrl
293 } else {
294 popupVisible.value = false
295 }
296 }
297
298 provide('secondButton', { secondButtonClick })
299 provide('changingStepsFunctions', {
300 nextStep,
301 previousStep,
302 footerButtonClick,
303 footerButtonReset,
304 footerBtnDisabledUpdater,
305 footerButtonClicked,
306 footerBtnDisabled,
307 headerButtonPreviousClick,
308 headerButtonPreviousReset,
309 headerButtonPreviousClicked,
310 })
311
312 const dialogContainer = ref(null)
313 const dialogWidth = ref(0)
314 provide('dialogWidth', dialogWidth)
315
316 const injectedCustomizedDataForm = inject('customizedDataForm', null)
317 const customizedDataForm =
318 injectedCustomizedDataForm ??
319 computed(() => {
320 if (amSettings.customizedData && props.formKey in amSettings.customizedData) {
321 return amSettings.customizedData[props.formKey]
322 }
323
324 return defaultCustomizeSettings[props.formKey]
325 })
326 if (!injectedCustomizedDataForm) {
327 provide('customizedDataForm', customizedDataForm)
328 }
329
330 const langDetection = computed(() => amSettings.general.usedLanguages.includes(localLanguage.value))
331 const customizedStepLabels = computed(() => {
332 const customLabels = {}
333
334 const step = currentStep.value
335 if (!step) {
336 return labels
337 }
338
339 const stepKey = step.key
340 const customizedLabels = customizedDataForm.value[stepKey].translations
341 if (customizedLabels) {
342 Object.keys(customizedLabels).forEach((labelKey) => {
343 if (customizedLabels[labelKey][localLanguage.value] && langDetection.value) {
344 customLabels[labelKey] = customizedLabels[labelKey][localLanguage.value]
345 } else if (customizedLabels[labelKey].default) {
346 customLabels[labelKey] = customizedLabels[labelKey].default
347 }
348 })
349 }
350
351 return Object.keys(customLabels).length ? customLabels : labels
352 })
353
354 const customizedStepOptions = computed(() => {
355 const step = currentStep.value
356 if (!step) {
357 return {}
358 }
359 return customizedDataForm.value[step.key].options
360 })
361
362 const secBtnShow = computed(() => {
363 if (
364 'secBtn' in customizedStepOptions.value &&
365 'visibility' in customizedStepOptions.value.secBtn
366 ) {
367 return customizedStepOptions.value.secBtn.visibility
368 }
369
370 return true
371 })
372
373 const bookingBtnType = computed(() => {
374 const step = currentStep.value
375 if (isWaitingList.value && step?.key === 'info') {
376 if ('waitingBtn' in customizedStepOptions.value) {
377 return customizedStepOptions.value.waitingBtn.buttonType
378 }
379
380 return 'filled'
381 }
382
383 return customizedStepOptions.value.primBtn?.buttonType ?? 'filled'
384 })
385
386 function onCloseEventDialog() {
387 if (currentStep.value?.name === 'CongratulationsStep') {
388 window.location.reload()
389 }
390
391 eventFluidStepKey.value = []
392 stepIndex.value = 0
393 }
394
395 function onClosedEventDialog() {
396 nextTick(() => {
397 if (props.resetEventIdOnClose) {
398 store.commit('eventBooking/setEventId', null)
399 }
400
401 store.dispatch('persons/resetPersons')
402 store.dispatch('coupon/resetCoupon')
403 store.dispatch('tickets/resetCustomTickets')
404 store.dispatch('eventWaitingListOptions/resetWaitingOptions')
405 store.dispatch('payment/resetPaymentData')
406 })
407 }
408
409 function onOpenedEventDialog() {
410 nextTick(() => {
411 syncEventWaitingListOptionsForSelectedEvent()
412
413 if (dialogContainer.value) {
414 dialogWidth.value = dialogContainer.value.offsetWidth
415 }
416
417 if (
418 props.skipEventTicketsStep &&
419 stepIndex.value === 0 &&
420 stepsArray.value.length > 1 &&
421 stepsArray.value[0].name === 'EventInfo'
422 ) {
423 stepIndex.value = 1
424 }
425 })
426 }
427
428 onMounted(() => {
429 const restore = useRestore(store, shortcodeData.value)
430 if (!restore) {
431 return
432 }
433
434 stepsArray.value.splice(0, stepsArray.value.length)
435 stepIndex.value = 0
436
437 restore.steps.forEach((key) => {
438 if (props.skipEventTicketsStep && key === 'EventTickets') {
439 return
440 }
441
442 switch (key) {
443 case 'EventInfo':
444 stepsArray.value.push(eventInfo)
445 break
446 case 'EventTickets':
447 stepsArray.value.push(eventTickets)
448 break
449 case 'EventCustomerInfo':
450 stepsArray.value.push(eventCustomerInfo)
451 break
452 case 'EventPayment':
453 stepsArray.value.push(eventPayment)
454 break
455 case 'CongratulationsStep':
456 stepsArray.value.push(eventCongratulations)
457 break
458 }
459
460 stepIndex.value++
461 })
462
463 let index = -1
464 if (restore.result === 'success') {
465 index = stepsArray.value.length - 1
466 } else if (restore.result === 'error' || restore.result === 'canceled') {
467 index = stepsArray.value.length - 2
468 }
469
470 stepIndex.value = index
471 popupVisible.value = true
472 })
473 </script>
474
475 <style lang="scss">
476 @import '@/assets/scss/public/overides/overides';
477 @import '@/assets/scss/common/reset/reset';
478 @import '@/assets/scss/common/icon-fonts/style';
479 @import '@/assets/scss/common/skeleton/skeleton.scss';
480 @import '@/assets/scss/common/empty-state/_empty-state-mixin.scss';
481 @import '@/assets/scss/common/transitions/_transitions-mixin.scss';
482
483 :root {
484 // Colors
485 // shortcuts
486 // -c- color
487 // -bgr- background
488 // -prim- primary
489 // -sec- secondary
490 // primitive colors
491 --am-c-primary: #{$blue-1000};
492 --am-c-success: #{$green-1000};
493 --am-c-error: #{$red-900};
494 --am-c-warning: #{$yellow-1000};
495 // main container colors - right part of the form
496 --am-c-main-bgr: #{$am-white};
497 --am-c-main-heading-text: #{$shade-800};
498 --am-c-main-text: #{$shade-900};
499 // sidebar container colors - left part of the form
500 --am-c-sb-bgr: #17295a;
501 --am-c-sb-text: #{$am-white};
502 // input global colors - usage input, textarea, checkbox, radio button, select input, adv select input
503 --am-c-inp-bgr: #{$am-white};
504 --am-c-inp-border: #{$shade-250};
505 --am-c-inp-text: #{$shade-900};
506 --am-c-inp-placeholder: #{$shade-500};
507 // dropdown global colors - usage select dropdown, adv select dropdown
508 --am-c-drop-bgr: #{$am-white};
509 --am-c-drop-text: #{$shade-1000};
510 // button global colors
511 --am-c-btn-prim: #{$blue-900};
512 --am-c-btn-prim-text: #{$am-white};
513 --am-c-btn-sec: #{$am-white};
514 --am-c-btn-sec-text: #{$shade-900};
515
516 // Properties
517 // shortcuts
518 // -h- height
519 // -fs- font size
520 // -rad- border radius
521 --am-h-inp: 40px;
522 --am-fs-inp: 15px;
523 --am-rad-inp: 6px;
524 --am-fs-label: 15px;
525 --am-fs-btn: 15px;
526
527 // Font
528 --am-font-family: 'Amelia Roboto', sans-serif;
529 }
530
531 .amelia-v2-booking {
532 /* Event list dialog */
533 // el - event list
534 &.am-dialog-el.am-dialog-popup {
535 background-color: rgba(0, 0, 0, 0.5);
536 z-index: 99999999 !important;
537
538 .el-dialog {
539 // am - amelia
540 border-radius: 8px;
541 overflow: hidden;
542 background-color: var(--am-c-main-bgr);
543
544 * {
545 box-sizing: border-box;
546 font-family: var(--am-font-family), sans-serif;
547 word-break: break-word;
548 }
549
550 @media only screen and (max-width: 768px) {
551 margin-top: 0;
552 }
553
554 // Visually hidden utility – content accessible to screen readers only
555 .am-sr-only {
556 position: absolute;
557 width: 1px;
558 height: 1px;
559 padding: 0;
560 margin: -1px;
561 overflow: hidden;
562 clip: rect(0, 0, 0, 0);
563 white-space: nowrap;
564 border: 0;
565 }
566 }
567 }
568
569 .el-skeleton {
570 width: 100%;
571 padding: 16px 32px;
572 }
573
574 * {
575 font-family: var(--am-font-family, 'Amelia Roboto'), sans-serif;
576 font-style: initial;
577 box-sizing: border-box;
578 word-break: break-word;
579 }
580
581 // elf - events list form; ecf - events calendar form
582 &.am-elf,
583 &.am-ecf {
584 margin: 0 auto;
585 max-width: var(--am-mw-main);
586 background-color: var(--am-c-main-bgr);
587 }
588
589 // el - event list
590 .am-dialog-el {
591 &__main-container {
592 @media only screen and (max-width: 768px) {
593 overflow: visible;
594 }
595
596 &::-webkit-scrollbar {
597 width: 6px;
598 }
599
600 &::-webkit-scrollbar-thumb {
601 border-radius: 6px;
602 background: var(--am-c-scroll-op30);
603 }
604
605 &::-webkit-scrollbar-track {
606 border-radius: 6px;
607 background: var(--am-c-scroll-op10);
608 }
609 }
610 }
611 }
612 </style>
613