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 / assets / js / public / slots.js
ameliabooking / v3 / src / assets / js / public Last commit date
actions.js 1 month ago booking.js 2 days ago cabinet.js 1 month ago cart.js 2 days ago catalog.js 2 days ago coupon.js 1 month ago customFields.js 1 month ago customFontFace.js 1 month ago eventFormCssVars.js 1 month ago events.js 1 month ago facebookPixel.js 1 month ago ivy.js 1 month ago package.js 2 days ago panel.js 1 month ago public.js 1 month ago renderActions.js 1 month ago restore.js 2 days ago slots.js 2 days ago stripePaymentIntent.js 2 days ago translation.js 1 month ago user.js 1 month ago
slots.js
440 lines
1 import moment from 'moment'
2 import httpClient from '../../../plugins/axios'
3 import { useSortedDateStrings, useUrlParams } from '../common/helper'
4 import { settings } from '../../../plugins/settings'
5 import { useAvailableSlots, useDuration } from '../common/appointments'
6 import { useCartItem } from './cart'
7
8 function useLocalFromUtcSlots(slots) {
9 let formattedSlots = {}
10
11 for (let date in slots) {
12 for (let time in slots[date]) {
13 let dateTime = moment
14 .utc(date + ' ' + time, 'YYYY-MM-DD HH:mm')
15 .local()
16 .format('YYYY-MM-DD HH:mm')
17 .split(' ')
18
19 if (!(dateTime[0] in formattedSlots)) {
20 formattedSlots[dateTime[0]] = {}
21 }
22
23 formattedSlots[dateTime[0]][dateTime[1]] = slots[date][time]
24 }
25 }
26
27 return formattedSlots
28 }
29
30 function useAppointmentParams(store) {
31 let employeeId = store.getters['booking/getEmployeeId']
32
33 return {
34 queryTimeZone: settings.general.showClientTimeZone
35 ? Intl.DateTimeFormat().resolvedOptions().timeZone
36 : null,
37 monthsLoad: 1,
38 locationId: store.getters['booking/getLocationId'],
39 serviceId: store.getters['booking/getServiceId'],
40 serviceDuration: store.getters['booking/getBookingDuration'],
41 providerIds: !employeeId
42 ? store.getters['entities/filteredEmployees'](store.getters['booking/getSelection']).map(
43 (item) => item.id,
44 )
45 : [employeeId],
46 extras: JSON.stringify(
47 store.getters['entities/getService'](store.getters['booking/getServiceId'])
48 .extras.map((extra) =>
49 extra.quantity
50 ? {
51 id: extra.id,
52 quantity: extra.quantity,
53 }
54 : null,
55 )
56 .filter((extra) => extra !== null),
57 ),
58 group: 1,
59 page: 'booking',
60 structured: true,
61 persons: store.getters['booking/getBookingPersons'],
62 }
63 }
64
65 function useAppointmentSlots(
66 params,
67 fetchedSlots,
68 callback,
69 customCallback,
70 waitingListOptions = null,
71 getActiveServiceId = null,
72 requestedCartItemIndex = null,
73 ) {
74 const requestedServiceId = params.serviceId
75
76 httpClient.get('/slots', { params: useUrlParams(params) }).then((response) => {
77 const activeServiceId = getActiveServiceId ? getActiveServiceId() : null
78
79 // Ignore only when the active service is known and differs (stale response).
80 // null/undefined active ids happen for packages and must not discard slots.
81 if (activeServiceId != null && activeServiceId !== requestedServiceId) {
82 return
83 }
84
85 let resultSlots =
86 'queryTimeZone' in params && params.queryTimeZone
87 ? useLocalFromUtcSlots(response.data.data.slots)
88 : response.data.data.slots
89
90 let slots = fetchedSlots !== null ? fetchedSlots : resultSlots
91
92 if (fetchedSlots !== null) {
93 Object.keys(resultSlots).forEach((date) => {
94 slots[date] = resultSlots[date]
95 })
96 }
97
98 let occupied =
99 'queryTimeZone' in params && params.queryTimeZone
100 ? useLocalFromUtcSlots(response.data.data.occupied)
101 : response.data.data.occupied
102
103 // Waiting list slots provided by backend (preferred)
104 let waitingListSlots = {}
105
106 // Fallback: derive on frontend if backend doesn't yet supply and waiting list enabled
107 if (!Object.keys(waitingListSlots).length) {
108 const wlEnabled = !!(waitingListOptions && waitingListOptions.enabled)
109 if (wlEnabled) {
110 Object.keys(occupied || {}).forEach((date) => {
111 Object.keys(occupied[date] || {}).forEach((time) => {
112 if (!(date in slots) || !(time in slots[date])) {
113 const providersData = occupied[date][time] || []
114 const serviceId = params.serviceId
115 const duration = params.serviceDuration
116 // Consider only rows for this service; require at least one match
117 let serviceRows = providersData.filter((p) => p.s === serviceId)
118 if (
119 'locationId' in params &&
120 params.locationId !== null &&
121 params.locationId !== ''
122 ) {
123 serviceRows = serviceRows.filter(
124 (p) => p.l != null && p.l !== '' && String(p.l) === String(params.locationId),
125 )
126 }
127 if (!serviceRows.length) return
128 if (duration) {
129 const expectedDuration = duration / 60
130 serviceRows = serviceRows.filter((p) => Number(p.d) === Number(expectedDuration))
131 if (!serviceRows.length) return
132 }
133 // Filter for selected providers only
134 const selectedProviderRows =
135 params.providerIds && params.providerIds.length
136 ? serviceRows.filter((p) => params.providerIds.includes(p.e))
137 : serviceRows
138
139 // Check if ALL selected providers are fully booked (no capacity available)
140 const allFullyBooked =
141 selectedProviderRows.length > 0 && selectedProviderRows.every((p) => p.c <= 0)
142
143 if (!allFullyBooked) return
144
145 // Filter providers that have waiting list capacity
146 const waitingListProviders = selectedProviderRows.filter((p) => {
147 return (
148 !waitingListOptions.maxCapacity || (p.w || 0) < waitingListOptions.maxCapacity
149 )
150 })
151
152 // Add to waiting list only if all providers are fully booked and at least one has waiting capacity
153 if (waitingListProviders.length > 0) {
154 // If this time exists in regular slots, don't add waiting list (user can book the regular slot)
155 let regularSlotExists = slots[date] && slots[date][time]
156
157 if (!regularSlotExists) {
158 if (!(date in waitingListSlots)) waitingListSlots[date] = {}
159 waitingListSlots[date][time] = waitingListProviders
160 }
161 }
162 }
163 })
164 })
165 }
166 }
167
168 callback(
169 slots,
170 occupied,
171 response.data.data.minimum,
172 response.data.data.maximum,
173 response.data.data.busyness,
174 response.data.data.appCount,
175 { providerId: response.data.data.lastProvider, fromBackend: true },
176 customCallback,
177 waitingListSlots,
178 requestedServiceId,
179 requestedCartItemIndex,
180 )
181 })
182 }
183
184 function useRange(store) {
185 return store.getters['booking/getMultipleAppointmentsRange']
186 }
187
188 function useSelectedDuration(store, value) {
189 let cartItem = useCartItem(store)
190
191 store.commit('booking/setDuration', value)
192
193 let service = store.getters['entities/getService'](cartItem.serviceId)
194
195 let extrasIds = store.getters['booking/getSelectedExtras'].map((i) => i.extraId)
196
197 return useDuration(
198 value,
199 service.extras.filter((i) => extrasIds.includes(i.id)),
200 )
201 }
202
203 function useSelectedDate(store, date, range) {
204 store.commit('booking/setMultipleAppointmentsDate', date)
205
206 store.commit('booking/setMultipleAppointmentsRange', range)
207
208 return useAvailableSlots(store)
209 }
210
211 function useSelectedTime(store, time) {
212 store.commit('booking/setMultipleAppointmentsTime', time)
213 }
214
215 function useDeselectedDate(store) {
216 let cartItem = useCartItem(store)
217
218 store.commit('booking/unsetMultipleAppointmentsData', cartItem.index)
219 }
220
221 function useSlotsCallback(
222 store,
223 slots,
224 occupied,
225 minimumDateTime,
226 maximumDateTime,
227 busyness,
228 appCount,
229 lastBookedProviderId,
230 searchStart,
231 searchEnd,
232 requestedServiceId,
233 requestedCartItemIndex,
234 ) {
235 store.commit('booking/setMultipleAppointmentsSlots', slots)
236 store.commit('booking/setMultipleAppointmentsOccupied', occupied)
237 store.commit('booking/setMultipleAppointmentsLastDate', maximumDateTime)
238 store.commit('booking/setBusyness', {
239 serviceId: requestedServiceId,
240 busyness,
241 cartItemIndex: requestedCartItemIndex,
242 })
243 store.commit('booking/setLastBookedProviderId', lastBookedProviderId)
244 store.commit('booking/setMultipleAppointmentsAppCount', appCount)
245
246 let result = {}
247
248 let cartItem = useCartItem(store)
249
250 let activeService = cartItem.services[cartItem.serviceId]
251
252 if (cartItem.index !== '' && activeService.list.length) {
253 let dates = useSortedDateStrings(Object.keys(slots))
254
255 result['calendarStartDate'] = activeService.list[cartItem.index].date
256 ? activeService.list[cartItem.index].date
257 : dates.length
258 ? dates[0]
259 : null
260
261 result['calendarEventDate'] = activeService.list[cartItem.index].date
262 ? activeService.list[cartItem.index].date
263 : ''
264
265 if (!(activeService.list[cartItem.index].date in slots)) {
266 store.commit('booking/setMultipleAppointmentsDate', null)
267 store.commit('booking/setMultipleAppointmentsTime', null)
268
269 result['calendarEventSlot'] = ''
270
271 result['calendarEventSlots'] = []
272 } else if (
273 activeService.list.length &&
274 !(activeService.list[cartItem.index].time in slots[activeService.list[cartItem.index].date])
275 ) {
276 store.commit('booking/setMultipleAppointmentsTime', null)
277
278 result['calendarEventSlot'] = ''
279 }
280
281 if (
282 activeService.list.length &&
283 activeService.list[cartItem.index].date &&
284 (searchStart.value
285 ? moment(activeService.list[cartItem.index].date).isSameOrAfter(searchStart.value)
286 : true) &&
287 (searchEnd.value
288 ? moment(activeService.list[cartItem.index].date).isSameOrBefore(searchEnd.value)
289 : true)
290 ) {
291 if (activeService.list[cartItem.index].date in activeService.slots) {
292 let availableSlots = useAvailableSlots(store)
293
294 result['calendarEventSlots'] = availableSlots.length
295 ? availableSlots
296 : Object.keys(activeService.slots[activeService.list[cartItem.index].date])
297
298 if (activeService.list[cartItem.index].time) {
299 result['calendarEventSlot'] = activeService.list[cartItem.index].time
300 }
301 }
302 }
303 }
304
305 return result
306 }
307
308 function useSlotsPricing(store, slots, serviceId) {
309 let employeesPrices = {}
310
311 store.getters['entities/getEmployees'].forEach((item) => {
312 let employeeService = item.serviceList.find((i) => i.id === serviceId)
313
314 if (typeof employeeService !== 'undefined') {
315 employeesPrices[item.id] = item.serviceList.find((i) => i.id === serviceId).price
316 }
317 })
318
319 let result = {}
320
321 let minPrice = null
322
323 let midPrice = null
324
325 let maxPrice = null
326
327 let multipleMin = false
328
329 let multipleMid = false
330
331 let multipleMax = false
332
333 Object.keys(slots).forEach((date) => {
334 result[date] = { slots: {} }
335
336 Object.keys(slots[date]).forEach((time) => {
337 let haveHigh = false
338
339 let haveLow = false
340
341 let haveMid = false
342
343 let minDatePrice = null
344
345 let maxDatePrice = null
346
347 slots[date][time].forEach((i) => {
348 let price = i.p === null ? employeesPrices[i.e] : i.p
349
350 if (minDatePrice === null || price < minDatePrice) {
351 minDatePrice = price
352 }
353
354 if (maxDatePrice === null || price > maxDatePrice) {
355 maxDatePrice = price
356 }
357
358 if (price === employeesPrices[i.e]) {
359 if (midPrice !== null && price !== midPrice) {
360 multipleMid = true
361 }
362
363 if (midPrice === null || price < midPrice) {
364 midPrice = price
365 }
366
367 haveMid = true
368 } else if (price < employeesPrices[i.e]) {
369 if (minPrice !== null && price !== minPrice) {
370 multipleMin = true
371 }
372
373 if (minPrice === null || minDatePrice < minPrice) {
374 minPrice = minDatePrice
375 }
376
377 haveLow = true
378 } else if (price > employeesPrices[i.e]) {
379 if (maxPrice !== null && price !== maxPrice) {
380 multipleMax = true
381 }
382
383 if (maxPrice === null || maxDatePrice < maxPrice) {
384 maxPrice = maxDatePrice
385 }
386
387 haveHigh = true
388 }
389 })
390
391 result[date].slots[time] = {
392 type:
393 haveLow && !haveHigh && !haveMid
394 ? 'low'
395 : haveHigh && !haveLow && !haveMid
396 ? 'high'
397 : 'mid',
398 price: minDatePrice,
399 }
400 })
401
402 result[date].price =
403 Object.values(result[date].slots).filter((i) => i.price === null).length === 0
404
405 let types = Object.values(result[date].slots).map((i) => i.type)
406
407 if (types.filter((i) => i === 'low').length === types.length) {
408 result[date].type = 'low'
409 } else if (types.filter((i) => i === 'high').length === types.length) {
410 result[date].type = 'high'
411 } else {
412 result[date].type = 'mid'
413 }
414 })
415
416 return {
417 price: {
418 low: minPrice,
419 mid: midPrice,
420 high: maxPrice,
421 uniqueMin: !multipleMin,
422 uniqueMid: !multipleMid,
423 uniqueMax: !multipleMax,
424 },
425 dates: result,
426 }
427 }
428
429 export {
430 useRange,
431 useSelectedDuration,
432 useSelectedDate,
433 useSelectedTime,
434 useDeselectedDate,
435 useSlotsCallback,
436 useAppointmentSlots,
437 useAppointmentParams,
438 useSlotsPricing,
439 }
440