PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 2.4.4
Booking for Appointments and Events Calendar – Amelia v2.4.4
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 / common / StepCardLayout.vue
ameliabooking / v3 / src / views / public / StepForm / common Last commit date
MoreInfoPopup.vue 5 days ago StepCard.vue 5 days ago StepCardLayout.vue 5 days ago
StepCardLayout.vue
401 lines
1 <template>
2 <div
3 v-if="loaded"
4 ref="stepCardLayoutRef"
5 class="am-fs-scl"
6 :class="[props.customClass, { 'am-oxvisible': bringingAnyoneVisibility || packagesVisibility }]"
7 :style="cssVars"
8 >
9 <div class="am-fs-scl__filters" :class="responsiveClass">
10 <slot name="filters" />
11 </div>
12 <div class="am-fs-scl__content">
13 <slot />
14 </div>
15
16 <!-- Bringing Anyone with you -->
17 <AmSlidePopup
18 v-if="bringingAnyoneOptions.availability"
19 ref="bringingPopupRef"
20 :visibility="bringingAnyoneVisibility"
21 class="am-fs__init__bringing"
22 >
23 <p
24 class="am-fs__popup-x"
25 :class="{ 'am-rtl': isRtl }"
26 tabindex="0"
27 @click="closeBringingPopup"
28 @keydown.enter="closeBringingPopup"
29 >
30 <AmeliaIconClose></AmeliaIconClose>
31 </p>
32 <BringingAnyone :in-popup="true" />
33 <template #footer>
34 <AmButton
35 v-if="
36 bringingAnyoneOptions.min !== bringingAnyoneOptions.max &&
37 bringingAnyoneOptions.min <= 0
38 "
39 category="secondary"
40 :type="amCustomize.bringingAnyone.options.secondaryButton.buttonType"
41 :disabled="
42 bringingAnyoneOptions.min === bringingAnyoneOptions.max ||
43 (!amSettings.appointments.allowBookingIfNotMin && bringingAnyoneOptions.min > 0)
44 "
45 @click="noOneBringWith"
46 >
47 {{ bringingLabels.bringing_no }}
48 </AmButton>
49 <AmButton
50 :type="
51 bringingAnyoneOptions.min !== bringingAnyoneOptions.max &&
52 bringingAnyoneOptions.min <= 0
53 ? amCustomize.bringingAnyone.options.primaryButton.buttonType
54 : amCustomize.bringingAnyone.options.primaryFooterButton.buttonType
55 "
56 :disabled="
57 bookingPersons ===
58 (amSettings.appointments.bringingAnyoneLogic === 'additional' ? 1 : 0)
59 "
60 @click="bringPeopleWithYou"
61 >
62 <template
63 v-if="
64 bringingAnyoneOptions.min !== bringingAnyoneOptions.max &&
65 bringingAnyoneOptions.min <= 0
66 "
67 >
68 {{ bringingLabels.bringing_yes }}
69 </template>
70 <template v-else>
71 {{ bringingLabels.continue }}
72 </template>
73 </AmButton>
74 </template>
75 </AmSlidePopup>
76 <!--/ Bringing Anyone with you -->
77
78 <!-- Packages Popup -->
79 <PackagesPopup
80 class="am-fs__init__package"
81 :footer-visibility="packagesPopupFooterVisibility"
82 @continue-with-service="continueWithService()"
83 @close-package-popup="closePackagePopup"
84 />
85 <!--/ Packages Popup -->
86 </div>
87 </template>
88
89 <script setup>
90 // * Import from Vue
91 import { computed, inject, nextTick, provide, reactive, ref, watchEffect, watch } from 'vue'
92
93 // * Import from Vuex
94 import { useStore } from 'vuex'
95
96 // * Import components
97 import AmButton from '../../../_components/button/AmButton.vue'
98 import AmSlidePopup from '../../../_components/slide-popup/AmSlidePopup.vue'
99 import PackagesPopup from '../PakagesStep/parts/PackagesPopup.vue'
100 import BringingAnyone from '../BringingAnyone/BringingAnyone.vue'
101 import AmeliaIconClose from '../../../_components/icons/IconClose.vue'
102
103 // * Composables
104 import { useCapacity } from '../../../../assets/js/common/appointments'
105 import { useCart } from '../../../../assets/js/public/cart'
106 import { useColorTransparency } from '../../../../assets/js/common/colorManipulation'
107 import { useElementSize } from '@vueuse/core'
108 import { useResponsiveClass } from '../../../../assets/js/common/responsive'
109
110 // * Store
111 const store = useStore()
112
113 // * Props
114 const props = defineProps({
115 customClass: {
116 type: [String, Array],
117 default: '',
118 },
119 cardSelected: {
120 type: Boolean,
121 default: false,
122 },
123 allowPopup: {
124 type: Boolean,
125 default: false,
126 },
127 })
128
129 // * Step Card Layout reference
130 const stepCardLayoutRef = ref(null)
131 // * Component width
132 const { width: componentWidth } = useElementSize(stepCardLayoutRef)
133 // * Responsive class
134 const responsiveClass = computed(() => {
135 return useResponsiveClass(componentWidth.value)
136 })
137
138 // * Loaded state
139 let loaded = computed(() => {
140 return store.getters['entities/getReady']
141 })
142
143 // * Amelia Settings
144 const amSettings = inject('settings')
145
146 // * Short Code
147 const shortcodeData = inject('shortcodeData')
148
149 // * Labels
150 const globalLabels = inject('labels')
151
152 // * local language short code
153 const localLanguage = inject('localLanguage')
154
155 // * if local lang is in settings lang
156 let langDetection = computed(() => amSettings.general.usedLanguages.includes(localLanguage.value))
157
158 // * Computed labels
159 let amLabels = computed(() => {
160 let computedLabels = reactive({ ...globalLabels })
161
162 if (amSettings.customizedData?.sbsNew?.initStep?.translations) {
163 let customizedLabels = amSettings.customizedData.sbsNew.initStep.translations
164 Object.keys(customizedLabels).forEach((labelKey) => {
165 if (customizedLabels[labelKey][localLanguage.value] && langDetection.value) {
166 computedLabels[labelKey] = customizedLabels[labelKey][localLanguage.value]
167 } else if (customizedLabels[labelKey].default) {
168 computedLabels[labelKey] = customizedLabels[labelKey].default
169 }
170 })
171 }
172 return computedLabels
173 })
174
175 provide('amLabels', amLabels)
176
177 let bringingLabels = computed(() => {
178 let computedLabels = reactive({ ...globalLabels })
179
180 if (amSettings.customizedData?.sbsNew?.bringingAnyone?.translations) {
181 let customizedLabels = amSettings.customizedData.sbsNew.bringingAnyone.translations
182 Object.keys(customizedLabels).forEach((labelKey) => {
183 if (customizedLabels[labelKey][localLanguage.value] && langDetection.value) {
184 computedLabels[labelKey] = customizedLabels[labelKey][localLanguage.value]
185 } else if (customizedLabels[labelKey].default) {
186 computedLabels[labelKey] = customizedLabels[labelKey].default
187 }
188 })
189 }
190 return computedLabels
191 })
192
193 // * Customization
194 let amCustomize = inject('amCustomize')
195
196 // * Document text orientation
197 let isRtl = computed(() => store.getters['getIsRtl'])
198
199 // * Step Functions
200 const { nextStep, footerButtonReset, footerButtonClicked } = inject('changingStepsFunctions', {
201 nextStep: () => {},
202 footerButtonReset: () => {},
203 footerButtonClicked: {
204 value: false,
205 },
206 })
207
208 // * Bringing Anyone block
209 let bringingAnyoneOptions = computed(() => {
210 return useCapacity(
211 store.getters['entities/getEmployeeServices'](
212 store.getters['booking/getServiceProviderSelection'],
213 ),
214 )
215 })
216
217 provide('bringingOptions', {
218 bringingAnyoneOptions,
219 })
220
221 // * Bringing anyone with you pop up visibility
222 let bringingAnyoneVisibility = ref(false)
223 let bringingPopupRef = ref(null)
224
225 // * Booking persons
226 let bookingPersons = computed(() => {
227 return store.getters['booking/getBookingPersons']
228 })
229
230 function noOneBringWith() {
231 closeBringingPopup()
232 store.commit('booking/setBookingPersons', 0)
233 nextStep()
234 }
235
236 function bringPeopleWithYou() {
237 nextStep()
238 }
239
240 function continueWithService() {
241 packagesVisibility.value = false
242 if (bringingAnyoneOptions.value.availability) {
243 bringingAnyoneVisibility.value = true
244 } else {
245 nextStep()
246 }
247 }
248
249 function closeBringingPopup() {
250 bringingAnyoneVisibility.value = false
251 }
252
253 // * Focus management for keyboard navigation on Bringing Anyone popup
254 watch(bringingAnyoneVisibility, async (isVisible) => {
255 if (isVisible) {
256 // Wait for DOM to update
257 await nextTick()
258 // Find the input number field and focus it
259 const numberInput = bringingPopupRef.value?.$el?.querySelector('.am-input-number input')
260 if (numberInput) {
261 numberInput.focus()
262 }
263 }
264 })
265
266 async function focusSelectedOrFirstItem() {
267 await nextTick()
268
269 const layoutElement = stepCardLayoutRef.value
270
271 if (!layoutElement) {
272 return
273 }
274
275 const selectedItem = layoutElement.querySelector(
276 '.am-fs__init-item.am--selected:not(.am--disabled)[tabindex="0"]',
277 )
278
279 const firstFocusableItem = layoutElement.querySelector(
280 '.am-fs__init-item:not(.am--disabled)[tabindex="0"]',
281 )
282
283 ;(selectedItem || firstFocusableItem)?.focus()
284 }
285
286 // * Package popup
287 let packagesOptions = computed(() =>
288 store.getters['entities/filteredPackages'](store.getters['booking/getSelection']),
289 )
290
291 let packagesPopupFooterVisibility = ref(true)
292
293 let packagesVisibility = ref(false)
294 provide('packagesVisibility', packagesVisibility)
295
296 function closePackagePopup() {
297 packagesPopupFooterVisibility.value = true
298 focusSelectedOrFirstItem()
299 }
300
301 watchEffect(() => {
302 if (footerButtonClicked.value) {
303 footerButtonReset()
304 if (props.cardSelected) {
305 if (props.allowPopup) {
306 if (
307 packagesOptions.value.length &&
308 shortcodeData.value.show !== 'services' &&
309 useCart(store).length <= 1
310 ) {
311 packagesVisibility.value = true
312 } else {
313 continueWithService()
314 }
315 } else {
316 nextStep()
317 }
318 }
319 }
320 })
321
322 defineExpose({
323 focusSelectedOrFirstItem,
324 packagesVisibility,
325 packagesPopupFooterVisibility,
326 stepCardLayoutRef,
327 })
328
329 const amColors = inject(
330 'amColors',
331 ref({
332 colorPrimary: '#1246D6',
333 colorSuccess: '#019719',
334 colorError: '#B4190F',
335 colorWarning: '#CCA20C',
336 colorMainBgr: '#FFFFFF',
337 colorMainHeadingText: '#33434C',
338 colorMainText: '#1A2C37',
339 colorSbBgr: '#17295A',
340 colorSbText: '#FFFFFF',
341 colorInpBgr: '#FFFFFF',
342 colorInpBorder: '#D1D5D7',
343 colorInpText: '#1A2C37',
344 colorInpPlaceHolder: '#808A90',
345 colorDropBgr: '#FFFFFF',
346 colorDropBorder: '#D1D5D7',
347 colorDropText: '#0E1920',
348 colorBtnPrim: '#265CF2',
349 colorBtnPrimText: '#FFFFFF',
350 colorBtnSec: '#1A2C37',
351 colorBtnSecText: '#FFFFFF',
352 }),
353 )
354
355 let cssVars = computed(() => {
356 return {
357 '--am-c-primary': amColors.value.colorPrimary,
358 '--am-c-primary-op05': useColorTransparency(amColors.value.colorPrimary, 0.05),
359 '--am-c-main-bgr': amColors.value.colorMainBgr,
360 '--am-c-main-heading-text': amColors.value.colorMainHeadingText,
361 '--am-c-main-text': amColors.value.colorMainText,
362 '--am-c-main-text-op80': useColorTransparency(amColors.value.colorMainText, 0.8),
363 '--am-c-inp-border': amColors.value.colorInpBorder,
364 '--am-c-scroll-op30': useColorTransparency(amColors.value.colorPrimary, 0.3),
365 '--am-c-scroll-op10': useColorTransparency(amColors.value.colorPrimary, 0.1),
366 }
367 })
368 </script>
369
370 <style lang="scss">
371 // scl - Step Card Layout
372 .amelia-v2-booking #amelia-container {
373 .am-fs-scl {
374 display: flex;
375 flex-direction: column;
376 gap: 16px;
377
378 &.am-oxvisible {
379 overflow-x: visible;
380 }
381
382 &__filters {
383 display: flex;
384 flex-direction: row;
385 gap: 16px;
386
387 &.am-rw-360 {
388 flex-direction: column;
389 }
390 }
391
392 &__content {
393 display: flex;
394 flex-direction: column;
395 flex-wrap: wrap;
396 gap: 8px;
397 }
398 }
399 }
400 </style>
401