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 / ServiceStep / ServiceStep.vue
ameliabooking / v3 / src / views / public / StepForm / ServiceStep Last commit date
ServiceStep.vue 1 day ago
ServiceStep.vue
550 lines
1 <template>
2 <StepCardLayout
3 ref="stepCardLayoutReference"
4 :custom-class="[props.globalClass, 'am-fs-service-step']"
5 :allow-popup="true"
6 :card-selected="store.getters['booking/getServiceId'] !== null"
7 >
8 <template #filters>
9 <AmInput
10 v-if="customizeOptions.search.visibility"
11 v-model="searchFilter"
12 :placeholder="amLabels.filter_input"
13 :prefix-icon="iconSearch"
14 :disabled="keyboardSelectionDisabled"
15 />
16 <AmSelect
17 v-if="customizeOptions.category.visibility && categoryOptions.length > 1"
18 v-model="categoryFilter"
19 :placeholder="amLabels.select_service_category"
20 class="am-fs__init-category-select"
21 :no-match-text="amLabels.dropdown_empty"
22 :filterable="customizeOptions.category.filterable"
23 :fit-input-width="true"
24 clearable
25 :disabled="keyboardSelectionDisabled"
26 :filter-method="filterCategory"
27 >
28 <AmOption
29 v-for="category in filteredCategories"
30 :key="category.id"
31 :value="category.id"
32 :label="category.name"
33 >
34 {{ category.name }}
35 </AmOption>
36 </AmSelect>
37 </template>
38
39 <!-- Card -->
40 <StepCard
41 v-for="service in serviceOptions"
42 :key="service.id"
43 :item="service"
44 :selected="store.getters['booking/getServiceId'] === service.id"
45 :disabled="disabledService(service)"
46 :is-person="false"
47 :item-name="service.name"
48 :price="calculateServicePrice(service.id)"
49 :price-visibility="customizeOptions.price.visibility"
50 :tax-visibility="
51 customizeOptions.tax.visibility && useTaxVisibility(store, service.id, 'service')
52 "
53 :tax-excluded="amSettings.payments.taxes.excluded"
54 :info-items="serviceInfoArray(service)"
55 :info-btn-visibility="customizeOptions.moreBtn.visibility"
56 :packages-btn-visibility="
57 !!(
58 !licence.isLite &&
59 !licence.isStarter &&
60 servicePackage(service.id).length &&
61 shortcodeData.show !== 'services' &&
62 useCart(store).length <= 1
63 ) && customizeOptions.packagesBtn.visibility
64 "
65 :parent-width="componentWidth"
66 :keyboard-selection-disabled="keyboardSelectionDisabled"
67 :labels="amLabels"
68 @select-item="selectService(service)"
69 @trigger-info-popup="
70 () => {
71 moreInfoVisibility = true
72 selectedService = service
73 }
74 "
75 @trigger-packages-popup="triggerPackagesPopup(service)"
76 />
77 <!-- /Card -->
78
79 <!-- Empty State -->
80 <EmptyState v-if="serviceOptions.length === 0" :heading="amLabels.no_results_found" />
81 <!-- /Empty State -->
82 </StepCardLayout>
83
84 <!-- More Info -->
85 <MoreInfoPopup
86 v-if="moreInfoVisibility"
87 v-model:visibility="moreInfoVisibility"
88 :heading="amLabels.service_information"
89 :item-name="selectedService.name"
90 :item="selectedService"
91 :employees-heading="amLabels.employees"
92 :employees-data="displayServiceEmployees(selectedService.id)"
93 :locations-heading="amLabels.locations"
94 :locations-data="displayServiceLocation(selectedService.id)"
95 />
96 <!-- /More Info -->
97 </template>
98
99 <script setup>
100 // * Import from Vue
101 import {
102 ref,
103 computed,
104 inject,
105 nextTick,
106 watch,
107 watchEffect,
108 reactive,
109 onMounted,
110 onUnmounted,
111 } from 'vue'
112
113 // * Import from Vuex
114 import { useStore } from 'vuex'
115
116 // * Import components
117 import StepCardLayout from '../common/StepCardLayout.vue'
118 import AmInput from '../../../_components/input/AmInput.vue'
119 import AmSelect from '../../../_components/select/AmSelect.vue'
120 import AmOption from '../../../_components/select/AmOption.vue'
121 import IconComponent from '../../../_components/icons/IconComponent.vue'
122 import MoreInfoPopup from '../common/MoreInfoPopup.vue'
123 import StepCard from '../common/StepCard.vue'
124 import EmptyState from '../../Cabinet/common/parts/EmptyState.vue'
125
126 // * Composables
127 import useAction from '../../../../assets/js/public/actions'
128 import {
129 useServiceLocation,
130 useEmployeesServiceCapacity,
131 useServiceDuration,
132 useServicePrice,
133 } from '../../../../assets/js/public/catalog'
134 import { useTaxVisibility } from '../../../../assets/js/common/pricing'
135 import { useFormattedPrice } from '../../../../assets/js/common/formatting'
136 import { useElementSize } from '@vueuse/core'
137 import { useCart } from '../../../../assets/js/public/cart'
138
139 import { defaultCustomizeSettings } from '../../../../assets/js/common/defaultCustomize'
140
141 // * Store
142 const store = useStore()
143
144 // * Props
145 const props = defineProps({
146 globalClass: {
147 type: String,
148 default: '',
149 },
150 })
151
152 // * Short Code
153 const shortcodeData = inject('shortcodeData')
154
155 // * Customize
156 const amCustomize = inject('amCustomize')
157 let customizeOptions = computed(
158 () => amCustomize.serviceStep?.options || defaultCustomizeSettings.sbsNew.serviceStep.options,
159 )
160
161 // * Component reference
162 const stepCardLayoutReference = ref(null)
163 const stepCardLayoutDom = computed(() => stepCardLayoutReference.value?.stepCardLayoutRef || null)
164
165 // * Component width
166 const { width: componentWidth } = useElementSize(stepCardLayoutDom)
167
168 const packagesPopupVisibility = computed(() => {
169 const popupVisibility = stepCardLayoutReference.value?.packagesVisibility
170
171 if (typeof popupVisibility === 'boolean') {
172 return popupVisibility
173 }
174
175 return popupVisibility?.value ?? false
176 })
177
178 // * Plugin Licence
179 let licence = inject('licence')
180
181 // * Amelia Settings
182 const amSettings = inject('settings')
183
184 // * Labels
185 let labels = 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 // * Labels
194 const amLabels = computed(() => {
195 let computedLabels = reactive({ ...labels })
196
197 const customizedLabels = amCustomize.serviceStep?.translations
198
199 if (customizedLabels) {
200 Object.keys(customizedLabels).forEach((labelKey) => {
201 const labelData = customizedLabels[labelKey]
202 const localizedLabel = labelData[localLanguage.value]
203
204 if (localizedLabel && langDetection.value) {
205 computedLabels[labelKey] = localizedLabel
206 } else if (labelData.default) {
207 computedLabels[labelKey] = labelData.default
208 }
209 })
210 }
211
212 return computedLabels
213 })
214
215 // * Sidebar steps
216 let { sidebarDataCollector } = inject('sidebarStepsFunctions', {
217 sidebarDataCollector: () => {},
218 })
219
220 const { changeInitStepDataService } = inject('initDataChanges', {
221 changeInitStepDataService: () => {},
222 })
223
224 const { footerBtnDisabledUpdater } = inject('changingStepsFunctions', {
225 footerBtnDisabledUpdater: () => {},
226 })
227
228 // * Entities
229 let amEntities = computed(() => {
230 return store.state.entities
231 })
232
233 const locationsRules = computed(() => store.getters['entities/getLocations'].length <= 1)
234 const employeesRules = computed(() => store.getters['entities/getEmployees'].length === 1)
235
236 // * Set Service and Category Options from entities
237 let initStepOrder = [{ id: 'ServiceStep' }, { id: 'EmployeeStep' }, { id: 'LocationStep' }]
238
239 let stepOrder = computed(() => {
240 return 'order' in amCustomize ? amCustomize.order : initStepOrder
241 })
242
243 function filterRules(entityName = '') {
244 const idx = stepOrder.value.findIndex((step) => step.id === 'ServiceStep')
245 let arrRules = stepOrder.value.slice(0, idx)
246 const obj = {
247 serviceId: null,
248 categoryId: entityName === 'category' ? null : categoryFilter.value,
249 providerId: null,
250 locationId: null,
251 }
252 if (arrRules.some((step) => step.id === 'EmployeeStep') || employeesRules.value)
253 delete obj.providerId
254 if (arrRules.some((step) => step.id === 'LocationStep') || locationsRules.value)
255 delete obj.locationId
256 return obj
257 }
258
259 // * FILTER
260 let searchFilter = computed({
261 get: () => store.getters['stepByStepFilters/getSearchFilterText'],
262 set: (value) => {
263 store.commit('stepByStepFilters/setSearchFilterText', value)
264 },
265 })
266
267 let iconSearch = {
268 components: { IconComponent },
269 template: `<IconComponent icon="search"/>`,
270 }
271 function searchingStrings(name) {
272 let arr = []
273 searchFilter.value
274 .toLowerCase()
275 .split(' ')
276 .forEach((item) => {
277 arr.push(name.toLowerCase().includes(item))
278 })
279
280 return arr.filter((a) => a === false).length <= 0
281 }
282
283 // * Category Filter
284 const categoryFilter = computed({
285 get: () => store.getters['stepByStepFilters/getCategoryFilter'],
286 set: (value) => {
287 value = value ? value : null
288 store.commit('stepByStepFilters/setCategoryFilter', value)
289 },
290 })
291
292 // * Set Category Options from entities
293 let categoryOptions = computed(() =>
294 store.getters['entities/filteredCategories'](
295 Object.assign(store.getters['booking/getSelection'], filterRules('category')),
296 ),
297 )
298
299 let queryLower = ref('')
300 function filterCategory(query) {
301 queryLower.value = query.toLowerCase()
302 }
303
304 let filteredCategories = computed(() => {
305 if (queryLower.value) {
306 return categoryOptions.value.filter((item) => {
307 return item.name.toLowerCase().includes(queryLower.value)
308 })
309 }
310 return categoryOptions.value
311 })
312
313 let serviceOptions = computed(() => {
314 const services = store.getters['entities/filteredServices'](
315 Object.assign(store.getters['booking/getSelection'], filterRules()),
316 )
317
318 if (searchFilter.value) {
319 return services.filter((service) => searchingStrings(service.name))
320 }
321
322 if (categoryFilter.value) {
323 return services
324 }
325
326 // Sort services by position property in ascending order
327 return services.sort((a, b) => a.position - b.position)
328 })
329
330 let selectedService = ref(null)
331
332 function selectService(service = null) {
333 if (disabledService(service)) {
334 return
335 }
336
337 let serviceData = {
338 reference: 'service',
339 // position will depends on fields order
340 position: 0,
341 value: service.id !== store.getters['booking/getServiceId'] ? service.name : '',
342 }
343
344 if (service.id !== store.getters['booking/getServiceId']) {
345 store.commit('booking/setServiceId', service.id)
346 let category = categoryOptions.value.find((item) => item.id === service.categoryId)
347 store.commit('booking/setCategoryId', category ? category.id : null)
348 } else {
349 store.commit('booking/setServiceId', null)
350 store.commit('booking/setCategoryId', null)
351 }
352
353 changeInitStepDataService()
354
355 useAction(store, {}, 'SelectService', 'appointment', null, null)
356
357 sidebarDataCollector(serviceData)
358
359 if (serviceData.value) {
360 footerBtnDisabledUpdater(false)
361 } else {
362 footerBtnDisabledUpdater(true)
363 }
364 }
365
366 function disabledService(service) {
367 const employeeId = store.getters['booking/getEmployeeId']
368 const locationId = store.getters['booking/getLocationId']
369 if (employeeId !== null) {
370 return !displayServiceEmployees(service.id).some((emp) => emp.id === employeeId)
371 }
372
373 if (locationId !== null) {
374 return !displayServiceLocation(service.id).some((loc) => loc.id === locationId)
375 }
376 return false
377 }
378
379 function servicePackage(serviceId) {
380 return store.getters['entities/filteredPackages'](
381 Object.assign(store.getters['booking/getSelection'], {
382 categoryId: null,
383 serviceId: serviceId,
384 }),
385 ).filter((pack) => pack.available && pack.status === 'visible')
386 }
387
388 function calculateServicePrice(id) {
389 if (useServicePrice(amEntities.value, id).min || useServicePrice(amEntities.value, id).max) {
390 return useServicePrice(amEntities.value, id).price
391 }
392
393 return amLabels.value.free
394 }
395
396 function serviceInfoArray(service) {
397 let arr = []
398 if (service.categoryId && customizeOptions.value.serviceCategory.visibility) {
399 let category = categoryOptions.value.find((c) => c.id === service.categoryId)
400 if (category) {
401 arr.push({
402 icon: 'folder',
403 name: category.name,
404 })
405 }
406 }
407
408 if (service.duration && customizeOptions.value.serviceDuration.visibility) {
409 arr.push({
410 icon: 'clock',
411 name: useServiceDuration(service.duration),
412 })
413 }
414
415 if (!licence.isLite && !licence.isStarter && customizeOptions.value.serviceCapacity.visibility) {
416 arr.push({
417 icon: 'user',
418 name: useEmployeesServiceCapacity(amEntities.value, service.id),
419 })
420 }
421
422 if (
423 customizeOptions.value.serviceLocation.visibility &&
424 displayServiceLocationLabel(service.id).length > 0
425 ) {
426 arr.push({
427 icon: 'locations',
428 name: displayServiceLocationLabel(service.id),
429 })
430 }
431 return arr
432 }
433
434 // * EMPLOYEES
435 function displayServiceEmployees(serviceId) {
436 let arr = []
437 let employeesIds = Object.keys(amEntities.value.entitiesRelations)
438
439 employeesIds.forEach((employeeId) => {
440 if (
441 serviceId in amEntities.value.entitiesRelations[employeeId] &&
442 amEntities.value.employees.find((a) => a.id === parseInt(employeeId)) &&
443 amEntities.value.employees.find((a) => a.id === parseInt(employeeId)).status === 'visible'
444 ) {
445 let employee = amEntities.value.employees.find((a) => a.id === parseInt(employeeId))
446 let price = employee ? serviceEmployeePrice(employee) : ''
447 arr.push({ ...employee, price })
448 }
449 })
450
451 return arr
452 }
453
454 let infoEmployeeData = ref([])
455
456 function serviceEmployeePrice(employee) {
457 if (selectedService.value) {
458 let employeeService = employee.serviceList.find((a) => a.id === selectedService.value.id)
459
460 if (employeeService && employeeService.price - selectedService.value.price !== 0)
461 return `${
462 employeeService.price - selectedService.value.price > 0
463 ? selectedService.value.price > 0
464 ? '+'
465 : ''
466 : '-'
467 } ${useFormattedPrice(employeeService.price - selectedService.value.price)}`
468 }
469 return ''
470 }
471
472 // * LOCATION
473 function displayServiceLocationLabel(id) {
474 let locations = useServiceLocation(amEntities.value, id)
475 if (locations.length === 0) return ''
476
477 if (
478 locations.length === 1 ||
479 (locations.length && locations.every((location) => location.id === locations[0].id))
480 ) {
481 return locations[0].address ? locations[0].address : locations[0].name
482 }
483 return amLabels.value.multiple_locations
484 }
485
486 function displayServiceLocation(serviceId) {
487 return useServiceLocation(amEntities.value, serviceId).filter(
488 (loc, idx, arr) => arr.findIndex((l) => l.id === loc.id) === idx,
489 )
490 }
491
492 // * Service - More Info
493 let moreInfoVisibility = ref(false)
494
495 const keyboardSelectionDisabled = computed(() => {
496 return moreInfoVisibility.value || packagesPopupVisibility.value
497 })
498
499 watch(moreInfoVisibility, (isVisible, wasVisible) => {
500 if (wasVisible && !isVisible) {
501 stepCardLayoutReference.value?.focusSelectedOrFirstItem?.()
502 }
503 })
504
505 watchEffect(() => {
506 if (!moreInfoVisibility.value) {
507 infoEmployeeData.value = []
508 }
509 })
510
511 // * Service Packages
512 function triggerPackagesPopup(service) {
513 if (disabledService(service)) {
514 return
515 }
516
517 if (service.id !== store.getters['booking/getServiceId']) {
518 selectService(service)
519 }
520 nextTick(() => {
521 stepCardLayoutReference.value.packagesPopupFooterVisibility = false
522 stepCardLayoutReference.value.packagesVisibility = true
523 })
524 }
525
526 // * Mounted hook
527 onMounted(() => {
528 // Update footer button state
529 footerBtnDisabledUpdater(store.getters['booking/getServiceId'] === null)
530 })
531
532 onUnmounted(() => {
533 footerBtnDisabledUpdater(false)
534 })
535 </script>
536
537 <script>
538 export default {
539 name: 'ServiceStep',
540 key: 'serviceStep',
541 sidebarData: {
542 label: 'service_selection',
543 icon: 'service',
544 stepSelectedData: [],
545 finished: false,
546 selected: false,
547 },
548 }
549 </script>
550