@wix/headless-bookings 0.0.11 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/dist/api/index.d.ts +1 -0
- package/cjs/dist/api/index.js +1 -0
- package/cjs/dist/api/query-services/index.d.ts +6 -7
- package/cjs/dist/api/query-services/index.js +4 -4
- package/cjs/dist/api/time-slot-list/index.d.ts +53 -0
- package/cjs/dist/api/time-slot-list/index.js +133 -0
- package/cjs/dist/react/core/booking/Booking.d.ts +7 -7
- package/cjs/dist/react/core/booking/Booking.js +5 -5
- package/cjs/dist/react/core/location/Location.d.ts +3 -3
- package/cjs/dist/react/core/location/Location.js +2 -2
- package/cjs/dist/react/core/service/Service.d.ts +8 -8
- package/cjs/dist/react/core/service-list/ServiceList.d.ts +2 -2
- package/cjs/dist/react/location/Location.d.ts +3 -3
- package/cjs/dist/react/service/Service.d.ts +9 -9
- package/cjs/dist/react/service/Service.js +5 -5
- package/cjs/dist/react/service-list/ServiceList.d.ts +3 -3
- package/cjs/dist/services/booking/booking.d.ts +25 -16
- package/cjs/dist/services/booking/booking.js +50 -16
- package/cjs/dist/services/index.d.ts +2 -1
- package/cjs/dist/services/index.js +1 -0
- package/cjs/dist/services/service/service.d.ts +5 -5
- package/cjs/dist/services/service/service.js +20 -21
- package/cjs/dist/services/service-list/service-list.d.ts +3 -3
- package/cjs/dist/services/time-slot-list/index.d.ts +6 -0
- package/cjs/dist/services/time-slot-list/index.js +6 -0
- package/cjs/dist/services/time-slot-list/time-slot-list.d.ts +58 -0
- package/cjs/dist/services/time-slot-list/time-slot-list.def.d.ts +84 -0
- package/cjs/dist/services/time-slot-list/time-slot-list.def.js +17 -0
- package/cjs/dist/services/time-slot-list/time-slot-list.js +349 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/query-services/index.d.ts +6 -7
- package/dist/api/query-services/index.js +4 -4
- package/dist/api/time-slot-list/index.d.ts +53 -0
- package/dist/api/time-slot-list/index.js +133 -0
- package/dist/react/core/booking/Booking.d.ts +7 -7
- package/dist/react/core/booking/Booking.js +5 -5
- package/dist/react/core/location/Location.d.ts +3 -3
- package/dist/react/core/location/Location.js +2 -2
- package/dist/react/core/service/Service.d.ts +8 -8
- package/dist/react/core/service-list/ServiceList.d.ts +2 -2
- package/dist/react/location/Location.d.ts +3 -3
- package/dist/react/service/Service.d.ts +9 -9
- package/dist/react/service/Service.js +5 -5
- package/dist/react/service-list/ServiceList.d.ts +3 -3
- package/dist/services/booking/booking.d.ts +25 -16
- package/dist/services/booking/booking.js +50 -16
- package/dist/services/index.d.ts +2 -1
- package/dist/services/index.js +1 -0
- package/dist/services/service/service.d.ts +5 -5
- package/dist/services/service/service.js +20 -21
- package/dist/services/service-list/service-list.d.ts +3 -3
- package/dist/services/time-slot-list/index.d.ts +6 -0
- package/dist/services/time-slot-list/index.js +6 -0
- package/dist/services/time-slot-list/time-slot-list.d.ts +58 -0
- package/dist/services/time-slot-list/time-slot-list.def.d.ts +84 -0
- package/dist/services/time-slot-list/time-slot-list.def.js +17 -0
- package/dist/services/time-slot-list/time-slot-list.js +349 -0
- package/package.json +2 -2
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TimeSlotList Service Implementation
|
|
3
|
+
* Manages time slot fetching, selection, and state
|
|
4
|
+
*/
|
|
5
|
+
import { implementService } from '@wix/services-definitions';
|
|
6
|
+
import { SignalsServiceDefinition } from '@wix/services-definitions/core-services/signals';
|
|
7
|
+
import { TimeSlotListServiceDefinition, ViewMode, } from './time-slot-list.def.js';
|
|
8
|
+
import { fetchAvailability } from '../../api/time-slot-list/index.js';
|
|
9
|
+
import { BookingServiceDefinition } from '../booking/booking.js';
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Helpers
|
|
12
|
+
// ============================================================================
|
|
13
|
+
function getDefaultEndDate() {
|
|
14
|
+
return new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
|
15
|
+
}
|
|
16
|
+
function getClientTimezone() {
|
|
17
|
+
return typeof Intl !== 'undefined'
|
|
18
|
+
? Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
19
|
+
: undefined;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Formats a Date to local ISO string (YYYY-MM-DDThh:mm:ss)
|
|
23
|
+
*/
|
|
24
|
+
function toLocalDateString(date) {
|
|
25
|
+
const year = date.getFullYear();
|
|
26
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
27
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
28
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
29
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
30
|
+
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
31
|
+
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Merges new grouped slots with existing ones
|
|
35
|
+
* Slots with the same start time are merged
|
|
36
|
+
*/
|
|
37
|
+
function mergeGroupedSlots(existing, newSlots) {
|
|
38
|
+
const mergedMap = new Map();
|
|
39
|
+
for (const slot of existing) {
|
|
40
|
+
mergedMap.set(slot.localStartDate, slot);
|
|
41
|
+
}
|
|
42
|
+
for (const newSlot of newSlots) {
|
|
43
|
+
const key = newSlot.localStartDate;
|
|
44
|
+
const existingSlot = mergedMap.get(key);
|
|
45
|
+
if (existingSlot) {
|
|
46
|
+
// Merge staffByLocation and originalSlots
|
|
47
|
+
existingSlot.staffByLocation.push(...newSlot.staffByLocation);
|
|
48
|
+
existingSlot.originalSlots.push(...newSlot.originalSlots);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
mergedMap.set(key, newSlot);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return Array.from(mergedMap.values());
|
|
55
|
+
}
|
|
56
|
+
// ============================================================================
|
|
57
|
+
// Service Implementation
|
|
58
|
+
// ============================================================================
|
|
59
|
+
export const TimeSlotListService = implementService.withConfig()(TimeSlotListServiceDefinition, ({ getService, config }) => {
|
|
60
|
+
// Get dependent services
|
|
61
|
+
const signalsService = getService(SignalsServiceDefinition);
|
|
62
|
+
const bookingService = getService(BookingServiceDefinition);
|
|
63
|
+
// Initialize default values
|
|
64
|
+
const currentDate = new Date();
|
|
65
|
+
// Initialize Signals
|
|
66
|
+
const timeSlotsSignal = signalsService.signal(config.timeSlots || []);
|
|
67
|
+
const selectionSignal = signalsService.signal(null);
|
|
68
|
+
const isLoadingSignal = signalsService.signal(false);
|
|
69
|
+
const errorSignal = signalsService.signal(null);
|
|
70
|
+
const hasMoreSignal = signalsService.signal(false);
|
|
71
|
+
const viewModeSignal = signalsService.signal(config.viewMode || ViewMode.CLASSIC);
|
|
72
|
+
const startDateSignal = signalsService.signal(config.startDate || currentDate);
|
|
73
|
+
const endDateSignal = signalsService.signal(config.endDate || getDefaultEndDate());
|
|
74
|
+
const timezoneSignal = signalsService.signal(config.timezone ?? getClientTimezone());
|
|
75
|
+
const cursorSignal = signalsService.signal(undefined);
|
|
76
|
+
/**
|
|
77
|
+
* Helper: Get services from config or BookingService
|
|
78
|
+
*/
|
|
79
|
+
const getServices = () => {
|
|
80
|
+
if (config.services?.length)
|
|
81
|
+
return config.services;
|
|
82
|
+
return bookingService.bookingSelections.get().map((s) => s.service);
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Fetch time slots from API
|
|
86
|
+
* Called on initialization or when filters change
|
|
87
|
+
*/
|
|
88
|
+
const fetchTimeSlots = async () => {
|
|
89
|
+
const selectedServices = getServices();
|
|
90
|
+
isLoadingSignal.set(true);
|
|
91
|
+
errorSignal.set(null);
|
|
92
|
+
cursorSignal.set(undefined);
|
|
93
|
+
try {
|
|
94
|
+
const response = await fetchAvailability({
|
|
95
|
+
services: selectedServices,
|
|
96
|
+
fromLocalDate: toLocalDateString(startDateSignal.get()),
|
|
97
|
+
toLocalDate: toLocalDateString(endDateSignal.get()),
|
|
98
|
+
timeZone: timezoneSignal.get(),
|
|
99
|
+
viewMode: viewModeSignal.get(),
|
|
100
|
+
});
|
|
101
|
+
timeSlotsSignal.set(response.timeSlots);
|
|
102
|
+
const nextCursor = response.cursorPagingMetadata?.cursors?.next ?? undefined;
|
|
103
|
+
cursorSignal.set(nextCursor);
|
|
104
|
+
hasMoreSignal.set(!!nextCursor);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
const message = error instanceof Error
|
|
108
|
+
? error.message
|
|
109
|
+
: 'Failed to load time slots';
|
|
110
|
+
errorSignal.set(message);
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
isLoadingSignal.set(false);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Action: Select a time slot
|
|
118
|
+
* Only updates booking service if there's exactly one location and one resource
|
|
119
|
+
* Otherwise, user must select location/resource before booking
|
|
120
|
+
*/
|
|
121
|
+
const selectSlot = (slot) => {
|
|
122
|
+
const currentSelection = selectionSignal.get();
|
|
123
|
+
if (currentSelection?.slot?.id === slot.id)
|
|
124
|
+
return;
|
|
125
|
+
const hasOneLocation = slot.staffByLocation.length === 1;
|
|
126
|
+
const hasOneResource = hasOneLocation && slot.staffByLocation[0].staffMembers.length === 1;
|
|
127
|
+
if (hasOneLocation && hasOneResource) {
|
|
128
|
+
const originalSlot = slot.originalSlots[0];
|
|
129
|
+
const { location, staffMembers } = slot.staffByLocation[0];
|
|
130
|
+
const staffMemberId = staffMembers[0].id;
|
|
131
|
+
selectionSignal.set({
|
|
132
|
+
slot,
|
|
133
|
+
locationId: location.id,
|
|
134
|
+
staffMemberId,
|
|
135
|
+
});
|
|
136
|
+
bookingService.actions.setTimeSlot(originalSlot);
|
|
137
|
+
bookingService.actions.setLocation({
|
|
138
|
+
id: location.id,
|
|
139
|
+
name: location.name,
|
|
140
|
+
});
|
|
141
|
+
bookingService.actions.updateBookingSelection({
|
|
142
|
+
staff: {
|
|
143
|
+
_id: staffMemberId,
|
|
144
|
+
name: staffMembers[0].name,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
selectionSignal.set({
|
|
150
|
+
slot,
|
|
151
|
+
locationId: null,
|
|
152
|
+
staffMemberId: null,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* Action: Select a location for the current slot
|
|
158
|
+
* Updates the booking service with the correct original slot for this location
|
|
159
|
+
*/
|
|
160
|
+
const selectLocation = (locationId) => {
|
|
161
|
+
const currentSelections = selectionSignal.get();
|
|
162
|
+
if (!currentSelections?.slot)
|
|
163
|
+
return;
|
|
164
|
+
selectionSignal.set({ ...currentSelections, locationId });
|
|
165
|
+
const location = currentSelections.slot.originalSlots.find((s) => s.location?._id === locationId)?.location;
|
|
166
|
+
if (location) {
|
|
167
|
+
bookingService.actions.setLocation({
|
|
168
|
+
id: location._id,
|
|
169
|
+
name: location.name,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* Action: Select a staff member for the current slot
|
|
175
|
+
* Updates the booking service with the selected resource for the slot's service
|
|
176
|
+
*/
|
|
177
|
+
const selectStaffMember = (staffMemberId) => {
|
|
178
|
+
const currentSelections = selectionSignal.get();
|
|
179
|
+
if (!currentSelections?.slot)
|
|
180
|
+
return;
|
|
181
|
+
selectionSignal.set({ ...currentSelections, staffMemberId });
|
|
182
|
+
const slot = currentSelections.slot;
|
|
183
|
+
// Find the resource info from original slots
|
|
184
|
+
for (const originalSlot of slot.originalSlots) {
|
|
185
|
+
for (const resourceGroup of originalSlot.availableResources || []) {
|
|
186
|
+
const staff = resourceGroup.resources?.find((r) => r._id === staffMemberId);
|
|
187
|
+
if (staff) {
|
|
188
|
+
bookingService.actions.updateBookingSelection({
|
|
189
|
+
staff,
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* Action: Clear preferences (location and staff) while keeping the slot selected
|
|
198
|
+
*/
|
|
199
|
+
const clearPreferences = () => {
|
|
200
|
+
const currentSelection = selectionSignal.get();
|
|
201
|
+
if (!currentSelection?.slot)
|
|
202
|
+
return;
|
|
203
|
+
// Keep the slot, but clear location and staff selections
|
|
204
|
+
selectionSignal.set({
|
|
205
|
+
slot: currentSelection.slot,
|
|
206
|
+
locationId: null,
|
|
207
|
+
staffMemberId: null,
|
|
208
|
+
});
|
|
209
|
+
// Clear location in booking service
|
|
210
|
+
bookingService.actions.clearLocation();
|
|
211
|
+
// Clear resources (staff) in booking service
|
|
212
|
+
const serviceId = currentSelection.slot.serviceId;
|
|
213
|
+
if (serviceId) {
|
|
214
|
+
bookingService.actions.updateBookingSelection({
|
|
215
|
+
staff: undefined,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
/**
|
|
220
|
+
* Action: Load more slots (pagination)
|
|
221
|
+
*/
|
|
222
|
+
const loadMore = async () => {
|
|
223
|
+
const cursor = cursorSignal.get();
|
|
224
|
+
if (!cursor || isLoadingSignal.get())
|
|
225
|
+
return;
|
|
226
|
+
const selectedServices = getServices();
|
|
227
|
+
if (selectedServices.length === 0)
|
|
228
|
+
return;
|
|
229
|
+
isLoadingSignal.set(true);
|
|
230
|
+
try {
|
|
231
|
+
const response = await fetchAvailability({
|
|
232
|
+
services: selectedServices,
|
|
233
|
+
fromLocalDate: toLocalDateString(startDateSignal.get()),
|
|
234
|
+
toLocalDate: toLocalDateString(endDateSignal.get()),
|
|
235
|
+
timeZone: timezoneSignal.get(),
|
|
236
|
+
cursorPaging: { cursor },
|
|
237
|
+
viewMode: viewModeSignal.get(),
|
|
238
|
+
});
|
|
239
|
+
// API returns grouped slots, merge with existing
|
|
240
|
+
const existing = timeSlotsSignal.get();
|
|
241
|
+
timeSlotsSignal.set(mergeGroupedSlots(existing, response.timeSlots));
|
|
242
|
+
const nextCursor = response.cursorPagingMetadata?.cursors?.next ?? undefined;
|
|
243
|
+
cursorSignal.set(nextCursor);
|
|
244
|
+
hasMoreSignal.set(!!nextCursor);
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
const message = error instanceof Error
|
|
248
|
+
? error.message
|
|
249
|
+
: 'Failed to load more time slots';
|
|
250
|
+
errorSignal.set(message);
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
isLoadingSignal.set(false);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
/**
|
|
257
|
+
* Action: Update date range and refetch slots
|
|
258
|
+
*/
|
|
259
|
+
const setDateRange = (newStartDate, newEndDate) => {
|
|
260
|
+
startDateSignal.set(newStartDate);
|
|
261
|
+
endDateSignal.set(newEndDate);
|
|
262
|
+
fetchTimeSlots();
|
|
263
|
+
};
|
|
264
|
+
/**
|
|
265
|
+
* Action: Update timezone and refetch slots
|
|
266
|
+
*/
|
|
267
|
+
const setTimezone = (newTimezone) => {
|
|
268
|
+
timezoneSignal.set(newTimezone);
|
|
269
|
+
fetchTimeSlots();
|
|
270
|
+
};
|
|
271
|
+
// Auto-fetch on initialization if no pre-loaded data provided
|
|
272
|
+
if (!config.timeSlots && config.services && config.services.length > 0) {
|
|
273
|
+
fetchTimeSlots();
|
|
274
|
+
}
|
|
275
|
+
// Return service API
|
|
276
|
+
return {
|
|
277
|
+
// State Signals
|
|
278
|
+
timeSlots: timeSlotsSignal,
|
|
279
|
+
selection: selectionSignal,
|
|
280
|
+
isLoading: isLoadingSignal,
|
|
281
|
+
error: errorSignal,
|
|
282
|
+
hasMore: hasMoreSignal,
|
|
283
|
+
// Configuration Signals
|
|
284
|
+
viewMode: viewModeSignal,
|
|
285
|
+
startDate: startDateSignal,
|
|
286
|
+
endDate: endDateSignal,
|
|
287
|
+
timezone: timezoneSignal,
|
|
288
|
+
// Actions
|
|
289
|
+
actions: {
|
|
290
|
+
selectSlot,
|
|
291
|
+
selectLocation,
|
|
292
|
+
selectStaffMember,
|
|
293
|
+
clearPreferences,
|
|
294
|
+
loadMore,
|
|
295
|
+
setDateRange,
|
|
296
|
+
setTimezone,
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
});
|
|
300
|
+
/**
|
|
301
|
+
* SSR Loader: Pre-fetches time slots for server-side rendering
|
|
302
|
+
* Use this in Astro/Next.js getServerSideProps or page loaders
|
|
303
|
+
*
|
|
304
|
+
* @example
|
|
305
|
+
* ```typescript
|
|
306
|
+
* // In Astro page
|
|
307
|
+
* const loadResult = await loadTimeSlotListConfig({
|
|
308
|
+
* services: bookingServices,
|
|
309
|
+
* startDate: new Date(),
|
|
310
|
+
* endDate: addDays(new Date(), 7),
|
|
311
|
+
* timezone: 'America/New_York',
|
|
312
|
+
* });
|
|
313
|
+
*
|
|
314
|
+
* if (loadResult.type === 'error') {
|
|
315
|
+
* // Handle error
|
|
316
|
+
* }
|
|
317
|
+
*
|
|
318
|
+
* const timeSlotListConfig = loadResult.config;
|
|
319
|
+
* ```
|
|
320
|
+
*/
|
|
321
|
+
export async function loadTimeSlotListConfig(options) {
|
|
322
|
+
const now = new Date();
|
|
323
|
+
const startDate = options.startDate || now;
|
|
324
|
+
const endDate = options.endDate || getDefaultEndDate();
|
|
325
|
+
const viewMode = options.viewMode || ViewMode.CLASSIC;
|
|
326
|
+
// check for location and staff in config
|
|
327
|
+
try {
|
|
328
|
+
const response = await fetchAvailability({
|
|
329
|
+
services: options.services || [],
|
|
330
|
+
fromLocalDate: toLocalDateString(startDate),
|
|
331
|
+
toLocalDate: toLocalDateString(endDate),
|
|
332
|
+
timeZone: options.timezone ?? getClientTimezone(),
|
|
333
|
+
viewMode,
|
|
334
|
+
});
|
|
335
|
+
const config = {
|
|
336
|
+
services: options.services,
|
|
337
|
+
timeSlots: response.timeSlots || [],
|
|
338
|
+
startDate,
|
|
339
|
+
endDate,
|
|
340
|
+
timezone: options.timezone,
|
|
341
|
+
viewMode,
|
|
342
|
+
};
|
|
343
|
+
return { type: 'success', config };
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
const message = error instanceof Error ? error.message : 'Failed to load time slots';
|
|
347
|
+
return { type: 'error', error: message };
|
|
348
|
+
}
|
|
349
|
+
}
|
package/dist/api/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @module API
|
|
6
6
|
*/
|
|
7
7
|
export { getServiceById, getServiceBySlug, queryServices, type PagingMetadata, type ServiceListFilters, type QueryServicesRequest, } from './query-services/index.js';
|
|
8
|
+
export { fetchAvailability } from './time-slot-list/index.js';
|
|
8
9
|
/**
|
|
9
10
|
* PagingMetadata includes:
|
|
10
11
|
* - limit: Number of items per page
|
package/dist/api/index.js
CHANGED
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
* Query Services API
|
|
3
3
|
* Fetches single or multiple services with pagination and sorting
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import type { services as servicesTypes } from '@wix/bookings';
|
|
5
|
+
import { type Service, type Paging, type Sorting } from '@wix/auto_sdk_bookings_services';
|
|
7
6
|
/**
|
|
8
7
|
* Paging metadata from query services response.
|
|
9
8
|
* Extends the SDK's Paging type with hasNext flag to indicate if more services are available.
|
|
10
9
|
*/
|
|
11
|
-
export type PagingMetadata =
|
|
10
|
+
export type PagingMetadata = Paging & {
|
|
12
11
|
/** Number of items returned in the current page */
|
|
13
12
|
count?: number;
|
|
14
13
|
/** Whether there are more services available to load */
|
|
@@ -31,7 +30,7 @@ export interface QueryServicesRequest {
|
|
|
31
30
|
appId: string;
|
|
32
31
|
filters?: ServiceListFilters;
|
|
33
32
|
pagingMetadata?: PagingMetadata;
|
|
34
|
-
sort?:
|
|
33
|
+
sort?: Sorting[];
|
|
35
34
|
}
|
|
36
35
|
/**
|
|
37
36
|
* Fetches a service by its ID
|
|
@@ -40,7 +39,7 @@ export interface QueryServicesRequest {
|
|
|
40
39
|
* @returns Object containing the service or null if not found
|
|
41
40
|
*/
|
|
42
41
|
export declare const getServiceById: (id: string, appId?: string) => Promise<{
|
|
43
|
-
service:
|
|
42
|
+
service: Service | null;
|
|
44
43
|
}>;
|
|
45
44
|
/**
|
|
46
45
|
* Fetches a service by its slug
|
|
@@ -49,7 +48,7 @@ export declare const getServiceById: (id: string, appId?: string) => Promise<{
|
|
|
49
48
|
* @returns Object containing the service or null if not found
|
|
50
49
|
*/
|
|
51
50
|
export declare const getServiceBySlug: (slug: string, appId?: string) => Promise<{
|
|
52
|
-
service:
|
|
51
|
+
service: Service | null;
|
|
53
52
|
}>;
|
|
54
53
|
/**
|
|
55
54
|
* Query services with pagination and sorting.
|
|
@@ -73,6 +72,6 @@ export declare const getServiceBySlug: (slug: string, appId?: string) => Promise
|
|
|
73
72
|
* ```
|
|
74
73
|
*/
|
|
75
74
|
export declare function queryServices({ appId, filters, pagingMetadata, sort, }: QueryServicesRequest): Promise<{
|
|
76
|
-
services:
|
|
75
|
+
services: Service[];
|
|
77
76
|
pagingMetadata: PagingMetadata;
|
|
78
77
|
}>;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Query Services API
|
|
3
3
|
* Fetches single or multiple services with pagination and sorting
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { queryServices as queryServicesApi, } from '@wix/auto_sdk_bookings_services';
|
|
6
6
|
/**
|
|
7
7
|
* Booking app ID constant
|
|
8
8
|
*/
|
|
@@ -17,7 +17,7 @@ export const getServiceById = async (id, appId) => {
|
|
|
17
17
|
try {
|
|
18
18
|
const finalAppId = appId ?? BOOKING_APP_ID;
|
|
19
19
|
// Query services where _id equals the provided id and appId matches
|
|
20
|
-
const query =
|
|
20
|
+
const query = queryServicesApi({
|
|
21
21
|
conditionalFields: ['STAFF_MEMBER_DETAILS'],
|
|
22
22
|
});
|
|
23
23
|
const result = await query
|
|
@@ -44,7 +44,7 @@ export const getServiceBySlug = async (slug, appId) => {
|
|
|
44
44
|
try {
|
|
45
45
|
const finalAppId = appId ?? BOOKING_APP_ID;
|
|
46
46
|
// Query services where mainSlug.name equals the provided slug and appId matches
|
|
47
|
-
const query =
|
|
47
|
+
const query = queryServicesApi({
|
|
48
48
|
conditionalFields: ['STAFF_MEMBER_DETAILS'],
|
|
49
49
|
});
|
|
50
50
|
const result = await query
|
|
@@ -115,7 +115,7 @@ export async function queryServices({ appId, filters, pagingMetadata, sort, }) {
|
|
|
115
115
|
sort,
|
|
116
116
|
});
|
|
117
117
|
// Call queryServices with QueryServicesRequest and execute the query
|
|
118
|
-
const result = await
|
|
118
|
+
const result = await queryServicesApi(queryRequest).find();
|
|
119
119
|
// Construct PagingMetadata from result properties
|
|
120
120
|
// Includes count (number of items returned) and hasNext (whether more items are available)
|
|
121
121
|
const resultPagingMetadata = {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time Slot List API
|
|
3
|
+
* Fetches available time slots for booking services from Wix Bookings
|
|
4
|
+
*/
|
|
5
|
+
import { type Location, type CursorPaging, type CursorPagingMetadata } from '@wix/auto_sdk_bookings_availability-time-slots';
|
|
6
|
+
import { type Service } from '@wix/auto_sdk_bookings_services';
|
|
7
|
+
import { ViewMode, type TimeSlot } from '../../services/time-slot-list/time-slot-list.def.js';
|
|
8
|
+
export interface FetchAvailabilityOptions {
|
|
9
|
+
services: Service[];
|
|
10
|
+
fromLocalDate?: string | null;
|
|
11
|
+
toLocalDate?: string | null;
|
|
12
|
+
timeZone?: string | null;
|
|
13
|
+
locations?: Location[];
|
|
14
|
+
cursorPaging?: CursorPaging;
|
|
15
|
+
viewMode?: ViewMode;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Response from fetchAvailability with grouped time slots
|
|
19
|
+
*/
|
|
20
|
+
export interface FetchAvailabilityResponse {
|
|
21
|
+
/** Grouped time slots (one per unique time) */
|
|
22
|
+
timeSlots: TimeSlot[];
|
|
23
|
+
/** Cursor pagination metadata for loading more results */
|
|
24
|
+
cursorPagingMetadata?: CursorPagingMetadata;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Main entry point for fetching availability
|
|
28
|
+
* Detects flow type, calls appropriate API, and groups slots by time
|
|
29
|
+
*
|
|
30
|
+
* @param options - Fetch options including services, date range, timezone, and view mode
|
|
31
|
+
* @returns Promise with grouped time slots
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* const response = await fetchAvailability({
|
|
36
|
+
* services: [service],
|
|
37
|
+
* fromLocalDate: '2024-01-15T00:00:00',
|
|
38
|
+
* toLocalDate: '2024-01-22T00:00:00',
|
|
39
|
+
* timeZone: 'America/New_York',
|
|
40
|
+
* viewMode: ViewMode.CLASSIC,
|
|
41
|
+
* });
|
|
42
|
+
* // response.timeSlots are grouped by time
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare function fetchAvailability({ services: selectedServices, viewMode, fromLocalDate, toLocalDate, timeZone, locations, cursorPaging, }: {
|
|
46
|
+
services: Service[];
|
|
47
|
+
fromLocalDate: string;
|
|
48
|
+
toLocalDate: string;
|
|
49
|
+
timeZone?: string;
|
|
50
|
+
locations?: Location[];
|
|
51
|
+
cursorPaging?: CursorPaging;
|
|
52
|
+
viewMode?: ViewMode;
|
|
53
|
+
}): Promise<FetchAvailabilityResponse>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time Slot List API
|
|
3
|
+
* Fetches available time slots for booking services from Wix Bookings
|
|
4
|
+
*/
|
|
5
|
+
import { listAvailabilityTimeSlots, } from '@wix/auto_sdk_bookings_availability-time-slots';
|
|
6
|
+
import { ServiceType } from '@wix/auto_sdk_bookings_services';
|
|
7
|
+
import { ViewMode, } from '../../services/time-slot-list/time-slot-list.def.js';
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Internal Types
|
|
10
|
+
// ============================================================================
|
|
11
|
+
var FlowType;
|
|
12
|
+
(function (FlowType) {
|
|
13
|
+
FlowType["SINGLE_SERVICE"] = "single_service";
|
|
14
|
+
FlowType["MULTI_SERVICE"] = "multi_service";
|
|
15
|
+
FlowType["CLASS"] = "class";
|
|
16
|
+
})(FlowType || (FlowType = {}));
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Grouping Functions
|
|
19
|
+
// ============================================================================
|
|
20
|
+
/**
|
|
21
|
+
* Groups API time slots by their start time
|
|
22
|
+
* Multiple slots for the same time (different locations) are merged into one
|
|
23
|
+
*/
|
|
24
|
+
function groupTimeSlotsByTime(apiSlots) {
|
|
25
|
+
const groupedMap = new Map();
|
|
26
|
+
for (const apiSlot of apiSlots) {
|
|
27
|
+
const key = apiSlot.localStartDate;
|
|
28
|
+
if (!groupedMap.has(key)) {
|
|
29
|
+
// Create new grouped slot - omit location and availableResources
|
|
30
|
+
const { location: _location, availableResources: _availableResources, ...rest } = apiSlot;
|
|
31
|
+
groupedMap.set(key, {
|
|
32
|
+
...rest,
|
|
33
|
+
staffByLocation: [],
|
|
34
|
+
originalSlots: [],
|
|
35
|
+
id: apiSlot.localStartDate,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const groupedSlot = groupedMap.get(key);
|
|
39
|
+
// Extract staff members with IDs and names from availableResources
|
|
40
|
+
const staffMembers = (apiSlot.availableResources || [])
|
|
41
|
+
.flatMap((resourceGroup) => resourceGroup.resources || [])
|
|
42
|
+
.filter((resource) => resource._id && resource.name)
|
|
43
|
+
.map((resource) => ({
|
|
44
|
+
id: resource._id,
|
|
45
|
+
name: resource.name,
|
|
46
|
+
}));
|
|
47
|
+
// Add this slot's location/staff info
|
|
48
|
+
const staffByLocation = {
|
|
49
|
+
location: {
|
|
50
|
+
id: apiSlot.location?._id,
|
|
51
|
+
name: apiSlot.location?.name,
|
|
52
|
+
},
|
|
53
|
+
staffMembers,
|
|
54
|
+
};
|
|
55
|
+
groupedSlot.staffByLocation.push(staffByLocation);
|
|
56
|
+
// Store original slot for booking service
|
|
57
|
+
groupedSlot.originalSlots.push(apiSlot);
|
|
58
|
+
}
|
|
59
|
+
return Array.from(groupedMap.values());
|
|
60
|
+
}
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// Internal Flow Detection
|
|
63
|
+
// ============================================================================
|
|
64
|
+
/**
|
|
65
|
+
* Detects the booking flow type based on services and view mode
|
|
66
|
+
*/
|
|
67
|
+
function detectFlow(bookingServices, viewMode) {
|
|
68
|
+
if (bookingServices.length === 0) {
|
|
69
|
+
if (viewMode === ViewMode.AGENDA) {
|
|
70
|
+
return FlowType.CLASS; // AGENDA can fetch all classes without specifying services
|
|
71
|
+
}
|
|
72
|
+
throw new Error('Services required for classic view mode');
|
|
73
|
+
}
|
|
74
|
+
const allAppointments = bookingServices.every((s) => s.type === ServiceType.APPOINTMENT);
|
|
75
|
+
const allClasses = bookingServices.every((s) => s.type === ServiceType.CLASS);
|
|
76
|
+
if (!allAppointments && !allClasses) {
|
|
77
|
+
throw new Error('All services must be either appointments or classes');
|
|
78
|
+
}
|
|
79
|
+
if (allClasses)
|
|
80
|
+
return FlowType.CLASS;
|
|
81
|
+
if (bookingServices.length === 1)
|
|
82
|
+
return FlowType.SINGLE_SERVICE;
|
|
83
|
+
return FlowType.MULTI_SERVICE;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Main entry point for fetching availability
|
|
87
|
+
* Detects flow type, calls appropriate API, and groups slots by time
|
|
88
|
+
*
|
|
89
|
+
* @param options - Fetch options including services, date range, timezone, and view mode
|
|
90
|
+
* @returns Promise with grouped time slots
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* const response = await fetchAvailability({
|
|
95
|
+
* services: [service],
|
|
96
|
+
* fromLocalDate: '2024-01-15T00:00:00',
|
|
97
|
+
* toLocalDate: '2024-01-22T00:00:00',
|
|
98
|
+
* timeZone: 'America/New_York',
|
|
99
|
+
* viewMode: ViewMode.CLASSIC,
|
|
100
|
+
* });
|
|
101
|
+
* // response.timeSlots are grouped by time
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export async function fetchAvailability({ services: selectedServices, viewMode = ViewMode.CLASSIC, fromLocalDate, toLocalDate, timeZone, locations, cursorPaging, }) {
|
|
105
|
+
const flow = detectFlow(selectedServices, viewMode);
|
|
106
|
+
if (flow === FlowType.SINGLE_SERVICE) {
|
|
107
|
+
const serviceId = selectedServices[0]?._id;
|
|
108
|
+
if (!serviceId) {
|
|
109
|
+
throw new Error('Service ID is required');
|
|
110
|
+
}
|
|
111
|
+
const response = await listAvailabilityTimeSlots({
|
|
112
|
+
serviceId,
|
|
113
|
+
fromLocalDate,
|
|
114
|
+
toLocalDate,
|
|
115
|
+
timeZone,
|
|
116
|
+
locations,
|
|
117
|
+
cursorPaging,
|
|
118
|
+
});
|
|
119
|
+
return {
|
|
120
|
+
timeSlots: groupTimeSlotsByTime(response.timeSlots || []),
|
|
121
|
+
cursorPagingMetadata: response.cursorPagingMetadata,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (flow === FlowType.MULTI_SERVICE) {
|
|
125
|
+
// Phase 2: Multi-service support
|
|
126
|
+
throw new Error('Multi-service appointments not yet supported');
|
|
127
|
+
}
|
|
128
|
+
if (flow === FlowType.CLASS) {
|
|
129
|
+
// Phase 2: Class support
|
|
130
|
+
throw new Error('Class bookings not yet supported');
|
|
131
|
+
}
|
|
132
|
+
throw new Error(`Unknown flow type: ${flow}`);
|
|
133
|
+
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Provides low-level access to BookingService via render props and service setup
|
|
4
4
|
*/
|
|
5
5
|
import React from 'react';
|
|
6
|
-
import { type BookingServiceConfig, type
|
|
7
|
-
import type {
|
|
6
|
+
import { type BookingServiceConfig, type BookingSelection } from '../../../services/booking/booking.js';
|
|
7
|
+
import type { TimeSlot } from '@wix/auto_sdk_bookings_availability-time-slots';
|
|
8
8
|
/**
|
|
9
9
|
* Props for Booking.Root component
|
|
10
10
|
*/
|
|
@@ -33,8 +33,8 @@ export declare function Root(props: RootProps): React.ReactNode;
|
|
|
33
33
|
*/
|
|
34
34
|
export interface DataProps {
|
|
35
35
|
children: (data: {
|
|
36
|
-
|
|
37
|
-
selectedTimeSlot:
|
|
36
|
+
bookingSelections: BookingSelection[];
|
|
37
|
+
selectedTimeSlot: TimeSlot | null;
|
|
38
38
|
}) => React.ReactNode;
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
@@ -45,9 +45,9 @@ export interface DataProps {
|
|
|
45
45
|
* @example
|
|
46
46
|
* ```tsx
|
|
47
47
|
* <Booking.Data>
|
|
48
|
-
* {({
|
|
49
|
-
*
|
|
50
|
-
* <div>{
|
|
48
|
+
* {({ bookingSelections }) => (
|
|
49
|
+
* bookingSelections.length > 0 ? (
|
|
50
|
+
* <div>{bookingSelections.map(s => s.service.name).join(', ')}</div>
|
|
51
51
|
* ) : (
|
|
52
52
|
* <div>No services selected</div>
|
|
53
53
|
* )
|