PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 2.4.8
Booking for Appointments and Events Calendar – Amelia v2.4.8
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 / StepForm / InitStep / InitStep.vue
ameliabooking / v3 / src / views / public / StepForm / InitStep Last commit date
InitStep.vue 1 day ago
InitStep.vue
635 lines
1 <template>
2 <div
3 v-if="loaded"
4 class="am-fs__init"
5 :class="[props.globalClass, { 'am-oxvisible': bringingAnyoneVisibility || packagesVisibility }]"
6 >
7 <el-form
8 ref="initFormRef"
9 :model="initFormData"
10 :rules="rules"
11 label-position="top"
12 class="am-fs__init-form"
13 >
14 <template v-for="field in amCustomize.initStep.order" :key="field.id">
15 <component :is="amFields[field.id].template" v-bind="amFields[field.id].props"></component>
16 </template>
17 </el-form>
18
19 <!-- Bringing Anyone with you -->
20 <AmSlidePopup
21 v-if="bringingAnyoneOptions.availability"
22 ref="bringingPopupRef"
23 :visibility="bringingAnyoneVisibility"
24 class="am-fs__init__bringing"
25 >
26 <p class="am-fs__popup-x" :class="{ 'am-rtl': isRtl }" @click="closeBringingPopup">
27 <AmeliaIconClose></AmeliaIconClose>
28 </p>
29 <BringingAnyone :in-popup="true" />
30 <template #footer>
31 <AmButton
32 v-if="
33 bringingAnyoneOptions.min !== bringingAnyoneOptions.max &&
34 bringingAnyoneOptions.min <= 0
35 "
36 category="secondary"
37 :type="amCustomize.bringingAnyone.options.secondaryButton.buttonType"
38 :disabled="
39 bringingAnyoneOptions.min === bringingAnyoneOptions.max ||
40 (!amSettings.appointments.allowBookingIfNotMin && bringingAnyoneOptions.min > 0)
41 "
42 @click="noOneBringWith"
43 >
44 {{ bringingLabels.bringing_no }}
45 </AmButton>
46 <AmButton
47 :type="
48 bringingAnyoneOptions.min !== bringingAnyoneOptions.max &&
49 bringingAnyoneOptions.min <= 0
50 ? amCustomize.bringingAnyone.options.primaryButton.buttonType
51 : amCustomize.bringingAnyone.options.primaryFooterButton.buttonType
52 "
53 :disabled="
54 bookingPersons ===
55 (amSettings.appointments.bringingAnyoneLogic === 'additional' ? 1 : 0)
56 "
57 @click="bringPeopleWithYou"
58 >
59 <template
60 v-if="
61 bringingAnyoneOptions.min !== bringingAnyoneOptions.max &&
62 bringingAnyoneOptions.min <= 0
63 "
64 >
65 {{ bringingLabels.bringing_yes }}
66 </template>
67 <template v-else>
68 {{ bringingLabels.continue }}
69 </template>
70 </AmButton>
71 </template>
72 </AmSlidePopup>
73 <!--/ Bringing Anyone with you -->
74
75 <!-- Packages Popup -->
76 <PackagesPopup class="am-fs__init__package" @continue-with-service="continueWithService()" />
77 <!--/ Packages Popup -->
78 </div>
79
80 <!-- Skeleton -->
81 <el-skeleton v-else animated :style="cssPackage">
82 <template #template>
83 <div v-for="item in new Array(3)" :key="item">
84 <div class="el-skeleton-item-wrapper">
85 <el-skeleton-item variant="h3" />
86 <el-skeleton-item variant="text" />
87 </div>
88 </div>
89 </template>
90 </el-skeleton>
91 <!-- /Skeleton -->
92 </template>
93
94 <script setup>
95 import { useStore } from 'vuex'
96 import {
97 ref,
98 reactive,
99 computed,
100 watchEffect,
101 inject,
102 provide,
103 markRaw,
104 nextTick,
105 watch,
106 } from 'vue'
107
108 import AmSlidePopup from '../../../_components/slide-popup/AmSlidePopup.vue'
109 import AmButton from '../../../_components/button/AmButton.vue'
110 import BringingAnyone from '../BringingAnyone/BringingAnyone.vue'
111 import AmeliaIconClose from '../../../_components/icons/IconClose'
112 import PackagesPopup from '../PakagesStep/parts/PackagesPopup'
113
114 import ServiceFormField from '../_fields/ServiceFormField.vue'
115 import LocationFormField from '../_fields/LocationFormField.vue'
116 import EmployeeFormField from '../_fields/EmployeeFormField.vue'
117
118 import { useColorTransparency } from '../../../../assets/js/common/colorManipulation'
119 import { useCapacity } from '../../../../assets/js/common/appointments'
120 import { useCart } from '../../../../assets/js/public/cart'
121 import { announceFormErrors, useLiveAnnouncer } from '../../../../assets/js/common/liveAnnouncer'
122
123 let props = defineProps({
124 globalClass: {
125 type: String,
126 default: '',
127 },
128 })
129
130 let amCustomize = inject('amCustomize')
131
132 const { announceAlert } = useLiveAnnouncer()
133
134 // * Amelia Settings
135 const amSettings = inject('settings')
136
137 // * Store
138 let store = useStore()
139
140 // * Document text orientation
141 let isRtl = computed(() => store.getters['getIsRtl'])
142
143 function isFieldFilterable(key) {
144 return amCustomize.initStep.options[key] && 'filterable' in amCustomize.initStep.options[key]
145 ? amCustomize.initStep.options[key].filterable
146 : true
147 }
148
149 let amFields = reactive({
150 service: {
151 template: markRaw(ServiceFormField),
152 props: {
153 class: isRtl.value ? 'am-rtl' : '',
154 filterable: isFieldFilterable('service'),
155 taxVisible: amCustomize.initStep.options.tax?.visibility ?? true,
156 multiplePricesVisibility: amCustomize.initStep.options.multiplePrices?.visibility ?? false,
157 },
158 },
159 location: {
160 template: markRaw(LocationFormField),
161 props: {
162 class: isRtl.value ? 'am-rtl' : '',
163 visibility: amCustomize.initStep.options.location.visibility,
164 filterable: isFieldFilterable('location'),
165 },
166 },
167 employee: {
168 template: markRaw(EmployeeFormField),
169 props: {
170 class: isRtl.value ? 'am-rtl' : '',
171 visibility: amCustomize.initStep.options.employee.visibility,
172 filterable: isFieldFilterable('employee'),
173 },
174 },
175 })
176
177 let loaded = computed(() => {
178 return store.getters['entities/getReady']
179 })
180
181 let bookingPersons = computed(() => {
182 return store.getters['booking/getBookingPersons']
183 })
184
185 // * Short Code
186 const shortcodeData = inject('shortcodeData')
187
188 // * Labels
189 const globalLabels = inject('labels')
190
191 // * local language short code
192 const localLanguage = inject('localLanguage')
193
194 // * if local lang is in settings lang
195 let langDetection = computed(() => amSettings.general.usedLanguages.includes(localLanguage.value))
196
197 // * Computed labels
198 let amLabels = computed(() => {
199 let computedLabels = reactive({ ...globalLabels })
200
201 if (amSettings.customizedData?.sbsNew?.initStep?.translations) {
202 let customizedLabels = amSettings.customizedData.sbsNew.initStep.translations
203 Object.keys(customizedLabels).forEach((labelKey) => {
204 if (customizedLabels[labelKey][localLanguage.value] && langDetection.value) {
205 computedLabels[labelKey] = customizedLabels[labelKey][localLanguage.value]
206 } else if (customizedLabels[labelKey].default) {
207 computedLabels[labelKey] = customizedLabels[labelKey].default
208 }
209 })
210 }
211 return computedLabels
212 })
213
214 provide('amLabels', amLabels)
215
216 let bringingLabels = computed(() => {
217 let computedLabels = reactive({ ...globalLabels })
218
219 if (
220 amSettings.customizedData &&
221 amSettings.customizedData.sbsNew &&
222 amSettings.customizedData.sbsNew.bringingAnyone.translations
223 ) {
224 let customizedLabels = amSettings.customizedData.sbsNew.bringingAnyone.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 // * Step Functions
237 const { nextStep, footerButtonReset, footerButtonClicked } = inject('changingStepsFunctions', {
238 nextStep: () => {},
239 footerButtonReset: () => {},
240 footerButtonClicked: {
241 value: false,
242 },
243 })
244
245 /**
246 * * Bringing anyone with you - block
247 */
248
249 let bringingAnyoneOptions = computed(() => {
250 return useCapacity(
251 store.getters['entities/getEmployeeServices'](
252 store.getters['booking/getServiceProviderSelection'],
253 ),
254 )
255 })
256
257 provide('bringingOptions', {
258 bringingAnyoneOptions,
259 })
260
261 // * Bringing anyone with you popup visibility
262 let bringingAnyoneVisibility = ref(false)
263 let bringingPopupRef = ref(null)
264
265 function noOneBringWith() {
266 closeBringingPopup()
267 store.commit('booking/setBookingPersons', 0)
268 nextStep()
269 }
270
271 function bringPeopleWithYou() {
272 nextStep()
273 }
274
275 // * Focus management for keyboard navigation on Bringing Anyone popup
276 watch(bringingAnyoneVisibility, async (isVisible) => {
277 if (isVisible) {
278 // Wait for DOM to update
279 await nextTick()
280 // Find the input number field and focus it
281 const numberInput = bringingPopupRef.value?.$el?.querySelector('.am-input-number input')
282 if (numberInput) {
283 numberInput.focus()
284 }
285 }
286 })
287
288 function continueWithService() {
289 packagesVisibility.value = false
290 if (bringingAnyoneOptions.value.availability) {
291 bringingAnyoneVisibility.value = true
292 } else {
293 nextStep()
294 }
295 }
296
297 function closeBringingPopup() {
298 bringingAnyoneVisibility.value = false
299 }
300
301 // * Package popup
302 let packagesOptions = computed(() =>
303 store.getters['entities/filteredPackages'](store.getters['booking/getSelection']),
304 )
305
306 let packagesVisibility = ref(false)
307
308 provide('packagesVisibility', packagesVisibility)
309
310 /**
311 * Form Block start
312 */
313 // * Form reference
314 let initFormRef = ref(null)
315
316 // * Form data
317 let initFormData = ref({
318 service: computed({
319 get: () => {
320 if (
321 store.getters['booking/getCategoryId'] === null &&
322 store.getters['booking/getServiceId'] === null
323 ) {
324 return null
325 }
326 return [store.getters['booking/getCategoryId'], store.getters['booking/getServiceId']]
327 },
328 set: (val) => {
329 store.commit('booking/setCategoryId', val ? val[0] : null)
330 store.commit('booking/setServiceId', val ? val[1] : null)
331 store.commit('booking/setBookableType', 'appointment')
332 },
333 }),
334 onlyService: computed({
335 get: () => store.getters['booking/getServiceId'],
336 set: (val) => {
337 store.commit('booking/setServiceId', val ? val : null)
338 store.commit('booking/setBookableType', 'appointment')
339 },
340 }),
341 location: computed({
342 get: () => store.getters['booking/getLocationId'],
343 set: (val) => {
344 store.commit('booking/setLocationId', val ? val : null)
345 },
346 }),
347 employee: computed({
348 get: () => store.getters['booking/getEmployeeId'],
349 set: (val) => {
350 // let employee = employeeOptions.value.find(item => item.id === val)
351 // store.commit('booking/setEmployeeId', employee ? employee.id : null)
352 store.commit('booking/setEmployeeId', val ? val : null)
353 },
354 }),
355 })
356
357 provide('initFormData', initFormData)
358
359 // * Form validation rules
360 let rules = ref({
361 service: [
362 {
363 required: true,
364 message: amLabels.value.please_select_service,
365 trigger: ['blur', 'change'],
366 },
367 ],
368 onlyService: [
369 {
370 required: true,
371 message: amLabels.value.please_select_service,
372 trigger: ['blur', 'change'],
373 },
374 ],
375 location: [
376 {
377 required: amCustomize.initStep.options.location.required,
378 message: amLabels.value.please_select_location,
379 trigger: ['blur', 'change'],
380 },
381 ],
382 employee: [
383 {
384 required: amCustomize.initStep.options.employee.required,
385 message: amLabels.value.please_select_employee,
386 trigger: ['blur', 'change'],
387 },
388 ],
389 })
390
391 /**
392 * Submit Form Function
393 */
394 function submitForm() {
395 initFormRef.value.validate((valid) => {
396 footerButtonReset()
397 if (valid) {
398 if (
399 packagesOptions.value.length &&
400 shortcodeData.value.show !== 'services' &&
401 useCart(store).length <= 1
402 ) {
403 packagesVisibility.value = true
404 } else {
405 continueWithService()
406 }
407 } else {
408 announceFormErrors(initFormRef, announceAlert)
409 return false
410 }
411 })
412 }
413
414 // * Watching when footer button was clicked
415 watchEffect(() => {
416 if (footerButtonClicked.value) {
417 submitForm()
418 }
419 })
420
421 // * Colors
422 let amColors = inject('amColors')
423 let cssPackage = computed(() => {
424 return {
425 '--am-c-ps-text-op60': useColorTransparency(amColors.value.colorMainText, 0.6),
426 '--am-c-ps-text-op20': useColorTransparency(amColors.value.colorMainText, 0.2),
427 }
428 })
429 </script>
430
431 <script>
432 export default {
433 name: 'InitStep',
434 key: 'initStep',
435 sidebarData: {
436 label: 'service_selection',
437 icon: 'service',
438 stepSelectedData: [],
439 finished: false,
440 selected: false,
441 },
442 }
443 </script>
444
445 <style lang="scss">
446 .amelia-v2-booking #amelia-container {
447 .am-fs {
448 &__main {
449 &-content.am-fs__init {
450 padding-top: 40px;
451
452 &.am-oxvisible {
453 overflow-x: visible;
454 }
455 }
456
457 &-inner {
458 overflow: hidden;
459 }
460 }
461
462 &__init {
463 &-form {
464 &__item {
465 $count: 100;
466 @for $i from 0 through $count {
467 &:nth-child(#{$i + 1}) {
468 animation: 600ms cubic-bezier(0.45, 1, 0.4, 1.2) #{$i * 100}ms am-animation-slide-up;
469 animation-fill-mode: both;
470 }
471 }
472
473 &.el-form-item {
474 margin-bottom: 24px;
475 }
476 }
477
478 &__label {
479 display: inline-block;
480 font-family: var(--am-font-family);
481 font-weight: 500;
482 color: var(--am-c-main-text);
483 margin-bottom: 4px;
484 }
485
486 .el-form-item {
487 &__label {
488 color: var(--am-c-main-text);
489 line-height: unset;
490 padding: 0;
491
492 &:before {
493 color: var(--am-c-error);
494 }
495 }
496
497 &__error {
498 color: var(--am-c-error);
499 }
500 }
501 }
502 }
503
504 &__popup-x {
505 position: absolute;
506 top: 16px;
507 right: 16px;
508 cursor: pointer;
509 color: var(--am-c-main-text);
510 margin: 0;
511
512 &.am-rtl {
513 right: auto;
514 left: 16px;
515 }
516 }
517
518 &__ps {
519 &-popup {
520 position: relative;
521
522 &__heading {
523 font-size: 14px;
524 font-weight: 400;
525 line-height: 1.42857;
526 text-align: center;
527 color: var(--am-c-main-text);
528 margin: 0 0 16px;
529 padding: 20px 6px 0 0;
530 max-height: 40px;
531 overflow-x: hidden;
532
533 // Main Scroll styles
534 &::-webkit-scrollbar {
535 width: 6px;
536 }
537
538 &::-webkit-scrollbar-thumb {
539 border-radius: 6px;
540 background: var(--am-c-scroll-op30);
541 }
542
543 &::-webkit-scrollbar-track {
544 border-radius: 6px;
545 background: var(--am-c-scroll-op10);
546 }
547 }
548
549 &__or {
550 display: flex;
551 flex-direction: row;
552 font-weight: 400;
553 font-size: 14px;
554 line-height: 20px;
555 margin: 20px 0;
556 color: var(--am-c-ps-text-op60);
557
558 &:before,
559 &:after {
560 content: '';
561 flex: 1 1;
562 border-bottom: 1px solid var(--am-c-ps-text-op20);
563 margin: auto;
564 }
565
566 &:before {
567 margin-right: 10px;
568 }
569
570 &:after {
571 margin-left: 10px;
572 }
573 }
574
575 &__btn {
576 &.am-button.am-button--medium {
577 --am-h-btn: 56px;
578 --am-fs-btn: 14px;
579 line-height: 16px;
580 width: 100%;
581 justify-content: space-between;
582 }
583
584 &-mobile {
585 &.am-button.am-button--medium {
586 --am-fs-btn: 12px;
587 }
588 }
589 }
590 }
591
592 &-pill {
593 display: inline-block;
594 font-size: 14px;
595 font-weight: 500;
596 line-height: 1;
597 color: var(--am-c-btn-prim);
598 background-color: var(--am-c-btn-prim-text);
599 border-radius: 12px;
600 padding: 5px 8px;
601 }
602 }
603 }
604
605 .el-skeleton {
606 width: 100%;
607 padding: 16px 32px;
608
609 &.skeleton-mobile {
610 .el-skeleton {
611 padding: 16px;
612 }
613 }
614
615 &-item-wrapper {
616 padding: 8px 0;
617 display: flex;
618 align-items: flex-start;
619 flex-direction: column;
620
621 .el-skeleton__h3 {
622 margin-bottom: 4px;
623 width: 100px;
624 height: 20px;
625 }
626
627 .el-skeleton__text {
628 width: 100%;
629 height: 40px;
630 }
631 }
632 }
633 }
634 </style>
635