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 / BringingAnyone / BringingAnyone.vue
ameliabooking / v3 / src / views / public / StepForm / BringingAnyone Last commit date
BringingAnyone.vue 1 month ago
BringingAnyone.vue
558 lines
1 <template>
2 <div
3 class="am-fs__bringing"
4 :class="[
5 !props.inPopup && checkScreen
6 ? 'am-fs__bringing-full-mobile'
7 : !props.inPopup
8 ? 'am-fs__bringing-full'
9 : '',
10 { 'am-fs__bringing-full-mobile-s': mobileS },
11 ]"
12 :style="cssVars"
13 :tabindex="!props.inPopup ? 0 : -1"
14 >
15 <div class="am-fs__bringing-main">
16 <div
17 v-if="props.inPopup && headingVisibility"
18 class="am-fs__bringing-heading"
19 :aria-label="amLabels.bringing_anyone_title"
20 >
21 {{ amLabels.bringing_anyone_title }}
22 </div>
23 <div class="am-fs__bringing-content">
24 <span class="am-fs__bringing-content-left">
25 <span class="am-icon-users"></span>
26 <span
27 class="am-fs__bringing-content-text"
28 :aria-label="
29 amSettings.appointments.bringingAnyoneLogic === 'additional'
30 ? amLabels.bringing_people
31 : amLabels.bringing_people_total
32 "
33 >
34 {{
35 amSettings.appointments.bringingAnyoneLogic === 'additional'
36 ? amLabels.bringing_people
37 : amLabels.bringing_people_total
38 }}
39 </span>
40 </span>
41 <AmInputNumber v-model="persons" :min="options.min" :max="options.max" size="small" />
42 </div>
43 <div
44 v-if="infoVisibility"
45 class="am-fs__bringing-message"
46 :aria-label="
47 amSettings.appointments.bringingAnyoneLogic === 'additional'
48 ? amLabels.add_people
49 : amLabels.add_people_total
50 "
51 >
52 {{
53 amSettings.appointments.bringingAnyoneLogic === 'additional'
54 ? amLabels.add_people
55 : amLabels.add_people_total
56 }}
57 </div>
58 </div>
59
60 <div
61 v-if="Object.keys(personPricing).length > 1 && pricingVisibility"
62 class="am-fs__bringing-main"
63 >
64 <div v-if="service.customPricing.enabled === 'person'" class="am-fs__bringing-content-price">
65 <span class="am-fs__bringing-content-price-left">
66 <span class="am-icon-service"></span>
67 <span class="am-fs__bringing-content-text">{{ amLabels.bringing_price }}</span>
68 </span>
69 <p
70 v-for="(item, range) in personPricing"
71 :key="range"
72 class="am-fs__bringing-content-text am-fs__bringing-content-price-text"
73 :class="{
74 'am-fs__bringing-content-price-text-selected': selectedGroup(item.from, item.to),
75 }"
76 tabindex="0"
77 role="button"
78 :aria-label="`${item.from === item.to ? item.from : item.from + ' - ' + item.to}: ${item.prices[0] === item.prices[1] ? useFormattedPrice(item.prices[0]) : useFormattedPrice(item.prices[0]) + ' - ' + useFormattedPrice(item.prices[1])}`"
79 @click="rangeSelected(item.from)"
80 @keydown.enter="rangeSelected(item.from)"
81 @keydown.space.prevent="rangeSelected(item.from)"
82 >
83 <span class="am-icon-users"></span>
84 <span>{{ item.from === item.to ? item.from : item.from + ' - ' + item.to }}</span>
85 <span>{{
86 item.prices[0] === item.prices[1]
87 ? useFormattedPrice(item.prices[0])
88 : useFormattedPrice(item.prices[0]) + ' - ' + useFormattedPrice(item.prices[1])
89 }}</span>
90 </p>
91 </div>
92 </div>
93
94 <!-- Packages Popup -->
95 <PackagesPopup @continue-with-service="packagesVisibility = false" />
96 <!--/ Packages Popup -->
97 </div>
98 </template>
99
100 <script setup>
101 import { ref, reactive, computed, inject, watchEffect, provide, onMounted } from 'vue'
102 import { useStore } from 'vuex'
103
104 import AmInputNumber from '../../../_components/input-number/AmInputNumber.vue'
105 import PackagesPopup from '../PakagesStep/parts/PackagesPopup'
106
107 import { useColorTransparency } from '../../../../assets/js/common/colorManipulation'
108 import { useCapacity } from '../../../../assets/js/common/appointments'
109
110 import { useFormattedPrice } from '../../../../assets/js/common/formatting'
111
112 let props = defineProps({
113 globalClass: {
114 type: String,
115 default: '',
116 },
117 inPopup: {
118 type: Boolean,
119 default: false,
120 },
121 })
122
123 // Container Width
124 let cWidth = inject('containerWidth', 0)
125 let checkScreen = computed(() => cWidth.value < 560 || (cWidth.value > 560 && cWidth.value < 640))
126 let mobileS = computed(() => cWidth.value < 340)
127
128 // * Store
129 const store = useStore()
130
131 // * Short code
132 const shortcodeData = inject('shortcodeData')
133
134 // * Settings
135 const amSettings = inject('settings')
136
137 // * local language short code
138 const localLanguage = inject('localLanguage')
139
140 // * if local lang is in settings lang
141 let langDetection = computed(() => amSettings.general.usedLanguages.includes(localLanguage.value))
142
143 // * Labels
144 const globalLabels = inject('labels')
145
146 // * Customize
147 const amCustomize = inject('amCustomize')
148
149 let { bringingAnyoneOptions } = inject(
150 'bringingOptions',
151 ref({
152 availability: false,
153 min: 0,
154 max: 1,
155 }),
156 )
157
158 // * Package
159 let packagesOptions = computed(() =>
160 store.getters['entities/filteredPackages'](store.getters['booking/getSelection']),
161 )
162
163 let packagesVisibility = ref(false)
164 provide('packagesVisibility', packagesVisibility)
165
166 // * Computed labels
167 let amLabels = computed(() => {
168 let computedLabels = reactive({ ...globalLabels })
169
170 if (amSettings.customizedData?.sbsNew?.bringingAnyone?.translations) {
171 let customizedLabels = amSettings.customizedData.sbsNew.bringingAnyone.translations
172 Object.keys(customizedLabels).forEach((labelKey) => {
173 if (customizedLabels[labelKey][localLanguage.value] && langDetection.value) {
174 computedLabels[labelKey] = customizedLabels[labelKey][localLanguage.value]
175 } else if (customizedLabels[labelKey].default) {
176 computedLabels[labelKey] = customizedLabels[labelKey].default
177 }
178 })
179 }
180 return computedLabels
181 })
182
183 let headingVisibility = amCustomize ? amCustomize.bringingAnyone.options.heading.visibility : true
184 let infoVisibility = amCustomize ? amCustomize.bringingAnyone.options.info.visibility : true
185 let pricingVisibility = computed(() => {
186 return 'bringingPrice' in amCustomize.bringingAnyone.options
187 ? amCustomize.bringingAnyone.options.bringingPrice.visibility
188 : true
189 })
190
191 // * Step functionality
192 let { nextStep, footerButtonReset, footerButtonClicked } = inject('changingStepsFunctions', {
193 nextStep: () => {},
194 footerButtonReset: () => {},
195 footerButtonClicked: {
196 value: false,
197 },
198 })
199
200 let service = computed(() =>
201 store.getters['entities/getService'](store.getters['booking/getServiceId']),
202 )
203
204 let employeesServices = computed(() =>
205 store.getters['entities/getEmployeeServices'](
206 store.getters['booking/getServiceProviderSelection'],
207 ),
208 )
209
210 let options = computed(() => {
211 if (props.inPopup) {
212 return bringingAnyoneOptions.value
213 }
214
215 return useCapacity(employeesServices.value)
216 })
217
218 let persons = computed({
219 get: () => {
220 return (
221 store.getters['booking/getBookingPersons'] -
222 (amSettings.appointments.bringingAnyoneLogic === 'additional' ? 1 : 0)
223 )
224 },
225 set: (val) => {
226 store.commit('booking/setBookingPersons', val)
227 },
228 })
229
230 if (!props.inPopup) {
231 onMounted(() => {
232 if (!props.inPopup && packagesOptions.value.length && shortcodeData.value.show !== 'services') {
233 packagesVisibility.value = true
234 }
235 })
236
237 // * Watching when footer button was clicked
238 watchEffect(() => {
239 if (footerButtonClicked.value) {
240 footerButtonReset()
241 nextStep()
242 }
243 })
244 }
245
246 function rangeSelected(range) {
247 store.commit(
248 'booking/setBookingPersons',
249 range - (amSettings.appointments.bringingAnyoneLogic === 'additional' ? 1 : 0),
250 )
251 }
252
253 let personPricing = computed(() => {
254 let service = store.getters['entities/getService'](store.getters['booking/getServiceId'])
255
256 let maxCapacity = 0
257
258 let ranges = {}
259
260 let allowedRanges = {}
261
262 employeesServices.value.forEach((service) => {
263 Object.keys(service.customPricing.persons).forEach((key) => {
264 if (service.maxCapacity >= service.customPricing.persons[key].from) {
265 allowedRanges[service.customPricing.persons[key].from] = true
266 }
267 })
268
269 if (service.maxCapacity > maxCapacity) {
270 maxCapacity = service.maxCapacity
271 }
272 })
273
274 if (persons.value > maxCapacity) {
275 store.commit('booking/setBookingPersons', maxCapacity)
276 }
277
278 let additionalOffset = amSettings.appointments.bringingAnyoneLogic === 'additional' ? 1 : 0
279
280 Object.keys(service.customPricing.persons).forEach((key) => {
281 if (
282 service.customPricing.persons[key].from in allowedRanges &&
283 service.customPricing.persons[key].from <= options.value.max + additionalOffset
284 ) {
285 ranges[service.customPricing.persons[key].from] = {
286 from: service.customPricing.persons[key].from,
287 to: parseInt(key),
288 prices: [],
289 }
290 }
291 })
292
293 employeesServices.value.forEach((service) => {
294 Object.keys(service.customPricing.persons).forEach((key, index) => {
295 if (service.customPricing.persons[key].from in ranges) {
296 ranges[service.customPricing.persons[key].from].prices.push(
297 service.customPricing.persons[key].price,
298 )
299
300 if (Object.keys(allowedRanges).length - 1 === index) {
301 ranges[service.customPricing.persons[key].from].to = maxCapacity
302 }
303 }
304 })
305 })
306
307 Object.keys(ranges).forEach((key) => {
308 ranges[key].prices = [Math.min(...ranges[key].prices), Math.max(...ranges[key].prices)]
309 })
310
311 return ranges
312 })
313
314 function selectedGroup(from, range) {
315 let start = amSettings.appointments.bringingAnyoneLogic === 'additional' ? 1 : 0
316
317 return (
318 (persons.value === (start ? 0 : 1) && from === 1) ||
319 (persons.value + start >= from && persons.value + start <= range)
320 )
321 }
322
323 // * Global colors
324 let amColors = inject('amColors')
325 let cssVars = computed(() => {
326 return {
327 '--am-bringing-color-border': useColorTransparency(amColors.value.colorMainText, 0.25),
328 '--am-bringing-color-text-opacity60': useColorTransparency(amColors.value.colorMainText, 0.6),
329 '--am-c-ps-primary': amColors.value.colorPrimary,
330 '--am-c-ps-primary-op10': useColorTransparency(amColors.value.colorPrimary, 0.1),
331 }
332 })
333 </script>
334
335 <script>
336 export default {
337 name: 'BringingAnyone',
338 key: 'bringingAnyone',
339 sidebarData: {
340 label: 'bringing_anyone',
341 icon: 'users-plus',
342 stepSelectedData: [],
343 finished: false,
344 selected: false,
345 },
346 }
347 </script>
348
349 <style lang="scss">
350 @mixin bringing-anyone-block {
351 .am-fs {
352 &__bringing {
353 &-full {
354 padding: 16px 32px;
355
356 &-mobile {
357 padding: 16px;
358 }
359 }
360
361 &-main {
362 margin: 0 0 32px 0;
363 }
364
365 &-heading {
366 display: block;
367 width: 100%;
368 font-size: 16px;
369 font-weight: 500;
370 line-height: 1.5;
371 color: var(--am-c-main-text);
372 margin: 0 0 4px 0;
373 }
374
375 &-content-persons {
376 display: flex;
377
378 &-text {
379 margin: 0 0 0 8px;
380 }
381 }
382
383 &-content-price {
384 display: block;
385 background-color: var(--am-c-ps-primary-op10);
386
387 &-left {
388 margin-bottom: 5px;
389
390 .am-icon-service {
391 margin-right: 5px;
392 }
393 }
394
395 &-text {
396 padding: 3px;
397 border: 1px solid var(--am-bringing-color-border);
398 border-radius: 8px;
399 display: inline-block;
400 margin: 3px 0 0 3px;
401
402 &:focus {
403 outline: none;
404 border-color: var(--am-c-ps-primary);
405 }
406
407 span {
408 margin: 3px;
409 }
410
411 &-selected {
412 border-color: var(--am-c-ps-primary);
413 background-color: #ffffff;
414 }
415
416 span:nth-child(3) {
417 color: var(--am-c-ps-primary);
418 font-weight: bold;
419 }
420 }
421 }
422
423 &-content {
424 display: flex;
425 }
426
427 &-content,
428 &-content-price {
429 align-items: center;
430 justify-content: space-between;
431 padding: 12px 16px;
432 margin: 0 0 4px 0;
433 border: 1px solid var(--am-bringing-color-border);
434 border-radius: 8px;
435 cursor: pointer;
436
437 &-left {
438 display: flex;
439 align-items: center;
440
441 .am-icon-users {
442 font-size: 24px;
443 line-height: 1;
444 color: var(--am-c-main-text);
445 }
446 }
447
448 &-text {
449 font-size: 14px;
450 font-weight: 400;
451 line-height: 1.43;
452 color: var(--am-c-main-text);
453 }
454
455 .am-input-number {
456 max-width: 100px;
457 }
458 }
459
460 &-message {
461 font-size: 14px;
462 font-weight: 400;
463 line-height: 1.43;
464 color: var(--am-bringing-color-text-opacity60);
465 }
466
467 &-full-mobile-s {
468 .am-fs__bringing-content {
469 display: flex;
470 flex-direction: column;
471 //gap: 16px;
472 & > * + * {
473 margin: 0 1em;
474 }
475 }
476 }
477 }
478
479 &__ps {
480 &-popup {
481 position: relative;
482
483 &__heading {
484 font-size: 14px;
485 font-weight: 400;
486 line-height: 1.42857;
487 text-align: center;
488 color: var(--am-c-main-text);
489 margin: 20px 0 16px;
490 }
491
492 &__or {
493 display: flex;
494 flex-direction: row;
495 font-weight: 400;
496 font-size: 14px;
497 line-height: 20px;
498 margin: 20px 0;
499 color: var(--am-c-ps-text-op60);
500
501 &:before,
502 &:after {
503 content: '';
504 flex: 1 1;
505 border-bottom: 1px solid var(--am-c-ps-text-op20);
506 margin: auto;
507 }
508
509 &:before {
510 margin-right: 10px;
511 }
512
513 &:after {
514 margin-left: 10px;
515 }
516 }
517
518 &__btn {
519 &.am-button.am-button--medium {
520 --am-h-btn: 56px;
521 --am-fs-btn: 14px;
522 width: 100%;
523 justify-content: space-between;
524 }
525
526 &-mobile {
527 &.am-button.am-button--medium {
528 --am-fs-btn: 12px;
529 }
530 }
531 }
532 }
533
534 &-pill {
535 display: inline-block;
536 font-size: 14px;
537 font-weight: 500;
538 line-height: 1;
539 color: var(--am-c-btn-prim);
540 background-color: var(--am-c-btn-prim-text);
541 border-radius: 12px;
542 padding: 5px 8px;
543 }
544 }
545 }
546 }
547
548 // Public
549 .amelia-v2-booking #amelia-container {
550 @include bringing-anyone-block;
551 }
552
553 // Admin
554 #amelia-app-backend-new {
555 @include bringing-anyone-block;
556 }
557 </style>
558