@wix/headless-bookings 0.0.14 → 0.0.16

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.
@@ -9,7 +9,7 @@ import { SignalsServiceDefinition, } from '@wix/services-definitions/core-servic
9
9
  // Types
10
10
  // ============================================================================
11
11
  /**
12
- * Generates a unique instance ID for service selections
12
+ * Generates a unique instance ID for booking items
13
13
  */
14
14
  function generateInstanceId() {
15
15
  return crypto.randomUUID();
@@ -39,8 +39,8 @@ export const BookingServiceDefinition = defineService('booking');
39
39
  *
40
40
  * // In any page - access booking data
41
41
  * <Booking.Data>
42
- * {({ services, timeSlot, location, formValues, actions }) => (
43
- * <div>{services.map(s => s.service.name).join(', ')}</div>
42
+ * {({ bookingItems, location, formValues, actions }) => (
43
+ * <div>{bookingItems.map(item => item.service.name).join(', ')}</div>
44
44
  * )}
45
45
  * </Booking.Data>
46
46
  * ```
@@ -48,67 +48,93 @@ export const BookingServiceDefinition = defineService('booking');
48
48
  export const BookingService = implementService.withConfig()(BookingServiceDefinition, ({ getService, config }) => {
49
49
  const signalsService = getService(SignalsServiceDefinition);
50
50
  // Initialize signals with config values
51
- // Ensure all initial booking selections have instanceIds
52
- const initialSelectionsWithIds = (config.initialBookingSelections || []).map((s) => ({
53
- ...s,
54
- instanceId: s.instanceId || generateInstanceId(),
51
+ // Ensure all initial booking items have instanceIds
52
+ const initialItemsWithIds = (config.bookingItems || []).map((item) => ({
53
+ ...item,
54
+ instanceId: item.instanceId || generateInstanceId(),
55
55
  }));
56
- const bookingSelectionsSignal = signalsService.signal(initialSelectionsWithIds);
57
- const timeSlotSignal = signalsService.signal(config.initialTimeSlot || null);
58
- // Location is derived internally - initialize as null
59
- const locationSignal = signalsService.signal(null);
60
- const formValuesSignal = signalsService.signal(config.initialFormValues || null);
56
+ const bookingItemsSignal = signalsService.signal(initialItemsWithIds);
57
+ const locationSignal = signalsService.signal(config.location || null);
58
+ const formValuesSignal = signalsService.signal(config.formValues || null);
59
+ const numberOfParticipantsSignal = signalsService.signal(config.numberOfParticipants ?? 1);
61
60
  // ========================================================================
62
61
  // Actions Implementation
63
62
  // ========================================================================
64
63
  const actions = {
65
- // Services actions
66
- setService(service) {
64
+ // Item management actions
65
+ setItem(item) {
67
66
  const instanceId = generateInstanceId();
68
- const newSelection = {
69
- ...service,
67
+ const newItem = {
68
+ ...item,
70
69
  instanceId,
71
70
  };
72
- bookingSelectionsSignal.set([newSelection]);
71
+ bookingItemsSignal.set([newItem]);
73
72
  return instanceId;
74
73
  },
75
- addService(service) {
74
+ addItem(item) {
76
75
  const instanceId = generateInstanceId();
77
- const current = bookingSelectionsSignal.get();
78
- const newSelection = {
79
- ...service,
76
+ const current = bookingItemsSignal.get();
77
+ const newItem = {
78
+ ...item,
80
79
  instanceId,
81
80
  };
82
- bookingSelectionsSignal.set([...current, newSelection]);
81
+ bookingItemsSignal.set([...current, newItem]);
83
82
  return instanceId;
84
83
  },
85
- updateBookingSelection(updates, instanceId) {
86
- const current = bookingSelectionsSignal.get();
87
- // If no instanceId provided, update the first selection (index 0)
84
+ updateItem(updates, instanceId) {
85
+ const current = bookingItemsSignal.get();
86
+ // If no instanceId provided, update the first item (index 0)
88
87
  const targetId = instanceId ?? current[0]?.instanceId;
89
- const updated = current.map((s) => s.instanceId === targetId ? { ...s, ...updates } : s);
90
- bookingSelectionsSignal.set(updated);
88
+ const updated = current.map((item) => item.instanceId === targetId ? { ...item, ...updates } : item);
89
+ bookingItemsSignal.set(updated);
91
90
  },
92
- removeBookingSelection(instanceId) {
93
- const current = bookingSelectionsSignal.get();
91
+ removeItem(instanceId) {
92
+ const current = bookingItemsSignal.get();
94
93
  if (!instanceId) {
95
- // Remove first selection if no instanceId provided
96
- bookingSelectionsSignal.set(current.slice(1));
94
+ // Remove first item if no instanceId provided
95
+ bookingItemsSignal.set(current.slice(1));
97
96
  }
98
97
  else {
99
- const filtered = current.filter((s) => s.instanceId !== instanceId);
100
- bookingSelectionsSignal.set(filtered);
98
+ const filtered = current.filter((item) => item.instanceId !== instanceId);
99
+ bookingItemsSignal.set(filtered);
101
100
  }
102
101
  },
103
- clearServices() {
104
- bookingSelectionsSignal.set([]);
102
+ clearItems() {
103
+ bookingItemsSignal.set([]);
105
104
  },
106
- // Time Slot actions
107
- setTimeSlot(slot) {
108
- timeSlotSignal.set(slot);
105
+ // Resource helpers
106
+ setResourceById(resourceId, resource, instanceId) {
107
+ const current = bookingItemsSignal.get();
108
+ // If no instanceId provided, use the first item
109
+ const targetId = instanceId ?? current[0]?.instanceId;
110
+ if (!targetId)
111
+ return;
112
+ const updated = current.map((item) => {
113
+ if (item.instanceId !== targetId)
114
+ return item;
115
+ // Find and replace resource by ID, or add if not found
116
+ const existingIndex = item.resources.findIndex((r) => r._id === resourceId);
117
+ const newResources = [...item.resources];
118
+ if (existingIndex >= 0) {
119
+ newResources[existingIndex] = resource;
120
+ }
121
+ else {
122
+ newResources.push(resource);
123
+ }
124
+ return { ...item, resources: newResources };
125
+ });
126
+ bookingItemsSignal.set(updated);
109
127
  },
110
- clearTimeSlot() {
111
- timeSlotSignal.set(null);
128
+ getResourceById(resourceId, instanceId) {
129
+ const current = bookingItemsSignal.get();
130
+ // If no instanceId provided, use the first item
131
+ const targetId = instanceId ?? current[0]?.instanceId;
132
+ if (!targetId)
133
+ return undefined;
134
+ const targetItem = current.find((item) => item.instanceId === targetId);
135
+ if (!targetItem)
136
+ return undefined;
137
+ return targetItem.resources.find((r) => r._id === resourceId);
112
138
  },
113
139
  // Location actions
114
140
  setLocation(location) {
@@ -124,19 +150,23 @@ export const BookingService = implementService.withConfig()(BookingServiceDefini
124
150
  clearFormValues() {
125
151
  formValuesSignal.set(null);
126
152
  },
153
+ // Number of Participants actions
154
+ setNumberOfParticipants(count) {
155
+ numberOfParticipantsSignal.set(count);
156
+ },
127
157
  // Clear all
128
158
  clearAll() {
129
- bookingSelectionsSignal.set([]);
130
- timeSlotSignal.set(null);
159
+ bookingItemsSignal.set([]);
131
160
  locationSignal.set(null);
132
161
  formValuesSignal.set(null);
162
+ numberOfParticipantsSignal.set(1);
133
163
  },
134
164
  };
135
165
  return {
136
- bookingSelections: bookingSelectionsSignal,
137
- timeSlot: timeSlotSignal,
166
+ bookingItems: bookingItemsSignal,
138
167
  location: locationSignal,
139
168
  formValues: formValuesSignal,
169
+ numberOfParticipants: numberOfParticipantsSignal,
140
170
  actions,
141
171
  };
142
172
  });
@@ -3,6 +3,6 @@
3
3
  * Main export file for all service definitions and implementations
4
4
  */
5
5
  export { ServiceService, ServiceServiceDefinition, type ServiceServiceAPI, type ServiceServiceConfig, type ServiceActions, type ServicePaymentData, loadServiceServiceInitialData, type SuccessServiceServiceConfigResult, type NotFoundServiceServiceConfigResult, } from './service/service.js';
6
- export { BookingService, BookingServiceDefinition, type BookingServiceConfig, type BookingSelection, type Location, type BookingServiceActions, } from './booking/booking.js';
6
+ export { BookingService, BookingServiceDefinition, type BookingServiceConfig, type Location, type BookingServiceActions, } from './booking/booking.js';
7
7
  export { TimeSlotListService, TimeSlotListServiceDefinition, ViewMode, loadTimeSlotListConfig, type TimeSlotListServiceAPI, type TimeSlotListServiceActions, type TimeSlotListServiceConfig, type SlotSelection, type LoadTimeSlotListConfigOptions, type LoadTimeSlotListConfigResult, } from './time-slot-list/index.js';
8
8
  export { ServiceListService, ServiceListServiceDefinition, type ServiceListServiceAPI, type ServiceListServiceConfig, type ServiceListActions, type QueryOptions, loadServiceListServiceInitialData, type SuccessServiceListServiceConfigResult, type ErrorServiceListServiceConfigResult, } from './service-list/service-list.js';
@@ -81,8 +81,8 @@ export const ServiceService = implementService.withConfig()(ServiceServiceDefini
81
81
  const currentService = serviceSignal.get();
82
82
  if (!currentService._id)
83
83
  return false;
84
- const selections = bookingService.bookingSelections.get();
85
- return selections.some((selection) => selection.service._id === currentService._id);
84
+ const bookingItems = bookingService.bookingItems.get();
85
+ return bookingItems.some(({ service }) => service._id === currentService._id);
86
86
  });
87
87
  // requiresAvailability signal - true for APPOINTMENT and CLASS types
88
88
  const requiresAvailabilitySignal = signalsService.computed(() => {
@@ -185,25 +185,28 @@ export const ServiceService = implementService.withConfig()(ServiceServiceDefini
185
185
  if (!bookableSignal.get() || !bookingService)
186
186
  return;
187
187
  const currentService = serviceSignal.get();
188
- bookingService.actions.setService({ service: currentService });
188
+ bookingService.actions.updateItem({ service: currentService });
189
189
  },
190
190
  add: () => {
191
191
  if (!bookableSignal.get() || !bookingService)
192
192
  return;
193
193
  const currentService = serviceSignal.get();
194
- bookingService.actions.addService({ service: currentService });
194
+ bookingService.actions.addItem({
195
+ service: currentService,
196
+ resources: [],
197
+ });
195
198
  },
196
199
  remove: () => {
197
200
  if (!bookableSignal.get() || !bookingService)
198
201
  return;
199
202
  const currentService = serviceSignal.get();
200
203
  // Remove all instances of this service (by serviceId)
201
- const selections = bookingService.bookingSelections.get();
202
- const instancesToRemove = selections
204
+ const bookingItems = bookingService.bookingItems.get();
205
+ const instancesToRemove = bookingItems
203
206
  .filter((s) => s.service._id === currentService._id)
204
207
  .map((s) => s.instanceId);
205
208
  instancesToRemove.forEach((instanceId) => {
206
- bookingService.actions.removeBookingSelection(instanceId);
209
+ bookingService.actions.removeItem(instanceId);
207
210
  });
208
211
  },
209
212
  };
@@ -6,7 +6,7 @@ import { implementService } from '@wix/services-definitions';
6
6
  import { SignalsServiceDefinition } from '@wix/services-definitions/core-services/signals';
7
7
  import { TimeSlotListServiceDefinition, ViewMode, } from './time-slot-list.def.js';
8
8
  import { fetchAvailability } from '../../api/time-slot-list/index.js';
9
- import { BookingServiceDefinition } from '../booking/booking.js';
9
+ import { BookingServiceDefinition, } from '../booking/booking.js';
10
10
  // ============================================================================
11
11
  // Helpers
12
12
  // ============================================================================
@@ -79,7 +79,7 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
79
79
  const getServices = () => {
80
80
  if (config.services?.length)
81
81
  return config.services;
82
- return bookingService.bookingSelections.get().map((s) => s.service);
82
+ return bookingService.bookingItems.get().map((s) => s.service);
83
83
  };
84
84
  /**
85
85
  * Fetch time slots from API
@@ -115,82 +115,139 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
115
115
  };
116
116
  /**
117
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
118
+ * Auto-selects location/staff based on availability:
119
+ * - One location + one staff → auto-select both
120
+ * - One location + many staff → auto-select location only
121
+ * - Many locations + one staff → auto-select staff only
122
+ * - Many locations + many staff → don't auto-select anything
120
123
  */
121
124
  const selectSlot = (slot) => {
122
125
  const currentSelection = selectionSignal.get();
123
126
  if (currentSelection?.slot?.id === slot.id)
124
127
  return;
128
+ const timeSlot = {
129
+ startDate: slot.localStartDate,
130
+ endDate: slot.localEndDate,
131
+ totalCapacity: slot.totalCapacity ?? undefined,
132
+ remainingCapacity: slot.remainingCapacity ?? undefined,
133
+ scheduleId: slot.scheduleId ?? undefined,
134
+ eventInfo: slot.eventInfo ?? undefined,
135
+ };
125
136
  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
- },
137
+ // Collect all unique staff members across all locations
138
+ const allStaffMembers = slot.staffByLocation.flatMap((loc) => loc.staffMembers);
139
+ const uniqueStaffIds = new Set(allStaffMembers.map((s) => s.id));
140
+ const hasOneStaff = uniqueStaffIds.size === 1;
141
+ // Determine what to auto-select
142
+ let locationId = null;
143
+ let staffMemberId = null;
144
+ if (hasOneLocation) {
145
+ const { location } = slot.staffByLocation[0];
146
+ locationId = location.id;
147
+ bookingService.actions.setLocation(location);
148
+ }
149
+ else {
150
+ bookingService.actions.clearLocation();
151
+ }
152
+ if (hasOneStaff) {
153
+ const staffMember = allStaffMembers[0];
154
+ staffMemberId = staffMember.id;
155
+ bookingService.actions.updateItem({
156
+ timeSlot,
157
+ resources: [
158
+ {
159
+ _id: staffMemberId,
160
+ name: staffMember.name,
161
+ },
162
+ ],
146
163
  });
147
164
  }
148
165
  else {
149
- selectionSignal.set({
150
- slot,
151
- locationId: null,
152
- staffMemberId: null,
166
+ bookingService.actions.updateItem({
167
+ timeSlot,
168
+ resources: [],
153
169
  });
154
170
  }
171
+ selectionSignal.set({
172
+ slot,
173
+ locationId,
174
+ staffMemberId,
175
+ });
155
176
  };
156
177
  /**
157
178
  * Action: Select a location for the current slot
158
- * Updates the booking service with the correct original slot for this location
179
+ * Updates the booking service with the location
180
+ * If the location has only one staff member, auto-selects that staff member too
159
181
  */
160
182
  const selectLocation = (locationId) => {
161
183
  const currentSelections = selectionSignal.get();
162
184
  if (!currentSelections?.slot)
163
185
  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,
186
+ const slot = currentSelections.slot;
187
+ const locationEntry = slot.staffByLocation.find((loc) => loc.location.id === locationId);
188
+ if (!locationEntry)
189
+ return;
190
+ const { location, staffMembers } = locationEntry;
191
+ bookingService.actions.setLocation(location);
192
+ // If this location has only one staff member, auto-select them
193
+ const hasOneStaff = staffMembers.length === 1;
194
+ if (hasOneStaff) {
195
+ const staffMember = staffMembers[0];
196
+ selectionSignal.set({
197
+ ...currentSelections,
198
+ locationId,
199
+ staffMemberId: staffMember.id,
170
200
  });
201
+ bookingService.actions.updateItem({
202
+ resources: [
203
+ {
204
+ _id: staffMember.id,
205
+ name: staffMember.name,
206
+ },
207
+ ],
208
+ });
209
+ }
210
+ else {
211
+ selectionSignal.set({ ...currentSelections, locationId });
171
212
  }
172
213
  };
173
214
  /**
174
215
  * Action: Select a staff member for the current slot
175
- * Updates the booking service with the selected resource for the slot's service
216
+ * Updates the booking service with the selected resource
217
+ * If the staff member is only available at one location, auto-selects that location too
176
218
  */
177
219
  const selectStaffMember = (staffMemberId) => {
178
220
  const currentSelections = selectionSignal.get();
179
221
  if (!currentSelections?.slot)
180
222
  return;
181
- selectionSignal.set({ ...currentSelections, staffMemberId });
182
223
  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
- }
224
+ // Find all locations that have this staff member
225
+ const locationsWithThisStaff = slot.staffByLocation.filter((loc) => loc.staffMembers.some((s) => s.id === staffMemberId));
226
+ // Find the staff member info
227
+ const staffMember = locationsWithThisStaff[0]?.staffMembers.find((s) => s.id === staffMemberId);
228
+ if (!staffMember)
229
+ return;
230
+ bookingService.actions.updateItem({
231
+ resources: [
232
+ {
233
+ _id: staffMember.id,
234
+ name: staffMember.name,
235
+ },
236
+ ],
237
+ });
238
+ // If this staff member is only at one location, auto-select that location
239
+ const hasOneLocation = locationsWithThisStaff.length === 1;
240
+ if (hasOneLocation) {
241
+ const location = locationsWithThisStaff[0].location;
242
+ selectionSignal.set({
243
+ ...currentSelections,
244
+ staffMemberId,
245
+ locationId: location.id,
246
+ });
247
+ bookingService.actions.setLocation(location);
248
+ }
249
+ else {
250
+ selectionSignal.set({ ...currentSelections, staffMemberId });
194
251
  }
195
252
  };
196
253
  /**
@@ -206,15 +263,11 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
206
263
  locationId: null,
207
264
  staffMemberId: null,
208
265
  });
209
- // Clear location in booking service
266
+ // Clear selections in booking service
210
267
  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
- }
268
+ bookingService.actions.updateItem({
269
+ resources: [],
270
+ });
218
271
  };
219
272
  /**
220
273
  * Action: Load more slots (pagination)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/headless-bookings",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -68,5 +68,5 @@
68
68
  "groupId": "com.wixpress.headless-components"
69
69
  }
70
70
  },
71
- "falconPackageHash": "e3681a6dd045861d1cb7cefbdc04f1ad9e3eb62094bb6a8970b90cd2"
71
+ "falconPackageHash": "9e70aba1e4a39f28a7d02fca86dcf92c71014635fbfc9208bf1569f4"
72
72
  }