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