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 / EventForm / WpaEventsLandingForm / WpaEventsLandingForm.vue
ameliabooking / v3 / src / views / public / EventForm / WpaEventsLandingForm Last commit date
WpaEventsBookButton.vue 1 month ago WpaEventsLandingForm.vue 1 month ago WpaEventsSelectionSidebar.vue 2 weeks ago
WpaEventsLandingForm.vue
495 lines
1 <template>
2 <Teleport v-if="selectionSidebarItems.length > 0" to="#wpa-events-tickets-mount">
3 <WpaEventsSelectionSidebar
4 :event-css-vars="cssVars"
5 :title="selectionSidebarTitle"
6 :subtitle="selectionSidebarSubtitle"
7 :items="selectionSidebarItems"
8 @qty-update="onSelectionQtyUpdate"
9 />
10 </Teleport>
11 <Teleport v-if="wpaFooterTeleportEnabled" to="#wpa-events-footer-mount">
12 <WpaEventsBookButton
13 :event-css-vars="cssVars"
14 :event-id="eventId"
15 :book-label="bookLabel"
16 :book-disabled="bookDisabled"
17 :button-type="bookButtonType"
18 :button-category="bookButtonCategory"
19 @book="onBookClick"
20 />
21 </Teleport>
22 <div id="amelia-container" ref="ameliaContainer" class="am-ecf" :style="cssVars">
23 <EventBookingDialog
24 v-model="popupVisible"
25 form-key="ecf"
26 :ready="ready"
27 :loading="loading"
28 :loading-upcoming="false"
29 :css-vars="cssVars"
30 :reset-event-id-on-close="false"
31 :skip-event-tickets-step="true"
32 />
33 </div>
34 </template>
35
36 <script setup>
37 import {
38 computed,
39 ref,
40 reactive,
41 inject,
42 provide,
43 nextTick,
44 watch,
45 onMounted,
46 onBeforeUnmount,
47 } from 'vue'
48 import { useStore } from 'vuex'
49 import moment from 'moment/moment'
50
51 import EventBookingDialog from '../Common/EventBookingDialog/EventBookingDialog.vue'
52 import WpaEventsBookButton from './WpaEventsBookButton.vue'
53 import WpaEventsSelectionSidebar from './WpaEventsSelectionSidebar.vue'
54 import { defaultCustomizeSettings } from '@/assets/js/common/defaultCustomize'
55 import useAction from '@/assets/js/public/actions'
56 import { applyCustomFontFaceIfSelected } from '@/assets/js/public/customFontFace'
57 import { eventFormCssVars } from '@/assets/js/public/eventFormCssVars'
58 import { useRenderAction } from '@/assets/js/public/renderActions'
59
60 const store = useStore()
61 const amSettings = inject('settings')
62 const shortcodeData = inject('shortcodeData')
63
64 const wpaTicketsList = computed(() => {
65 const raw = shortcodeData.value?.wpaTickets
66 if (!Array.isArray(raw)) {
67 return []
68 }
69 return raw.filter((t) => t && parseInt(t.id, 10) > 0 && t.name)
70 })
71
72 const ticketStrings = computed(() => shortcodeData.value?.wpaTicketStrings || {})
73
74 const attendeesConfig = computed(() => {
75 const raw = shortcodeData.value?.wpaAttendees
76 if (!raw || typeof raw !== 'object') {
77 return null
78 }
79
80 const min = parseInt(raw.min, 10)
81 const max = parseInt(raw.max, 10)
82 const slotsLeft = parseInt(raw.slotsLeft, 10)
83 if (isNaN(min) || isNaN(max) || max <= min) {
84 return null
85 }
86
87 return {
88 min,
89 max,
90 default: parseInt(raw.default, 10) || min,
91 slotsLeft: isNaN(slotsLeft) ? max : slotsLeft,
92 }
93 })
94
95 const attendeesPersons = ref(1)
96
97 watch(
98 attendeesConfig,
99 (config) => {
100 if (!config) {
101 attendeesPersons.value = 1
102 return
103 }
104 attendeesPersons.value = config.default
105 },
106 { immediate: true },
107 )
108
109 const eventId = computed(() => parseInt(shortcodeData.value?.eventId, 10) || 0)
110
111 const bookLabel = computed(() => ticketStrings.value?.bookEvent || '')
112
113 const qtyById = reactive({})
114
115 function ensureQtyKeys() {
116 wpaTicketsList.value.forEach((t) => {
117 const id = String(t.id)
118 if (!(id in qtyById)) {
119 qtyById[id] = 0
120 }
121 })
122 }
123
124 watch(wpaTicketsList, ensureQtyKeys, { immediate: true })
125
126 function qty(ticketId) {
127 return qtyById[String(ticketId)] ?? 0
128 }
129
130 const selectionSidebarTitle = computed(() => {
131 if (wpaTicketsList.value.length > 0) {
132 return ticketStrings.value.selectTickets || ''
133 }
134 return ticketStrings.value.attendeesTitle || ''
135 })
136
137 const selectionSidebarSubtitle = computed(() => {
138 if (wpaTicketsList.value.length > 0) {
139 return ticketStrings.value.selectTicketsSub || ''
140 }
141 return ticketStrings.value.attendeesSubtitle || ''
142 })
143
144 const selectionSidebarItems = computed(() => {
145 if (wpaTicketsList.value.length > 0) {
146 const sharedSpotsLeft = sharedWaitingListSpotsLeft.value
147
148 return wpaTicketsList.value.map((ticket) => {
149 const left = Math.max(0, ticket.left || 0)
150 const currentQty = qty(ticket.id)
151 const remainingShared =
152 sharedSpotsLeft === null
153 ? null
154 : Math.max(0, sharedSpotsLeft - (selectedTicketsTotal.value - currentQty))
155 const max = remainingShared === null ? left : Math.min(left, remainingShared)
156
157 return {
158 id: ticket.id,
159 name: ticket.name,
160 left: remainingShared === null ? left : Math.min(left, remainingShared + currentQty),
161 leftLabel: ticketStrings.value.ticketsLeft || '',
162 priceFormatted: ticket.priceFormatted || '',
163 min: 0,
164 max,
165 quantity: currentQty,
166 disabled: max <= 0 && currentQty <= 0,
167 ariaLabel: `${ticketStrings.value.selectTickets || ''}${ticket.name}`,
168 kind: 'ticket',
169 }
170 })
171 }
172
173 if (!attendeesConfig.value) {
174 return []
175 }
176
177 return [
178 {
179 id: 'attendees',
180 left: attendeesConfig.value.slotsLeft,
181 leftLabel: ticketStrings.value.slotsLeft || '',
182 min: attendeesConfig.value.min,
183 max: attendeesConfig.value.max,
184 quantity: attendeesPersons.value,
185 disabled: false,
186 ariaLabel: ticketStrings.value.attendeesTitle || '',
187 kind: 'attendees',
188 },
189 ]
190 })
191
192 function onSelectionQtyUpdate(item, val) {
193 let n = typeof val === 'number' ? val : parseInt(String(val), 10)
194 if (isNaN(n)) {
195 n = item.min ?? 0
196 }
197
198 const min = item.min ?? 0
199 const max = item.max ?? 0
200 n = Math.min(max, Math.max(min, n))
201
202 if (item.kind === 'attendees') {
203 attendeesPersons.value = n
204 return
205 }
206
207 qtyById[String(item.id)] = n
208 }
209
210 const selectedPayload = computed(() => {
211 return wpaTicketsList.value
212 .filter((t) => qty(t.id) > 0)
213 .map((t) => ({
214 id: parseInt(t.id, 10),
215 quantity: qty(t.id),
216 available:
217 sharedWaitingListSpotsLeft.value === null
218 ? t.left
219 : Math.min(Math.max(0, t.left || 0), sharedWaitingListSpotsLeft.value),
220 }))
221 })
222
223 const requiresTickets = computed(() => wpaTicketsList.value.length > 0)
224
225 const eventBookable = computed(() => shortcodeData.value?.wpaEventBookable !== false)
226
227 const wpaFooterTeleportEnabled = computed(() => eventId.value > 0 && eventBookable.value)
228
229 const waitingListAvailable = computed(
230 () => shortcodeData.value?.wpaEventWaitingListAvailable === true,
231 )
232
233 const sharedWaitingListSpotsLeft = computed(() => {
234 if (!waitingListAvailable.value || !shortcodeData.value?.wpaMaxCustomCapacity) {
235 return null
236 }
237
238 const spotsLeft = parseInt(shortcodeData.value?.wpaWaitingListSpotsLeft, 10)
239 return isNaN(spotsLeft) ? 0 : Math.max(0, spotsLeft)
240 })
241
242 const selectedTicketsTotal = computed(() => {
243 return wpaTicketsList.value.reduce((sum, ticket) => sum + qty(ticket.id), 0)
244 })
245
246 const bookDisabled = computed(() => {
247 return (
248 !(eventId.value > 0) ||
249 !eventBookable.value ||
250 (requiresTickets.value && selectedPayload.value.length === 0)
251 )
252 })
253
254 const infoCustomizeOptions = computed(() => {
255 const customized = amSettings.customizedData?.ecf?.info?.options
256 const defaults = defaultCustomizeSettings.ecf?.info?.options || {}
257 return {
258 ...defaults,
259 ...(customized || {}),
260 }
261 })
262
263 const bookButtonCategory = computed(() => (waitingListAvailable.value ? 'waiting' : 'primary'))
264
265 const bookButtonType = computed(() => {
266 if (waitingListAvailable.value) {
267 return infoCustomizeOptions.value.waitingBtn?.buttonType || 'filled'
268 }
269
270 return infoCustomizeOptions.value.primBtn?.buttonType || 'filled'
271 })
272
273 const popupVisible = ref(false)
274 provide('popupVisible', popupVisible)
275
276 store.commit('setFormKey', 'ecf')
277 store.commit('shortcodeParams/setForm', 'eventCalendarForm')
278 store.commit('shortcodeParams/setShortcodeParams', shortcodeData.value)
279 store.commit('bookableType/setType', 'event')
280 store.commit('eventEntities/setEventsDisplay', 'calendar')
281
282 let dynamicVh = ref(0)
283 function updateVH() {
284 dynamicVh.value = window.visualViewport?.height ?? window.innerHeight
285 }
286
287 window.addEventListener('resize', updateVH)
288
289 let ameliaContainer = ref(null)
290 let containerWidth = ref(0)
291 provide('containerWidth', containerWidth)
292
293 function resize() {
294 if (ameliaContainer.value) {
295 containerWidth.value = ameliaContainer.value.offsetWidth
296 }
297 }
298
299 window.addEventListener('resize', resize)
300
301 const ready = computed(() => store.getters['getReady'])
302 const loading = computed(() => store.getters['getLoading'])
303 const events = computed(() => store.getters['eventEntities/getEvents'])
304 const selectedEvent = computed(() =>
305 store.getters['eventEntities/getEvent'](store.getters['eventBooking/getSelectedEventId']),
306 )
307
308 function useEvents(hookModifiedEvents) {
309 store.commit('eventEntities/setEvents', hookModifiedEvents)
310 }
311
312 let futureMonthsNumber = ref(1)
313 useRenderAction('upcomingDateRange', { futureMonthsNumber })
314
315 store.commit('params/setDates', [
316 moment().startOf('month').subtract(15, 'days').format('YYYY-MM-DD'),
317 moment().endOf('month').add(15, 'days').format('YYYY-MM-DD'),
318 ])
319 store.commit('pagination/setPage', 0)
320
321 store.dispatch('eventEntities/requestEntities', {
322 types: ['tags', 'employees', 'locations', 'customFields', 'taxes'],
323 loadEntities: shortcodeData.value.hasApiCall || shortcodeData.value.trigger,
324 })
325
326 function clampTicketQty(raw, max) {
327 let q = parseInt(raw, 10)
328 if (isNaN(q) || q < 0) {
329 q = 0
330 }
331 if (q > max) {
332 q = max
333 }
334 return q
335 }
336
337 function normalizePreselectedTickets(payloadTickets) {
338 if (!Array.isArray(payloadTickets)) {
339 return []
340 }
341
342 return payloadTickets
343 .filter((t) => t && typeof t === 'object')
344 .map((ticket) => {
345 const id = parseInt(ticket.id, 10)
346 const available = parseInt(ticket.available, 10)
347 const quantity = clampTicketQty(
348 ticket.quantity,
349 isNaN(available) ? Number.MAX_SAFE_INTEGER : available,
350 )
351
352 return {
353 id,
354 quantity,
355 available: isNaN(available) ? null : available,
356 }
357 })
358 .filter((ticket) => !isNaN(ticket.id) && ticket.quantity > 0)
359 }
360
361 function applyAttendeesPersonsToStore() {
362 if (!attendeesConfig.value) {
363 return
364 }
365
366 store.commit('persons/setMinPersons', attendeesConfig.value.min)
367 store.commit('persons/setMaxPersons', attendeesConfig.value.max)
368 store.commit('persons/setPersons', attendeesPersons.value)
369 }
370
371 function handleWpaOpenBooking(event) {
372 const detail = event && event.detail ? event.detail : {}
373 const resolvedEventId = parseInt(detail.eventId || shortcodeData.value.eventId, 10)
374 if (isNaN(resolvedEventId) || !resolvedEventId) {
375 return
376 }
377
378 const selectedTickets = normalizePreselectedTickets(detail.tickets)
379 store.commit('tickets/setPendingPreselectedTickets', selectedTickets)
380 applyAttendeesPersonsToStore()
381 store.commit('eventBooking/setEventId', resolvedEventId)
382 useAction(store, {}, 'SelectEvent', 'event', null, null)
383 popupVisible.value = true
384 }
385
386 function onOpenBookingFromSidebar(payload) {
387 handleWpaOpenBooking({ detail: payload })
388 }
389
390 function onBookClick() {
391 if (bookDisabled.value) {
392 return
393 }
394 onOpenBookingFromSidebar({
395 eventId: eventId.value,
396 tickets: selectedPayload.value,
397 })
398 }
399
400 watch(
401 [popupVisible, selectedEvent],
402 ([visible, currentEvent]) => {
403 if (!visible || !currentEvent || !currentEvent.id) {
404 return
405 }
406
407 if (attendeesConfig.value && !currentEvent.customPricing) {
408 applyAttendeesPersonsToStore()
409 return
410 }
411
412 if (!currentEvent.customPricing) {
413 return
414 }
415
416 if (!store.getters['tickets/getTicketsData'].length) {
417 store.commit('tickets/setTickets', currentEvent.customTickets || [])
418 }
419
420 if (currentEvent.maxCustomCapacity != null) {
421 store.commit('tickets/setMaxCustomCapacity', currentEvent.maxCustomCapacity)
422 }
423
424 if (currentEvent.maxExtraPeople != null) {
425 store.commit('tickets/setMaxExtraPeople', currentEvent.maxExtraPeople)
426 }
427
428 store.commit('tickets/applyPendingPreselectedTickets', {
429 waitingMode: shortcodeData.value?.wpaEventWaitingListAvailable === true,
430 sharedWaitingSpotsLeft:
431 shortcodeData.value?.wpaMaxCustomCapacity === true
432 ? parseInt(shortcodeData.value?.wpaWaitingListSpotsLeft, 10) || 0
433 : null,
434 })
435 },
436 { immediate: true },
437 )
438
439 onMounted(() => {
440 const wrapper = document.getElementById('amelia-v2-booking-' + shortcodeData.value.counter)
441 if (wrapper) {
442 wrapper.classList.add('amelia-v2-booking-' + shortcodeData.value.counter + '-loaded')
443 }
444
445 watch(ready, (current) => {
446 if (current) {
447 useAction(store, { events, useEvents }, 'ViewContent', 'event', null, null)
448 }
449 })
450
451 nextTick(() => {
452 resize()
453 })
454
455 useAction(store, { containerWidth }, 'ContainerWidth', 'event', null, null)
456 updateVH()
457 document.addEventListener('ameliaWpaOpenBooking', handleWpaOpenBooking)
458 })
459
460 onBeforeUnmount(() => {
461 document.removeEventListener('ameliaWpaOpenBooking', handleWpaOpenBooking)
462 window.removeEventListener('resize', updateVH)
463 window.removeEventListener('resize', resize)
464 })
465
466 const amFonts = ref(
467 amSettings.customizedData ? amSettings.customizedData.fonts : defaultCustomizeSettings.fonts,
468 )
469 provide('amFonts', amFonts)
470
471 applyCustomFontFaceIfSelected(amFonts.value)
472 watch(
473 amFonts,
474 (fonts) => {
475 applyCustomFontFaceIfSelected(fonts)
476 },
477 { deep: true },
478 )
479
480 const amColors = computed(() => {
481 return amSettings.customizedData && 'ecf' in amSettings.customizedData
482 ? amSettings.customizedData.ecf.colors
483 : defaultCustomizeSettings.ecf.colors
484 })
485 provide('amColors', amColors)
486
487 const cssVars = eventFormCssVars(amColors, amFonts, dynamicVh)
488 </script>
489
490 <script>
491 export default {
492 name: 'WpaEventsLandingFormWrapper',
493 }
494 </script>
495