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 / DateTimeStep / DateTimeStep.vue
ameliabooking / v3 / src / views / public / StepForm / DateTimeStep Last commit date
DateTimeStep.vue 3 weeks ago
DateTimeStep.vue
652 lines
1 <template>
2 <div
3 ref="dateTimeRef"
4 class="am-fs-dt__calendar"
5 :class="[props.globalClass, { 'am-oxvisible': packagesVisibility }]"
6 >
7 <div v-if="limitPerEmployeeError" ref="limitError" class="am-fs__payments-error">
8 <AmAlert type="error" :title="limitPerEmployeeError" :show-icon="true" :closable="false">
9 </AmAlert>
10 </div>
11
12 <Calendar
13 :id="0"
14 :preselect-slot="false"
15 :load-counter="loadCounter"
16 :end-time="amCustomize.dateTimeStep.options.endTimeVisibility.visibility"
17 :time-zone="amCustomize.dateTimeStep.options.timeZoneVisibility.visibility"
18 :show-busy-slots="busyTimeSlotsVisibility"
19 :show-estimated-pricing="estimatedPricingVisibility"
20 :show-indicator-pricing="indicatorPricingVisibility"
21 :show-slot-pricing="slotPricingVisibility"
22 :show-people-waiting="peopleWaitingVisibility"
23 :show-calendar-date-busyness="calendarDateBusynessVisibility"
24 :label-slots-selected="amLabels.date_time_slots_selected"
25 :label-waiting-list="amLabels.waiting_list"
26 :fetched-slots="null"
27 :service-id="cartItem ? cartItem.serviceId : 0"
28 :date="date"
29 :slots-params="slotsParams"
30 :tax-visibility="taxVisibility"
31 :tax-label="amLabels.total_tax_colon"
32 :tax-label-incl="amLabels.incl_tax"
33 :is-package="false"
34 ></Calendar>
35
36 <!-- Recurring Appointment -->
37 <AmSlidePopup
38 v-if="service.recurringCycle !== 'disabled' && notLastDay && cart.length <= 1"
39 ref="recurringPopupRef"
40 :visibility="recurringPopupVisibility"
41 class="am-fs-dt__calendar__recurring"
42 :aria-label="recurringLabels.repeat_appointment"
43 >
44 <div class="am-fs-dt__rec_popup">
45 <p>
46 {{ recurringLabels.repeat_appointment }}
47 </p>
48 <p v-if="amCustomize.recurringPopup.options.content.visibility">
49 {{ recurringLabels.repeat_appointment_quest }}
50 </p>
51 </div>
52 <template #footer>
53 <AmButton
54 category="secondary"
55 :type="amCustomize.recurringPopup.options.secondaryButton.buttonType"
56 @click="recurringStep(false)"
57 >
58 {{ recurringLabels.no }}
59 </AmButton>
60 <AmButton
61 :type="amCustomize.recurringPopup.options.primaryButton.buttonType"
62 @click="recurringStep(true)"
63 >
64 {{ recurringLabels.yes }}
65 </AmButton>
66 </template>
67 </AmSlidePopup>
68 <!--/ Recurring Appointment -->
69
70 <!-- Packages Popup -->
71 <PackagesPopup @continue-with-service="packagesVisibility = false"></PackagesPopup>
72 <!--/ Packages Popup -->
73 </div>
74 </template>
75
76 <script setup>
77 import { useStore } from 'vuex'
78 import {
79 ref,
80 provide,
81 inject,
82 computed,
83 onMounted,
84 watchEffect,
85 reactive,
86 watch,
87 nextTick,
88 } from 'vue'
89 import AmButton from '../../../_components/button/AmButton.vue'
90 import AmSlidePopup from '../../../_components/slide-popup/AmSlidePopup.vue'
91 import Calendar from '../../Parts/Calendar.vue'
92 import {
93 useAppointmentParams,
94 useRange,
95 useSelectedDuration,
96 useSelectedDate,
97 useSelectedTime,
98 useDeselectedDate,
99 useSlotsCallback,
100 } from '../../../../assets/js/public/slots.js'
101 import { useFormattedPrice } from '../../../../assets/js/common/formatting.js'
102 import {
103 useFillAppointments,
104 useDuration,
105 usePaymentError,
106 useBusySlots,
107 } from '../../../../assets/js/common/appointments.js'
108 import { useCart, useCartItem } from '../../../../assets/js/public/cart'
109 import PackagesPopup from '../PakagesStep/parts/PackagesPopup'
110 import { useCurrentUser } from '../../../../assets/js/public/user'
111 import { defaultCustomizeSettings } from '../../../../assets/js/common/defaultCustomize'
112 import AmAlert from '../../../_components/alert/AmAlert.vue'
113 import { useScrollTo } from '../../../../assets/js/common/scrollElements'
114 import { useTaxVisibility } from '../../../../assets/js/common/pricing'
115
116 let props = defineProps({
117 globalClass: {
118 type: String,
119 default: '',
120 },
121 showCart: {
122 type: Boolean,
123 default: false,
124 },
125 })
126
127 let dateTimeRef = ref(null)
128 provide('formWrapper', dateTimeRef)
129
130 let amCustomize = inject('amCustomize')
131
132 // * Busy Time Slots Visibility
133 let busyTimeSlotsVisibility = computed(() => {
134 if ('busyTimeSlotsVisibility' in amCustomize.dateTimeStep.options) {
135 return amCustomize.dateTimeStep.options.busyTimeSlotsVisibility.visibility
136 }
137
138 return defaultCustomizeSettings.sbsNew.dateTimeStep.options.busyTimeSlotsVisibility.visibility
139 })
140
141 // * Estimated Pricing Visibility
142 let estimatedPricingVisibility = computed(() => {
143 if ('estimatedPricingVisibility' in amCustomize.dateTimeStep.options) {
144 return amCustomize.dateTimeStep.options.estimatedPricingVisibility.visibility
145 }
146
147 return defaultCustomizeSettings.sbsNew.dateTimeStep.options.estimatedPricingVisibility.visibility
148 })
149
150 // * Indicator Pricing Visibility
151 let indicatorPricingVisibility = computed(() => {
152 if ('indicatorPricingVisibility' in amCustomize.dateTimeStep.options) {
153 return amCustomize.dateTimeStep.options.indicatorPricingVisibility.visibility
154 }
155
156 return defaultCustomizeSettings.sbsNew.dateTimeStep.options.indicatorPricingVisibility.visibility
157 })
158
159 // * Slot Pricing Visibility
160 let slotPricingVisibility = computed(() => {
161 if ('slotPricingVisibility' in amCustomize.dateTimeStep.options) {
162 return amCustomize.dateTimeStep.options.slotPricingVisibility.visibility
163 }
164
165 return defaultCustomizeSettings.sbsNew.dateTimeStep.options.slotPricingVisibility.visibility
166 })
167
168 // * Waiting List - Show People Waiting
169 let peopleWaitingVisibility = computed(() => {
170 if ('peopleWaitingVisibility' in amCustomize.dateTimeStep.options) {
171 return amCustomize.dateTimeStep.options.peopleWaitingVisibility.visibility
172 }
173
174 return defaultCustomizeSettings.sbsNew.dateTimeStep.options.peopleWaitingVisibility.visibility
175 })
176
177 let calendarDateBusynessVisibility = computed(() => {
178 if ('calendarDateBusynessVisibility' in amCustomize.dateTimeStep.options) {
179 return amCustomize.dateTimeStep.options.calendarDateBusynessVisibility.visibility
180 }
181
182 return defaultCustomizeSettings.sbsNew.dateTimeStep.options.calendarDateBusynessVisibility
183 .visibility
184 })
185
186 const store = useStore()
187
188 // * Amelia Settings
189 const amSettings = inject('settings')
190
191 // * Labels
192 const globalLabels = inject('labels')
193
194 // * local language short code
195 const localLanguage = inject('localLanguage')
196
197 // * if local lang is in settings lang
198 let langDetection = computed(() => amSettings.general.usedLanguages.includes(localLanguage.value))
199
200 // * Computed labels
201 let amLabels = computed(() => {
202 let computedLabels = reactive({ ...globalLabels })
203
204 if (amSettings.customizedData?.sbsNew?.dateTimeStep?.translations) {
205 let customizedLabels = amSettings.customizedData.sbsNew.dateTimeStep.translations
206 Object.keys(customizedLabels).forEach((labelKey) => {
207 if (customizedLabels[labelKey][localLanguage.value] && langDetection.value) {
208 computedLabels[labelKey] = customizedLabels[labelKey][localLanguage.value]
209 } else if (customizedLabels[labelKey].default) {
210 computedLabels[labelKey] = customizedLabels[labelKey].default
211 }
212 })
213 }
214 return computedLabels
215 })
216
217 let recurringLabels = computed(() => {
218 let computedLabels = reactive({ ...globalLabels })
219
220 if (
221 amSettings.customizedData &&
222 amSettings.customizedData.sbsNew &&
223 amSettings.customizedData.sbsNew.recurringPopup.translations
224 ) {
225 let customizedLabels = amSettings.customizedData.sbsNew.recurringPopup.translations
226 Object.keys(customizedLabels).forEach((labelKey) => {
227 if (customizedLabels[labelKey][localLanguage.value] && langDetection.value) {
228 computedLabels[labelKey] = customizedLabels[labelKey][localLanguage.value]
229 } else if (customizedLabels[labelKey].default) {
230 computedLabels[labelKey] = customizedLabels[labelKey].default
231 }
232 })
233 }
234 return computedLabels
235 })
236
237 let date = computed(() => {
238 return (
239 cartItem.value &&
240 cartItem.value.index !== '' &&
241 cartItem.value.services[cartItem.value.serviceId].list.length &&
242 cartItem.value.services[cartItem.value.serviceId].list[cartItem.value.index].date
243 )
244 })
245
246 let time = computed(() => {
247 return (
248 cartItem.value &&
249 cartItem.value.index !== '' &&
250 cartItem.value.services[cartItem.value.serviceId].list.length &&
251 cartItem.value.services[cartItem.value.serviceId].list[cartItem.value.index].time
252 )
253 })
254
255 let cart = useCart(store)
256
257 let cartItem = computed(() => useCartItem(store))
258
259 let taxVisibility = computed(() => {
260 let visible = amCustomize.dateTimeStep.options.tax?.visibility ?? true
261 if (cartItem.value && visible) {
262 return useTaxVisibility(store, cartItem.value.serviceId, 'service')
263 }
264 return false
265 })
266
267 let slotsParams = computed(() => {
268 return useAppointmentParams(store)
269 })
270
271 // * Package
272
273 let stepIndex = inject('stepIndex')
274 let shortcodeData = inject('shortcodeData')
275
276 let packagesOptions = computed(() =>
277 store.getters['entities/filteredPackages'](store.getters['booking/getSelection']),
278 )
279
280 let packagesVisibility = ref(
281 packagesOptions.value.length > 0 &&
282 shortcodeData.value.show !== 'services' &&
283 stepIndex.value === 0 &&
284 cart.length <= 1,
285 )
286 provide('packagesVisibility', packagesVisibility)
287
288 watch(packagesVisibility, (isVisible) => {
289 if (!isVisible && dateTimeRef.value) {
290 dateTimeRef.value.focus()
291 }
292 })
293
294 /**************
295 * Navigation *
296 *************/
297
298 const {
299 nextStep,
300 footerButtonReset,
301 footerBtnDisabledUpdater,
302 footerButtonClicked,
303 headerButtonPreviousClicked,
304 } = inject('changingStepsFunctions', {
305 nextStep: () => {},
306 footerButtonReset: () => {},
307 footerBtnDisabledUpdater: () => {},
308 footerButtonClicked: {
309 value: false,
310 },
311 headerButtonPreviousClicked: {
312 value: false,
313 },
314 })
315
316 let limitPerEmployeeError = ref('')
317 let limitError = ref(null)
318
319 watchEffect(() => {
320 if (footerButtonClicked.value && date.value && time.value) {
321 footerButtonReset()
322 limitPerEmployeeError.value = ''
323
324 usePaymentError(store, '')
325
326 if (
327 service.recurringCycle !== 'disabled' &&
328 service.recurringCycle !== null &&
329 notLastDay.value &&
330 cart.length <= 1 &&
331 !store.getters['appointmentWaitingListOptions/getIsWaitingListSlot']
332 ) {
333 recurringPopupVisibility.value = true
334 } else {
335 let bookingFailed = useFillAppointments(store)
336
337 if (bookingFailed) {
338 limitPerEmployeeError.value = amLabels.value.employee_limit_reached
339 } else {
340 nextStep()
341 }
342 }
343 }
344 })
345
346 watchEffect(() => {
347 if (limitPerEmployeeError.value && limitError.value) {
348 useScrollTo(dateTimeRef.value, limitError.value, 0, 300)
349 }
350 })
351
352 watchEffect(
353 () => {
354 footerBtnDisabledUpdater(!date.value || !time.value)
355 },
356 { flush: 'post' },
357 )
358
359 /*****************
360 * Calendar Data *
361 ****************/
362
363 let loadCounter = ref(0)
364
365 let calendarChangeSideBar = ref(true)
366
367 provide('calendarChangeSideBar', calendarChangeSideBar)
368
369 let calendarSlotDuration = ref(0)
370
371 provide('calendarSlotDuration', calendarSlotDuration)
372
373 let calendarServiceDuration = ref(0)
374
375 provide('calendarServiceDuration', calendarServiceDuration)
376
377 let calendarServiceDurations = ref([])
378
379 provide('calendarServiceDurations', calendarServiceDurations)
380
381 provide('useSlotsCallback', useSlotsCallback)
382
383 provide('useRange', useRange)
384
385 provide('useSelectedDuration', useSelectedDuration)
386
387 provide('useBusySlots', useBusySlots)
388
389 provide('useSelectedDate', useSelectedDate)
390
391 provide('useSelectedTime', useSelectedTime)
392
393 provide('useDeselectedDate', useDeselectedDate)
394
395 /*********
396 * Other *
397 ********/
398
399 let service = reactive({})
400
401 let recurringPopupVisibility = ref(false)
402 let recurringPopupRef = ref(null)
403
404 let { goToRecurringStep } = inject('goToRecurringStep', {
405 goToRecurringStep: () => {},
406 })
407
408 let { removeRecurringStep } = inject('removeRecurringStep', {
409 removeRecurringStep: () => {},
410 })
411
412 // * Focus management for keyboard navigation on Recurring Appointment Popup
413 watch(recurringPopupVisibility, async (isVisible) => {
414 if (isVisible) {
415 // Wait for DOM to update
416 await nextTick()
417 let footerButton = recurringPopupRef.value?.$el?.querySelector('.am-button--secondary')
418 if (footerButton) {
419 footerButton.focus()
420 }
421 }
422 })
423
424 let notLastDay = computed(() => {
425 if (
426 cartItem.value &&
427 cartItem.value.index !== '' &&
428 cartItem.value.services[cartItem.value.serviceId].list.length
429 ) {
430 let lastDate = store.getters['booking/getMultipleAppointmentsLastDate']
431 let chosenDate = store.getters['booking/getMultipleAppointmentsDate']
432 return lastDate !== chosenDate
433 }
434 return false
435 })
436
437 function recurringStep(repeat) {
438 if (repeat) {
439 goToRecurringStep()
440 } else {
441 if (
442 cartItem.value &&
443 'serviceId' in cartItem.value &&
444 cartItem.value.serviceId in cartItem.value.services &&
445 cartItem.value.services[cartItem.value.serviceId].list.length > 1
446 ) {
447 store.commit('booking/unsetRecurringItems')
448 }
449
450 limitPerEmployeeError.value = ''
451 let bookingFailed = useFillAppointments(store)
452
453 if (bookingFailed) {
454 limitPerEmployeeError.value = amLabels.value.employee_limit_reached
455 } else {
456 nextStep()
457 }
458 }
459 }
460
461 function setDurationsPrices(service, durations) {
462 if (service.customPricing.enabled === 'duration') {
463 Object.keys(service.customPricing.durations).forEach((duration) => {
464 if (!(duration in durations)) {
465 durations[parseInt(duration)] = []
466 }
467
468 durations[parseInt(duration)].push(service.customPricing.durations[duration].price)
469 })
470 }
471
472 return durations
473 }
474
475 function getDurations() {
476 let durations = {}
477
478 let serviceId = store.getters['booking/getServiceId']
479
480 let employeeId = store.getters['booking/getEmployeeId']
481
482 if (!employeeId && serviceId) {
483 let employees = store.getters['entities/filteredEmployees'](
484 store.getters['booking/getSelection'],
485 )
486
487 employees.forEach((provider) => {
488 let service = provider.serviceList.find((service) => service.id === serviceId)
489
490 durations = setDurationsPrices(service, durations)
491 })
492 } else {
493 let employee = store.getters['entities/getEmployee'](store.getters['booking/getEmployeeId'])
494
495 let service = employee.serviceList.find((service) => service.id === serviceId)
496
497 durations = setDurationsPrices(service, durations)
498 }
499
500 let result = []
501
502 Object.keys(durations)
503 .sort((a, b) => a - b)
504 .forEach((duration) => {
505 let min = Math.min(...durations[duration])
506
507 let max = Math.max(...durations[duration])
508
509 result.push({
510 duration: parseInt(duration),
511 priceLabel:
512 min !== max
513 ? useFormattedPrice(min) + ' - ' + useFormattedPrice(max)
514 : min === 0
515 ? ''
516 : useFormattedPrice(min),
517 })
518 })
519
520 return result
521 }
522
523 onMounted(() => {
524 if (props.showCart && !store.getters['booking/getShownCart']) {
525 return
526 }
527
528 service = store.getters['entities/getService'](cartItem.value.serviceId)
529
530 calendarServiceDurations.value = getDurations()
531
532 let serviceDuration = store.getters['booking/getBookingDuration']
533
534 serviceDuration =
535 serviceDuration &&
536 calendarServiceDurations.value.filter((i) => i.duration === serviceDuration).length
537 ? serviceDuration
538 : service.duration
539
540 store.commit('booking/setBookingDuration', serviceDuration)
541
542 let extrasIds = store.getters['booking/getSelectedExtras'].map((i) => i.extraId)
543
544 calendarSlotDuration.value = useDuration(
545 serviceDuration,
546 service.extras.filter((i) => extrasIds.includes(i.id)),
547 )
548
549 calendarServiceDuration.value = serviceDuration
550
551 let index = store.getters['booking/getCartItemIndex']
552
553 let items = store.getters['booking/getAllMultipleAppointments']
554
555 let hasItem = index in items
556
557 if (
558 (hasItem && cartItem.value.services[cartItem.value.serviceId].list.length > 1) ||
559 store.getters['appointmentWaitingListOptions/getIsWaitingListSlot']
560 ) {
561 removeRecurringStep()
562 }
563
564 useCurrentUser(store, shortcodeData.value.hasApiCall)
565
566 if (
567 'serviceId' in cartItem.value &&
568 cartItem.value.serviceId in cartItem.value.services &&
569 cartItem.value.index in cartItem.value.services[cartItem.value.serviceId].list
570 ) {
571 cartItem.value.services[cartItem.value.serviceId].list[cartItem.value.index].providerId = null
572
573 cartItem.value.services[cartItem.value.serviceId].list[cartItem.value.index].locationId = null
574 }
575
576 loadCounter.value++
577
578 if (dateTimeRef.value && !packagesVisibility.value) {
579 dateTimeRef.value.focus()
580 }
581 })
582 </script>
583
584 <script>
585 export default {
586 name: 'DateTimeStep',
587 key: 'dateTimeStep',
588 sidebarData: {
589 label: 'date_time',
590 icon: 'date-time',
591 stepSelectedData: [],
592 finished: false,
593 selected: false,
594 },
595 }
596 </script>
597
598 <style lang="scss">
599 // am -- amelia
600 // fs -- form steps
601
602 .amelia-v2-booking #amelia-container {
603 // Amelia Form Steps
604 .am-fs {
605 &__main {
606 &-content.am-fs-dt__calendar {
607 &.am-oxvisible {
608 overflow-x: visible;
609 }
610 }
611 }
612
613 // Container Wrapper
614 &__main {
615 &-heading {
616 &-inner {
617 display: flex;
618 align-items: center;
619
620 .am-heading-prev {
621 margin-right: 12px;
622 }
623 }
624 }
625 &-inner {
626 &#{&}-dt {
627 padding: 0 20px;
628 }
629 }
630 }
631
632 &-dt__rec_popup {
633 margin: 16px 0 48px 0;
634 & > p {
635 font-size: 15px;
636 line-height: 1.6;
637 color: var(--am-c-main-text);
638
639 &:first-child {
640 font-weight: 500;
641 margin-bottom: 7px;
642 }
643
644 &:last-child {
645 font-weight: 400;
646 }
647 }
648 }
649 }
650 }
651 </style>
652