PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 2.4.6
Booking for Appointments and Events Calendar – Amelia v2.4.6
2.4.9 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 / Cabinet / common / Appointments / Appointments.vue
ameliabooking / v3 / src / views / public / Cabinet / common / Appointments Last commit date
parts 1 month ago AppointmentStatus.vue 1 month ago Appointments.vue 1 month ago
Appointments.vue
555 lines
1 <template>
2 <div
3 ref="pageContainer"
4 class="am-cap am-capa-main"
5 :class="{ 'am-capai-main': appointmentVisibility }"
6 >
7 <div class="am-capa-main__inner" :class="responsiveClass">
8 <AmAlert
9 v-if="alertVisibility"
10 ref="alertContainer"
11 :type="alertType"
12 :show-border="true"
13 :close-after="5000"
14 custom-class="am-cap__alert"
15 @close="closeAlert"
16 @trigger-close="closeAlert"
17 >
18 <template #title>
19 <span v-if="alertType === 'success'" class="am-icon-checkmark-circle-full"></span>
20 <span v-if="alertType === 'error'" class="am-icon-clearable"></span>
21 {{ alertMessage }}
22 </template>
23 </AmAlert>
24
25 <CabinetFilters
26 v-if="!appointmentVisibility && ready"
27 :step-key="'appointments'"
28 :responsive-class="responsiveClass"
29 :empty="false"
30 @add-appointment="addAppointment"
31 @change-filters="getAppointments"
32 />
33
34 <div
35 v-if="
36 shortcodeData.cabinetType === 'employee' &&
37 amSettings.roles.allowWriteAppointments &&
38 !appointmentVisibility &&
39 ready
40 "
41 class="am-cap__actions"
42 >
43 <AmButton
44 prefix="plus"
45 size="small"
46 category="primary"
47 :type="amCustomize.appointments.options.newAppBtn.buttonType"
48 @click="addAppointment"
49 >
50 <span>{{ amLabels.new_appointment }}</span>
51 </AmButton>
52 </div>
53
54 <template v-if="!loading && ready">
55 <AppointmentsList
56 v-if="
57 !appointmentVisibility &&
58 dateGroupedAppointments &&
59 Object.keys(dateGroupedAppointments).length > 0
60 "
61 :grouped-appointments="dateGroupedAppointments"
62 :page-width="pageWidth"
63 :responsive-class="responsiveClass"
64 step-key="appointments"
65 @canceled="getAppointments"
66 @booked="getAppointments"
67 @edit-appointment="editAppointment"
68 @status-change="appointmentStatusChange"
69 ></AppointmentsList>
70 <EmptyState
71 v-else-if="!appointmentVisibility"
72 :heading="amLabels.no_app_found"
73 :text="amLabels.have_no_app"
74 ></EmptyState>
75 <AmPagination
76 v-if="
77 !appointmentVisibility &&
78 dateGroupedAppointments &&
79 Object.keys(dateGroupedAppointments).length > 0 &&
80 appointmentsCount > amSettings.general.itemsPerPage
81 "
82 :page-size="amSettings.general.itemsPerPage"
83 :pager-count="5"
84 layout="prev, pager, next"
85 :total="appointmentsCount"
86 :current-page="appointmentsPage"
87 @current-change="appointmentsPageChange"
88 />
89 <Appointment
90 v-if="appointmentVisibility"
91 :page-width="pageWidth"
92 :responsive-class="responsiveClass"
93 :linked-appointments="linkedAppointments"
94 @edit-linked-appointment="editAppointment"
95 @close="closeAppointment"
96 @save="saveAppointmentCallback"
97 ></Appointment>
98 </template>
99 <Skeleton v-else></Skeleton>
100 </div>
101 </div>
102 </template>
103
104 <script setup>
105 // * import from Vue
106 import { ref, computed, inject, provide, onMounted, watch, reactive } from 'vue'
107
108 // * Import from Vuex
109 import { useStore } from 'vuex'
110
111 // * Import from Libraries
112 import httpClient from '../../../../../plugins/axios'
113 import moment from 'moment'
114
115 // * Components Width and Height
116 import { useElementSize } from '@vueuse/core'
117
118 // * Components
119 import AmAlert from '../../../../_components/alert/AmAlert.vue'
120 import AmButton from '../../../../_components/button/AmButton.vue'
121 import AmPagination from '../../../../_components/pagination/AmPagination.vue'
122
123 // * Parts
124 import CabinetFilters from '../parts/Filters.vue'
125 import AppointmentsList from './parts/AppointmentsList.vue'
126 import Appointment from './parts/Appointment.vue'
127 import Skeleton from '../../common/parts/Skeleton.vue'
128 import EmptyState from '../parts/EmptyState.vue'
129
130 // * Composables
131 import { getDateRange } from '../../../../../assets/js/common/date'
132 import { useParsedAppointments } from '../../../../../assets/js/admin/appointment'
133 import { useUrlParams } from '../../../../../assets/js/common/helper'
134 import { useResponsiveClass } from '../../../../../assets/js/common/responsive'
135 import { useScrollTo } from '../../../../../assets/js/common/scrollElements'
136
137 // * Store
138 let store = useStore()
139
140 // * Root Settings
141 const amSettings = inject('settings')
142
143 // * Customized form data
144 let amCustomize = inject('amCustomize')
145
146 // * Data in shortcode
147 const shortcodeData = inject('shortcodeData')
148
149 // * labels
150 const labels = 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({ ...labels })
161
162 let customizedLabels = amCustomize.value.appointments.translations
163 if (customizedLabels) {
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 // * Page Content width
178 const pageContainer = ref(null)
179 const { width: pageWidth } = useElementSize(pageContainer)
180
181 let responsiveClass = computed(() => {
182 return useResponsiveClass(pageWidth.value)
183 })
184
185 provide('pageWidth', pageWidth)
186
187 // * Alert block
188 let alertContainer = ref(null)
189 let alertVisibility = ref(false)
190 let alertType = ref('success')
191
192 let alertMessage = ref('')
193
194 function closeAlert() {
195 alertVisibility.value = false
196 store.commit('cabinet/setPaymentLinkError', {
197 value: false,
198 type: 'appointment',
199 })
200 }
201
202 /********
203 * Form *
204 ********/
205 let props = defineProps({
206 loadBookingsCounter: {
207 type: Number,
208 default: 0,
209 },
210 appointments: {
211 type: Object,
212 default: null,
213 },
214 })
215
216 // * Cabinet type
217 let cabinetType = inject('cabinetType')
218 store.commit('cabinetFilters/setDates', getDateRange(cabinetType.value))
219
220 // * Loading
221 let loading = computed(() => store.getters['cabinet/getAppointmentsLoading'])
222
223 let ready = computed(() => store.getters['entities/getReady'])
224
225 let dateGroupedAppointments = ref(null)
226
227 let appointmentsCount = ref(0)
228
229 let appointmentsPage = ref(1)
230
231 function appointmentsPageChange(page) {
232 appointmentsPage.value = page
233
234 getAppointments(null, page)
235 }
236
237 function getAppointments(passedData = null, page = 1) {
238 store.commit('cabinet/setAppointmentsLoading', true)
239
240 let params = JSON.parse(JSON.stringify(store.getters['cabinetFilters/getAppointmentsFilters']))
241 let timeZone = store.getters['cabinet/getTimeZone']
242
243 params.dates = params.dates.map((d) => moment(d).format('YYYY-MM-DD'))
244 params.timeZone = timeZone
245 params.source = 'cabinet-' + cabinetType.value
246
247 params.page = page
248
249 store.commit(
250 'auth/setLoadingAppointmentsCounter',
251 store.getters['auth/getLoadingAppointmentsCounter'] + 1,
252 )
253
254 let loadingCounter = store.getters['auth/getLoadingAppointmentsCounter']
255
256 httpClient
257 .get('/appointments', {
258 params: useUrlParams(params),
259 })
260 .then((response) => {
261 if (loadingCounter !== store.getters['auth/getLoadingAppointmentsCounter']) {
262 return
263 }
264
265 appointmentsCount.value = response.data.data.total
266
267 dateGroupedAppointments.value = useParsedAppointments(
268 response.data.data.appointments,
269 timeZone,
270 cabinetType.value === 'provider',
271 )
272 })
273 .catch((error) => {
274 if (
275 error?.response?.data?.data?.reauthorize !== undefined &&
276 error.response.data.data.reauthorize
277 ) {
278 store.dispatch('auth/logout')
279 }
280
281 console.log(error)
282 })
283 .finally(() => {
284 if (loadingCounter !== store.getters['auth/getLoadingAppointmentsCounter']) {
285 return
286 }
287
288 store.commit('cabinet/setAppointmentsLoading', false)
289 if (passedData && 'message' in passedData) {
290 alertVisibility.value = true
291 alertMessage.value = passedData.message
292 alertType.value = 'success'
293
294 if (pageContainer.value && alertContainer.value) {
295 setTimeout(function () {
296 useScrollTo(pageContainer.value, alertContainer.value.$el, 0, 300)
297 }, 500)
298 }
299 }
300 })
301 }
302
303 /***************
304 * Appointment *
305 ***************/
306 let linkedAppointments = ref([])
307
308 function editAppointment(appointment) {
309 store.commit('cabinet/setAppointmentsLoading', true)
310
311 httpClient
312 .get('/appointments/' + appointment.id, {
313 params: {
314 source: 'cabinet-' + cabinetType.value,
315 timeZone: store.getters['cabinet/getTimeZone'],
316 },
317 })
318 .then((response) => {
319 let startDateTime = response.data.data.appointment.bookingStart.split(' ')
320
321 let service = store.getters['entities/getService'](response.data.data.appointment.serviceId)
322
323 let bookings = []
324
325 response.data.data.appointment.bookings.forEach((booking) => {
326 let extras = []
327
328 service.extras
329 .sort((a, b) => a.position - b.position)
330 .forEach((extra) => {
331 extras.push({
332 extraId: extra.id,
333 quantity: 0,
334 })
335
336 let bookingExtra = booking.extras.find((i) => i.extraId === extra.id)
337
338 if (typeof bookingExtra !== 'undefined') {
339 extras[extras.length - 1] = bookingExtra
340 }
341 })
342
343 let customFields = booking.customFields ? JSON.parse(booking.customFields) : {}
344
345 store.getters['entities/getCustomFields'].forEach((customField) => {
346 if (customField.allServices || customField.services.some((e) => e.id === service.id)) {
347 customFields[customField.id] = customFields[customField.id]
348 ? Object.assign({}, customFields[customField.id], {
349 value:
350 customFields[customField.id].type === 'datepicker'
351 ? customFields[customField.id].value
352 ? moment(customFields[customField.id].value).toDate()
353 : ''
354 : customFields[customField.id].value,
355 })
356 : {
357 label: customField.label,
358 type: customField.type,
359 value: customField.type === 'checkbox' ? [] : '',
360 position: customField.position,
361 }
362 }
363 })
364
365 bookings.push({
366 id: booking.id,
367 customer: appointment.bookings.find((b) => b.id === booking.id).customer,
368 persons: booking.persons,
369 status: booking.status,
370 duration: booking.duration ? booking.duration.toString() : null,
371 extras: extras,
372 customFields: customFields,
373 payments: booking.payments,
374 price: booking.price,
375 aggregatedPrice: booking.aggregatedPrice,
376 tax: booking.tax,
377 coupon: booking.coupon,
378 })
379 })
380
381 store.commit('appointment/setAppointment', {
382 id: response.data.data.appointment.id,
383 categoryId: service.categoryId,
384 serviceId: response.data.data.appointment.serviceId,
385 providerId: response.data.data.appointment.providerId,
386 locationId: response.data.data.appointment.locationId,
387 internalNotes: response.data.data.appointment.internalNotes,
388 lessonSpace:
389 response.data.data.appointment.lessonSpace !== null
390 ? response.data.data.appointment.lessonSpace.split(
391 'https://www.thelessonspace.com/space/',
392 )[1]
393 : null,
394 startDate: moment(startDateTime[0]).toDate(),
395 startTime: startDateTime[1].substring(0, 5),
396 bookings: bookings,
397 notifyParticipants: !!response.data.data.appointment.notifyParticipants,
398 createPaymentLinks: !!response.data.data.appointment.createPaymentLinks,
399 })
400
401 linkedAppointments.value = response.data.data.recurring
402
403 appointmentVisibility.value = true
404 })
405 .catch((error) => {
406 console.log(error)
407 })
408 .finally(() => {
409 store.commit('cabinet/setAppointmentsLoading', false)
410 })
411 }
412
413 let appointmentVisibility = ref(false)
414
415 function saveAppointmentCallback() {
416 alertVisibility.value = true
417 alertMessage.value = amLabels.value.appointment_saved
418 alertType.value = 'success'
419
420 if (pageContainer.value && alertContainer.value) {
421 setTimeout(function () {
422 useScrollTo(pageContainer.value, alertContainer.value.$el, 0, 300)
423 }, 500)
424 }
425
426 getAppointments()
427
428 closeAppointment()
429 }
430
431 function appointmentStatusChange(message, status) {
432 alertMessage.value = message
433 alertType.value = status
434
435 alertVisibility.value = true
436 }
437
438 function addAppointment() {
439 resetAppointment()
440
441 appointmentVisibility.value = true
442 }
443
444 function closeAppointment() {
445 appointmentVisibility.value = false
446
447 resetAppointment()
448 }
449
450 function resetAppointment() {
451 store.commit('appointment/resetAppointment', {
452 providerId: store.getters['auth/getProfile'].id,
453 })
454 store.commit('customerInfo/setCustomers', [])
455 store.commit('customerInfo/setCustomersIds', [])
456 }
457
458 watch(
459 () => props.loadBookingsCounter,
460 () => {
461 getAppointments()
462 },
463 )
464
465 onMounted(() => {
466 getAppointments()
467 })
468 </script>
469
470 <script>
471 export default {
472 name: 'CabinetAppointments',
473 key: 'appointments',
474 }
475 </script>
476
477 <style lang="scss">
478 @mixin am-cabinet-appointments {
479 .am-cap {
480 &__actions {
481 width: 100%;
482 display: flex;
483 justify-content: flex-end;
484 margin: 0 0 16px;
485
486 .am-button {
487 .am-icon-plus {
488 font-size: 24px;
489 }
490 }
491 }
492 &__alert {
493 margin: 0 0 16px;
494 .el-alert {
495 padding: 4px 12px 4px 0;
496 box-sizing: border-box;
497
498 &__content {
499 .el-alert__closebtn {
500 top: 50%;
501 transform: translateY(-50%);
502 }
503 }
504
505 &__title {
506 display: flex;
507 align-items: center;
508 font-size: 16px;
509 line-height: 1.5;
510
511 .am-icon-checkmark-circle-full {
512 font-size: 28px;
513 line-height: 1;
514 color: var(--am-c-alerts-bgr);
515 }
516
517 .am-icon-clearable {
518 font-size: 28px;
519 line-height: 1;
520 color: var(--am-c-alerte-bgr);
521 }
522 }
523 }
524 }
525 }
526
527 .am-fs__main-content.am-cap {
528 // am - amelia
529 // capai - cabinet panel appointments item
530 &.am-capai-main {
531 height: calc(100% - 148px);
532 }
533
534 &.am-capa-main {
535 padding: 0;
536
537 .am-capa-main__inner {
538 display: block;
539 padding: 16px 32px;
540
541 &.am-rw- {
542 &480 {
543 padding: 16px;
544 }
545 }
546 }
547 }
548 }
549 }
550
551 .amelia-v2-booking #amelia-container {
552 @include am-cabinet-appointments;
553 }
554 </style>
555