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 / InitStep / InitStep.vue
ameliabooking / v3 / src / views / public / StepForm / InitStep Last commit date
InitStep.vue 3 weeks ago
InitStep.vue
634 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 },
157 },
158 location: {
159 template: markRaw(LocationFormField),
160 props: {
161 class: isRtl.value ? 'am-rtl' : '',
162 visibility: amCustomize.initStep.options.location.visibility,
163 filterable: isFieldFilterable('location'),
164 },
165 },
166 employee: {
167 template: markRaw(EmployeeFormField),
168 props: {
169 class: isRtl.value ? 'am-rtl' : '',
170 visibility: amCustomize.initStep.options.employee.visibility,
171 filterable: isFieldFilterable('employee'),
172 },
173 },
174 })
175
176 let loaded = computed(() => {
177 return store.getters['entities/getReady']
178 })
179
180 let bookingPersons = computed(() => {
181 return store.getters['booking/getBookingPersons']
182 })
183
184 // * Short Code
185 const shortcodeData = inject('shortcodeData')
186
187 // * Labels
188 const globalLabels = inject('labels')
189
190 // * local language short code
191 const localLanguage = inject('localLanguage')
192
193 // * if local lang is in settings lang
194 let langDetection = computed(() => amSettings.general.usedLanguages.includes(localLanguage.value))
195
196 // * Computed labels
197 let amLabels = computed(() => {
198 let computedLabels = reactive({ ...globalLabels })
199
200 if (amSettings.customizedData?.sbsNew?.initStep?.translations) {
201 let customizedLabels = amSettings.customizedData.sbsNew.initStep.translations
202 Object.keys(customizedLabels).forEach((labelKey) => {
203 if (customizedLabels[labelKey][localLanguage.value] && langDetection.value) {
204 computedLabels[labelKey] = customizedLabels[labelKey][localLanguage.value]
205 } else if (customizedLabels[labelKey].default) {
206 computedLabels[labelKey] = customizedLabels[labelKey].default
207 }
208 })
209 }
210 return computedLabels
211 })
212
213 provide('amLabels', amLabels)
214
215 let bringingLabels = computed(() => {
216 let computedLabels = reactive({ ...globalLabels })
217
218 if (
219 amSettings.customizedData &&
220 amSettings.customizedData.sbsNew &&
221 amSettings.customizedData.sbsNew.bringingAnyone.translations
222 ) {
223 let customizedLabels = amSettings.customizedData.sbsNew.bringingAnyone.translations
224 Object.keys(customizedLabels).forEach((labelKey) => {
225 if (customizedLabels[labelKey][localLanguage.value] && langDetection.value) {
226 computedLabels[labelKey] = customizedLabels[labelKey][localLanguage.value]
227 } else if (customizedLabels[labelKey].default) {
228 computedLabels[labelKey] = customizedLabels[labelKey].default
229 }
230 })
231 }
232 return computedLabels
233 })
234
235 // * Step Functions
236 const { nextStep, footerButtonReset, footerButtonClicked } = inject('changingStepsFunctions', {
237 nextStep: () => {},
238 footerButtonReset: () => {},
239 footerButtonClicked: {
240 value: false,
241 },
242 })
243
244 /**
245 * * Bringing anyone with you - block
246 */
247
248 let bringingAnyoneOptions = computed(() => {
249 return useCapacity(
250 store.getters['entities/getEmployeeServices'](
251 store.getters['booking/getServiceProviderSelection'],
252 ),
253 )
254 })
255
256 provide('bringingOptions', {
257 bringingAnyoneOptions,
258 })
259
260 // * Bringing anyone with you popup visibility
261 let bringingAnyoneVisibility = ref(false)
262 let bringingPopupRef = ref(null)
263
264 function noOneBringWith() {
265 closeBringingPopup()
266 store.commit('booking/setBookingPersons', 0)
267 nextStep()
268 }
269
270 function bringPeopleWithYou() {
271 nextStep()
272 }
273
274 // * Focus management for keyboard navigation on Bringing Anyone popup
275 watch(bringingAnyoneVisibility, async (isVisible) => {
276 if (isVisible) {
277 // Wait for DOM to update
278 await nextTick()
279 // Find the input number field and focus it
280 const numberInput = bringingPopupRef.value?.$el?.querySelector('.am-input-number input')
281 if (numberInput) {
282 numberInput.focus()
283 }
284 }
285 })
286
287 function continueWithService() {
288 packagesVisibility.value = false
289 if (bringingAnyoneOptions.value.availability) {
290 bringingAnyoneVisibility.value = true
291 } else {
292 nextStep()
293 }
294 }
295
296 function closeBringingPopup() {
297 bringingAnyoneVisibility.value = false
298 }
299
300 // * Package popup
301 let packagesOptions = computed(() =>
302 store.getters['entities/filteredPackages'](store.getters['booking/getSelection']),
303 )
304
305 let packagesVisibility = ref(false)
306
307 provide('packagesVisibility', packagesVisibility)
308
309 /**
310 * Form Block start
311 */
312 // * Form reference
313 let initFormRef = ref(null)
314
315 // * Form data
316 let initFormData = ref({
317 service: computed({
318 get: () => {
319 if (
320 store.getters['booking/getCategoryId'] === null &&
321 store.getters['booking/getServiceId'] === null
322 ) {
323 return null
324 }
325 return [store.getters['booking/getCategoryId'], store.getters['booking/getServiceId']]
326 },
327 set: (val) => {
328 store.commit('booking/setCategoryId', val ? val[0] : null)
329 store.commit('booking/setServiceId', val ? val[1] : null)
330 store.commit('booking/setBookableType', 'appointment')
331 },
332 }),
333 onlyService: computed({
334 get: () => store.getters['booking/getServiceId'],
335 set: (val) => {
336 store.commit('booking/setServiceId', val ? val : null)
337 store.commit('booking/setBookableType', 'appointment')
338 },
339 }),
340 location: computed({
341 get: () => store.getters['booking/getLocationId'],
342 set: (val) => {
343 store.commit('booking/setLocationId', val ? val : null)
344 },
345 }),
346 employee: computed({
347 get: () => store.getters['booking/getEmployeeId'],
348 set: (val) => {
349 // let employee = employeeOptions.value.find(item => item.id === val)
350 // store.commit('booking/setEmployeeId', employee ? employee.id : null)
351 store.commit('booking/setEmployeeId', val ? val : null)
352 },
353 }),
354 })
355
356 provide('initFormData', initFormData)
357
358 // * Form validation rules
359 let rules = ref({
360 service: [
361 {
362 required: true,
363 message: amLabels.value.please_select_service,
364 trigger: ['blur', 'change'],
365 },
366 ],
367 onlyService: [
368 {
369 required: true,
370 message: amLabels.value.please_select_service,
371 trigger: ['blur', 'change'],
372 },
373 ],
374 location: [
375 {
376 required: amCustomize.initStep.options.location.required,
377 message: amLabels.value.please_select_location,
378 trigger: ['blur', 'change'],
379 },
380 ],
381 employee: [
382 {
383 required: amCustomize.initStep.options.employee.required,
384 message: amLabels.value.please_select_employee,
385 trigger: ['blur', 'change'],
386 },
387 ],
388 })
389
390 /**
391 * Submit Form Function
392 */
393 function submitForm() {
394 initFormRef.value.validate((valid) => {
395 footerButtonReset()
396 if (valid) {
397 if (
398 packagesOptions.value.length &&
399 shortcodeData.value.show !== 'services' &&
400 useCart(store).length <= 1
401 ) {
402 packagesVisibility.value = true
403 } else {
404 continueWithService()
405 }
406 } else {
407 announceFormErrors(initFormRef, announceAlert)
408 return false
409 }
410 })
411 }
412
413 // * Watching when footer button was clicked
414 watchEffect(() => {
415 if (footerButtonClicked.value) {
416 submitForm()
417 }
418 })
419
420 // * Colors
421 let amColors = inject('amColors')
422 let cssPackage = computed(() => {
423 return {
424 '--am-c-ps-text-op60': useColorTransparency(amColors.value.colorMainText, 0.6),
425 '--am-c-ps-text-op20': useColorTransparency(amColors.value.colorMainText, 0.2),
426 }
427 })
428 </script>
429
430 <script>
431 export default {
432 name: 'InitStep',
433 key: 'initStep',
434 sidebarData: {
435 label: 'service_selection',
436 icon: 'service',
437 stepSelectedData: [],
438 finished: false,
439 selected: false,
440 },
441 }
442 </script>
443
444 <style lang="scss">
445 .amelia-v2-booking #amelia-container {
446 .am-fs {
447 &__main {
448 &-content.am-fs__init {
449 padding-top: 40px;
450
451 &.am-oxvisible {
452 overflow-x: visible;
453 }
454 }
455
456 &-inner {
457 overflow: hidden;
458 }
459 }
460
461 &__init {
462 &-form {
463 &__item {
464 $count: 100;
465 @for $i from 0 through $count {
466 &:nth-child(#{$i + 1}) {
467 animation: 600ms cubic-bezier(0.45, 1, 0.4, 1.2) #{$i * 100}ms am-animation-slide-up;
468 animation-fill-mode: both;
469 }
470 }
471
472 &.el-form-item {
473 margin-bottom: 24px;
474 }
475 }
476
477 &__label {
478 display: inline-block;
479 font-family: var(--am-font-family);
480 font-weight: 500;
481 color: var(--am-c-main-text);
482 margin-bottom: 4px;
483 }
484
485 .el-form-item {
486 &__label {
487 color: var(--am-c-main-text);
488 line-height: unset;
489 padding: 0;
490
491 &:before {
492 color: var(--am-c-error);
493 }
494 }
495
496 &__error {
497 color: var(--am-c-error);
498 }
499 }
500 }
501 }
502
503 &__popup-x {
504 position: absolute;
505 top: 16px;
506 right: 16px;
507 cursor: pointer;
508 color: var(--am-c-main-text);
509 margin: 0;
510
511 &.am-rtl {
512 right: auto;
513 left: 16px;
514 }
515 }
516
517 &__ps {
518 &-popup {
519 position: relative;
520
521 &__heading {
522 font-size: 14px;
523 font-weight: 400;
524 line-height: 1.42857;
525 text-align: center;
526 color: var(--am-c-main-text);
527 margin: 0 0 16px;
528 padding: 20px 6px 0 0;
529 max-height: 40px;
530 overflow-x: hidden;
531
532 // Main Scroll styles
533 &::-webkit-scrollbar {
534 width: 6px;
535 }
536
537 &::-webkit-scrollbar-thumb {
538 border-radius: 6px;
539 background: var(--am-c-scroll-op30);
540 }
541
542 &::-webkit-scrollbar-track {
543 border-radius: 6px;
544 background: var(--am-c-scroll-op10);
545 }
546 }
547
548 &__or {
549 display: flex;
550 flex-direction: row;
551 font-weight: 400;
552 font-size: 14px;
553 line-height: 20px;
554 margin: 20px 0;
555 color: var(--am-c-ps-text-op60);
556
557 &:before,
558 &:after {
559 content: '';
560 flex: 1 1;
561 border-bottom: 1px solid var(--am-c-ps-text-op20);
562 margin: auto;
563 }
564
565 &:before {
566 margin-right: 10px;
567 }
568
569 &:after {
570 margin-left: 10px;
571 }
572 }
573
574 &__btn {
575 &.am-button.am-button--medium {
576 --am-h-btn: 56px;
577 --am-fs-btn: 14px;
578 line-height: 16px;
579 width: 100%;
580 justify-content: space-between;
581 }
582
583 &-mobile {
584 &.am-button.am-button--medium {
585 --am-fs-btn: 12px;
586 }
587 }
588 }
589 }
590
591 &-pill {
592 display: inline-block;
593 font-size: 14px;
594 font-weight: 500;
595 line-height: 1;
596 color: var(--am-c-btn-prim);
597 background-color: var(--am-c-btn-prim-text);
598 border-radius: 12px;
599 padding: 5px 8px;
600 }
601 }
602 }
603
604 .el-skeleton {
605 width: 100%;
606 padding: 16px 32px;
607
608 &.skeleton-mobile {
609 .el-skeleton {
610 padding: 16px;
611 }
612 }
613
614 &-item-wrapper {
615 padding: 8px 0;
616 display: flex;
617 align-items: flex-start;
618 flex-direction: column;
619
620 .el-skeleton__h3 {
621 margin-bottom: 4px;
622 width: 100px;
623 height: 20px;
624 }
625
626 .el-skeleton__text {
627 width: 100%;
628 height: 40px;
629 }
630 }
631 }
632 }
633 </style>
634