PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 2.4.4
Booking for Appointments and Events Calendar – Amelia v2.4.4
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 5 days ago booking.js 5 days ago cabinet.js 5 days ago cart.js 5 days ago catalog.js 5 days ago coupon.js 5 days ago customFields.js 5 days ago events.js 5 days ago facebookPixel.js 5 days ago ivy.js 5 days ago package.js 5 days ago panel.js 5 days ago public.js 5 days ago renderActions.js 5 days ago restore.js 5 days ago slots.js 5 days ago translation.js 5 days ago user.js 5 days ago
slots.js
420 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 ) {
72 httpClient.get('/slots', { params: useUrlParams(params) }).then((response) => {
73 let resultSlots =
74 'queryTimeZone' in params && params.queryTimeZone
75 ? useLocalFromUtcSlots(response.data.data.slots)
76 : response.data.data.slots
77
78 let slots = fetchedSlots !== null ? fetchedSlots : resultSlots
79
80 if (fetchedSlots !== null) {
81 Object.keys(resultSlots).forEach((date) => {
82 slots[date] = resultSlots[date]
83 })
84 }
85
86 let occupied =
87 'queryTimeZone' in params && params.queryTimeZone
88 ? useLocalFromUtcSlots(response.data.data.occupied)
89 : response.data.data.occupied
90
91 // Waiting list slots provided by backend (preferred)
92 let waitingListSlots = {}
93
94 // Fallback: derive on frontend if backend doesn't yet supply and waiting list enabled
95 if (!Object.keys(waitingListSlots).length) {
96 const wlEnabled = !!(waitingListOptions && waitingListOptions.enabled)
97 if (wlEnabled) {
98 Object.keys(occupied || {}).forEach((date) => {
99 Object.keys(occupied[date] || {}).forEach((time) => {
100 if (!(date in slots) || !(time in slots[date])) {
101 const providersData = occupied[date][time] || []
102 const serviceId = params.serviceId
103 const duration = params.serviceDuration
104 // Consider only rows for this service; require at least one match
105 let serviceRows = providersData.filter((p) => p.s === serviceId)
106 if (
107 'locationId' in params &&
108 params.locationId !== null &&
109 params.locationId !== ''
110 ) {
111 serviceRows = serviceRows.filter(
112 (p) => p.l != null && p.l !== '' && String(p.l) === String(params.locationId),
113 )
114 }
115 if (!serviceRows.length) return
116 if (duration) {
117 const expectedDuration = duration / 60
118 serviceRows = serviceRows.filter((p) => Number(p.d) === Number(expectedDuration))
119 if (!serviceRows.length) return
120 }
121 // Filter for selected providers only
122 const selectedProviderRows =
123 params.providerIds && params.providerIds.length
124 ? serviceRows.filter((p) => params.providerIds.includes(p.e))
125 : serviceRows
126
127 // Check if ALL selected providers are fully booked (no capacity available)
128 const allFullyBooked =
129 selectedProviderRows.length > 0 && selectedProviderRows.every((p) => p.c <= 0)
130
131 if (!allFullyBooked) return
132
133 // Filter providers that have waiting list capacity
134 const waitingListProviders = selectedProviderRows.filter((p) => {
135 return (
136 !waitingListOptions.maxCapacity || (p.w || 0) < waitingListOptions.maxCapacity
137 )
138 })
139
140 // Add to waiting list only if all providers are fully booked and at least one has waiting capacity
141 if (waitingListProviders.length > 0) {
142 // If this time exists in regular slots, don't add waiting list (user can book the regular slot)
143 let regularSlotExists = slots[date] && slots[date][time]
144
145 if (!regularSlotExists) {
146 if (!(date in waitingListSlots)) waitingListSlots[date] = {}
147 waitingListSlots[date][time] = waitingListProviders
148 }
149 }
150 }
151 })
152 })
153 }
154 }
155
156 callback(
157 slots,
158 occupied,
159 response.data.data.minimum,
160 response.data.data.maximum,
161 response.data.data.busyness,
162 response.data.data.appCount,
163 { providerId: response.data.data.lastProvider, fromBackend: true },
164 customCallback,
165 waitingListSlots,
166 )
167 })
168 }
169
170 function useRange(store) {
171 return store.getters['booking/getMultipleAppointmentsRange']
172 }
173
174 function useSelectedDuration(store, value) {
175 let cartItem = useCartItem(store)
176
177 store.commit('booking/setDuration', value)
178
179 let service = store.getters['entities/getService'](cartItem.serviceId)
180
181 let extrasIds = store.getters['booking/getSelectedExtras'].map((i) => i.extraId)
182
183 return useDuration(
184 value,
185 service.extras.filter((i) => extrasIds.includes(i.id)),
186 )
187 }
188
189 function useSelectedDate(store, date, range) {
190 store.commit('booking/setMultipleAppointmentsDate', date)
191
192 store.commit('booking/setMultipleAppointmentsRange', range)
193
194 return useAvailableSlots(store)
195 }
196
197 function useSelectedTime(store, time) {
198 store.commit('booking/setMultipleAppointmentsTime', time)
199 }
200
201 function useDeselectedDate(store) {
202 let cartItem = useCartItem(store)
203
204 store.commit('booking/unsetMultipleAppointmentsData', cartItem.index)
205 }
206
207 function useSlotsCallback(
208 store,
209 slots,
210 occupied,
211 minimumDateTime,
212 maximumDateTime,
213 busyness,
214 appCount,
215 lastBookedProviderId,
216 searchStart,
217 searchEnd,
218 ) {
219 store.commit('booking/setMultipleAppointmentsSlots', slots)
220 store.commit('booking/setMultipleAppointmentsOccupied', occupied)
221 store.commit('booking/setMultipleAppointmentsLastDate', maximumDateTime)
222 store.commit('booking/setBusyness', busyness)
223 store.commit('booking/setLastBookedProviderId', lastBookedProviderId)
224 store.commit('booking/setMultipleAppointmentsAppCount', appCount)
225
226 let result = {}
227
228 let cartItem = useCartItem(store)
229
230 let activeService = cartItem.services[cartItem.serviceId]
231
232 if (cartItem.index !== '' && activeService.list.length) {
233 let dates = useSortedDateStrings(Object.keys(slots))
234
235 result['calendarStartDate'] = activeService.list[cartItem.index].date
236 ? activeService.list[cartItem.index].date
237 : dates.length
238 ? dates[0]
239 : null
240
241 result['calendarEventDate'] = activeService.list[cartItem.index].date
242 ? activeService.list[cartItem.index].date
243 : ''
244
245 if (!(activeService.list[cartItem.index].date in slots)) {
246 store.commit('booking/setMultipleAppointmentsDate', null)
247 store.commit('booking/setMultipleAppointmentsTime', null)
248
249 result['calendarEventSlot'] = ''
250
251 result['calendarEventSlots'] = []
252 } else if (
253 activeService.list.length &&
254 !(activeService.list[cartItem.index].time in slots[activeService.list[cartItem.index].date])
255 ) {
256 store.commit('booking/setMultipleAppointmentsTime', null)
257
258 result['calendarEventSlot'] = ''
259 }
260
261 if (
262 activeService.list.length &&
263 activeService.list[cartItem.index].date &&
264 (searchStart.value
265 ? moment(activeService.list[cartItem.index].date).isSameOrAfter(searchStart.value)
266 : true) &&
267 (searchEnd.value
268 ? moment(activeService.list[cartItem.index].date).isSameOrBefore(searchEnd.value)
269 : true)
270 ) {
271 if (activeService.list[cartItem.index].date in activeService.slots) {
272 let availableSlots = useAvailableSlots(store)
273
274 result['calendarEventSlots'] = availableSlots.length
275 ? availableSlots
276 : Object.keys(activeService.slots[activeService.list[cartItem.index].date])
277
278 if (activeService.list[cartItem.index].time) {
279 result['calendarEventSlot'] = activeService.list[cartItem.index].time
280 }
281 }
282 }
283 }
284
285 return result
286 }
287
288 function useSlotsPricing(store, slots, serviceId) {
289 let employeesPrices = {}
290
291 store.getters['entities/getEmployees'].forEach((item) => {
292 let employeeService = item.serviceList.find((i) => i.id === serviceId)
293
294 if (typeof employeeService !== 'undefined') {
295 employeesPrices[item.id] = item.serviceList.find((i) => i.id === serviceId).price
296 }
297 })
298
299 let result = {}
300
301 let minPrice = null
302
303 let midPrice = null
304
305 let maxPrice = null
306
307 let multipleMin = false
308
309 let multipleMid = false
310
311 let multipleMax = false
312
313 Object.keys(slots).forEach((date) => {
314 result[date] = { slots: {} }
315
316 Object.keys(slots[date]).forEach((time) => {
317 let haveHigh = false
318
319 let haveLow = false
320
321 let haveMid = false
322
323 let minDatePrice = null
324
325 let maxDatePrice = null
326
327 slots[date][time].forEach((i) => {
328 let price = i.p === null ? employeesPrices[i.e] : i.p
329
330 if (minDatePrice === null || price < minDatePrice) {
331 minDatePrice = price
332 }
333
334 if (maxDatePrice === null || price > maxDatePrice) {
335 maxDatePrice = price
336 }
337
338 if (price === employeesPrices[i.e]) {
339 if (midPrice !== null && price !== midPrice) {
340 multipleMid = true
341 }
342
343 if (midPrice === null || price < midPrice) {
344 midPrice = price
345 }
346
347 haveMid = true
348 } else if (price < employeesPrices[i.e]) {
349 if (minPrice !== null && price !== minPrice) {
350 multipleMin = true
351 }
352
353 if (minPrice === null || minDatePrice < minPrice) {
354 minPrice = minDatePrice
355 }
356
357 haveLow = true
358 } else if (price > employeesPrices[i.e]) {
359 if (maxPrice !== null && price !== maxPrice) {
360 multipleMax = true
361 }
362
363 if (maxPrice === null || maxDatePrice < maxPrice) {
364 maxPrice = maxDatePrice
365 }
366
367 haveHigh = true
368 }
369 })
370
371 result[date].slots[time] = {
372 type:
373 haveLow && !haveHigh && !haveMid
374 ? 'low'
375 : haveHigh && !haveLow && !haveMid
376 ? 'high'
377 : 'mid',
378 price: minDatePrice,
379 }
380 })
381
382 result[date].price =
383 Object.values(result[date].slots).filter((i) => i.price === null).length === 0
384
385 let types = Object.values(result[date].slots).map((i) => i.type)
386
387 if (types.filter((i) => i === 'low').length === types.length) {
388 result[date].type = 'low'
389 } else if (types.filter((i) => i === 'high').length === types.length) {
390 result[date].type = 'high'
391 } else {
392 result[date].type = 'mid'
393 }
394 })
395
396 return {
397 price: {
398 low: minPrice,
399 mid: midPrice,
400 high: maxPrice,
401 uniqueMin: !multipleMin,
402 uniqueMid: !multipleMid,
403 uniqueMax: !multipleMax,
404 },
405 dates: result,
406 }
407 }
408
409 export {
410 useRange,
411 useSelectedDuration,
412 useSelectedDate,
413 useSelectedTime,
414 useDeselectedDate,
415 useSlotsCallback,
416 useAppointmentSlots,
417 useAppointmentParams,
418 useSlotsPricing,
419 }
420