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