@wix/headless-bookings 0.0.44 → 0.0.46

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.
Files changed (35) hide show
  1. package/cjs/dist/__mocks__/booking.d.ts +9 -4
  2. package/cjs/dist/__mocks__/booking.js +48 -3
  3. package/cjs/dist/__mocks__/services.d.ts +21 -0
  4. package/cjs/dist/__mocks__/services.js +54 -0
  5. package/cjs/dist/__mocks__/time-slots.d.ts +28 -0
  6. package/cjs/dist/__mocks__/time-slots.js +116 -0
  7. package/cjs/dist/api/index.d.ts +1 -0
  8. package/cjs/dist/api/index.js +1 -0
  9. package/cjs/dist/api/query-categories/index.d.ts +34 -0
  10. package/cjs/dist/api/query-categories/index.js +28 -0
  11. package/cjs/dist/api/query-services/index.js +9 -2
  12. package/cjs/dist/react/core/service-list/ServiceListFilter.d.ts +55 -0
  13. package/cjs/dist/react/core/service-list/ServiceListFilter.js +59 -0
  14. package/cjs/dist/react/service-list/ServiceList.d.ts +62 -1
  15. package/cjs/dist/react/service-list/ServiceList.js +53 -1
  16. package/cjs/dist/services/service-list/service-list.d.ts +14 -1
  17. package/cjs/dist/services/service-list/service-list.js +94 -12
  18. package/dist/__mocks__/booking.d.ts +9 -4
  19. package/dist/__mocks__/booking.js +48 -3
  20. package/dist/__mocks__/services.d.ts +21 -0
  21. package/dist/__mocks__/services.js +54 -0
  22. package/dist/__mocks__/time-slots.d.ts +28 -0
  23. package/dist/__mocks__/time-slots.js +116 -0
  24. package/dist/api/index.d.ts +1 -0
  25. package/dist/api/index.js +1 -0
  26. package/dist/api/query-categories/index.d.ts +34 -0
  27. package/dist/api/query-categories/index.js +28 -0
  28. package/dist/api/query-services/index.js +9 -2
  29. package/dist/react/core/service-list/ServiceListFilter.d.ts +55 -0
  30. package/dist/react/core/service-list/ServiceListFilter.js +59 -0
  31. package/dist/react/service-list/ServiceList.d.ts +62 -1
  32. package/dist/react/service-list/ServiceList.js +53 -1
  33. package/dist/services/service-list/service-list.d.ts +14 -1
  34. package/dist/services/service-list/service-list.js +94 -12
  35. package/package.json +2 -2
@@ -4,7 +4,9 @@
4
4
  */
5
5
  import { type Signal, type ReadOnlySignal } from '@wix/services-definitions/core-services/signals';
6
6
  import type { Service } from '@wix/auto_sdk_bookings_services';
7
- import type { QueryServicesRequest, PagingMetadata } from '../../api/query-services/index.js';
7
+ import type { QueryServicesRequest, PagingMetadata, ServiceListFilters } from '../../api/query-services/index.js';
8
+ import { type Category } from '../../api/query-categories/index.js';
9
+ import type { FilterOption } from '@wix/headless-components/react';
8
10
  export type { ServiceListFilters } from '../../api/query-services/index.js';
9
11
  /**
10
12
  * Query options without paging metadata (user-controlled request params)
@@ -16,6 +18,7 @@ export type QueryOptions = Omit<QueryServicesRequest, 'pagingMetadata'>;
16
18
  */
17
19
  export interface ServiceListServiceConfig {
18
20
  services?: Service[];
21
+ categories?: Category[];
19
22
  options?: QueryServicesRequest;
20
23
  }
21
24
  /**
@@ -24,6 +27,8 @@ export interface ServiceListServiceConfig {
24
27
  export interface ServiceListActions {
25
28
  loadMore: (count: number) => Promise<void>;
26
29
  setQueryOptions: (options: QueryOptions) => void;
30
+ setFilter: (filter: ServiceListFilters) => void;
31
+ resetFilter: () => void;
27
32
  }
28
33
  /**
29
34
  * API interface for the ServiceList service
@@ -39,6 +44,14 @@ export interface ServiceListServiceAPI {
39
44
  isLoadingSignal: Signal<boolean>;
40
45
  /** Reactive signal containing any error message, or null if no error */
41
46
  errorSignal: Signal<string | null>;
47
+ /** Reactive signal containing available categories for filtering */
48
+ categoriesSignal: Signal<Category[]>;
49
+ /** Reactive signal containing current filter state */
50
+ filterSignal: Signal<ServiceListFilters | null>;
51
+ /** Computed signal containing filter options derived from categories */
52
+ filterOptionsSignal: ReadOnlySignal<FilterOption[]>;
53
+ /** Computed signal indicating if any filters are active */
54
+ isFilteredSignal: ReadOnlySignal<boolean>;
42
55
  /** Actions object containing query actions */
43
56
  actions: ServiceListActions;
44
57
  /** Reactive signal indicating if there are more services to load */
@@ -5,6 +5,8 @@
5
5
  import { defineService, implementService } from '@wix/services-definitions';
6
6
  import { SignalsServiceDefinition, } from '@wix/services-definitions/core-services/signals';
7
7
  import { queryServices } from '../../api/query-services/index.js';
8
+ import { queryCategories, } from '../../api/query-categories/index.js';
9
+ import { BookingServiceDefinition } from '../booking/booking.js';
8
10
  /**
9
11
  * Booking app ID constant
10
12
  */
@@ -73,6 +75,7 @@ export const ServiceListServiceDefinition = defineService('service-list');
73
75
  export const ServiceListService = implementService.withConfig()(ServiceListServiceDefinition, ({ getService, config }) => {
74
76
  let firstRun = true;
75
77
  const signalsService = getService(SignalsServiceDefinition);
78
+ const bookingService = getService(BookingServiceDefinition);
76
79
  // Default appId to BOOKING_APP_ID if not provided
77
80
  const appId = config.options?.appId ?? BOOKING_APP_ID;
78
81
  // Check if services are provided (showcase mode vs query mode)
@@ -111,13 +114,59 @@ export const ServiceListService = implementService.withConfig()(ServiceListServi
111
114
  const pagingMetadataSignal = signalsService.signal(initialPagingMetadata);
112
115
  const isLoadingSignal = signalsService.signal(false);
113
116
  const errorSignal = signalsService.signal(null);
117
+ // Categories signal - initialized from config
118
+ const categoriesSignal = signalsService.signal(config.categories || []);
119
+ // Filter signal - current filter state (separate from queryOptions for UI)
120
+ const filterSignal = signalsService.signal(config.options?.filters || null);
121
+ // Computed filter options from categories
122
+ const filterOptionsSignal = signalsService.computed(() => {
123
+ const categories = categoriesSignal.get();
124
+ if (!categories.length)
125
+ return [];
126
+ return [
127
+ {
128
+ key: 'category',
129
+ label: 'Category',
130
+ type: 'single',
131
+ displayType: 'text',
132
+ fieldName: 'categoryIds',
133
+ validValues: categories
134
+ .map((c) => c._id)
135
+ .filter((id) => Boolean(id)),
136
+ valueFormatter: (id) => {
137
+ const cat = categories.find((c) => c._id === id);
138
+ return cat?.name || String(id);
139
+ },
140
+ },
141
+ ];
142
+ });
143
+ // Computed signal for filtered state
144
+ const isFilteredSignal = signalsService.computed(() => {
145
+ const filter = filterSignal.get();
146
+ return !!(filter?.categoryIds?.length || filter?.locationIds?.length);
147
+ });
148
+ // Helper to get effective filters including location from BookingService
149
+ const getEffectiveFilters = (baseFilters) => {
150
+ const selectedLocation = bookingService.location.get();
151
+ const locationId = selectedLocation?.business?._id;
152
+ if (!baseFilters && !locationId) {
153
+ return undefined;
154
+ }
155
+ return {
156
+ ...baseFilters,
157
+ ...(locationId ? { locationIds: [locationId] } : {}),
158
+ };
159
+ };
114
160
  // Auto-query on queryOptions changes (client-side only)
115
- // IMPORTANT: Only subscribes to queryOptionsSignal, NOT pagingMetadataSignal
161
+ // IMPORTANT: Only subscribes to queryOptionsSignal and bookingService.location
116
162
  if (typeof window !== 'undefined') {
117
163
  signalsService.effect(async () => {
118
164
  // CRITICAL: Read queryOptionsSignal to establish dependency
119
165
  // Do NOT read pagingMetadataSignal here to avoid infinite loop
120
166
  const queryOptions = queryOptionsSignal.get();
167
+ // Also subscribe to location changes from BookingService
168
+ // Reading the signal here establishes the dependency
169
+ bookingService.location.get();
121
170
  if (firstRun) {
122
171
  firstRun = false;
123
172
  // If we have initial services (showcase mode), don't query on first run
@@ -126,13 +175,15 @@ export const ServiceListService = implementService.withConfig()(ServiceListServi
126
175
  }
127
176
  }
128
177
  // Read paging metadata outside of dependency tracking for the fetch
129
- const currentPaging = pagingMetadataSignal.get();
178
+ const currentPaging = pagingMetadataSignal.peek();
179
+ // Get effective filters including location from BookingService
180
+ const effectiveFilters = getEffectiveFilters(queryOptions.filters);
130
181
  try {
131
182
  isLoadingSignal.set(true);
132
183
  errorSignal.set(null);
133
184
  const result = await queryServices({
134
185
  appId: queryOptions.appId,
135
- filters: queryOptions.filters,
186
+ filters: effectiveFilters,
136
187
  pagingMetadata: {
137
188
  limit: currentPaging.limit,
138
189
  offset: 0, // Reset to first page when query options change
@@ -162,12 +213,14 @@ export const ServiceListService = implementService.withConfig()(ServiceListServi
162
213
  offset: nextOffset,
163
214
  hasNext: false, // Will be updated after query
164
215
  };
216
+ // Get effective filters including location from BookingService
217
+ const effectiveFilters = getEffectiveFilters(currentQueryOptions.filters);
165
218
  try {
166
219
  isLoadingSignal.set(true);
167
220
  errorSignal.set(null);
168
221
  const result = await queryServices({
169
222
  appId: currentQueryOptions.appId,
170
- filters: currentQueryOptions.filters,
223
+ filters: effectiveFilters,
171
224
  pagingMetadata: nextPaging,
172
225
  sort: currentQueryOptions.sort,
173
226
  });
@@ -189,6 +242,10 @@ export const ServiceListService = implementService.withConfig()(ServiceListServi
189
242
  pagingMetadataSignal,
190
243
  isLoadingSignal,
191
244
  errorSignal,
245
+ categoriesSignal,
246
+ filterSignal,
247
+ filterOptionsSignal,
248
+ isFilteredSignal,
192
249
  actions: {
193
250
  loadMore: async (count) => {
194
251
  await loadMoreCursor(count);
@@ -204,6 +261,26 @@ export const ServiceListService = implementService.withConfig()(ServiceListServi
204
261
  });
205
262
  queryOptionsSignal.set(options);
206
263
  },
264
+ setFilter: (filter) => {
265
+ // Update filter signal
266
+ filterSignal.set(filter);
267
+ // Update query options with new filters (triggers re-fetch)
268
+ const currentOptions = queryOptionsSignal.get();
269
+ queryOptionsSignal.set({
270
+ ...currentOptions,
271
+ filters: filter,
272
+ });
273
+ },
274
+ resetFilter: () => {
275
+ // Clear filter signal
276
+ filterSignal.set(null);
277
+ // Clear filters from query options (triggers re-fetch)
278
+ const currentOptions = queryOptionsSignal.get();
279
+ queryOptionsSignal.set({
280
+ ...currentOptions,
281
+ filters: undefined,
282
+ });
283
+ },
207
284
  },
208
285
  hasMoreServicesSignal: signalsService.computed(() => {
209
286
  const paging = pagingMetadataSignal.get();
@@ -247,19 +324,24 @@ export const ServiceListService = implementService.withConfig()(ServiceListServi
247
324
  */
248
325
  export async function loadServiceListServiceInitialData(options) {
249
326
  try {
250
- const result = await queryServices({
251
- appId: options.appId,
252
- filters: options.filters,
253
- pagingMetadata: options.pagingMetadata,
254
- sort: options.sort,
255
- });
327
+ // Fetch services and categories in parallel
328
+ const [servicesResult, categoriesResult] = await Promise.all([
329
+ queryServices({
330
+ appId: options.appId,
331
+ filters: options.filters,
332
+ pagingMetadata: options.pagingMetadata,
333
+ sort: options.sort,
334
+ }),
335
+ queryCategories(),
336
+ ]);
256
337
  return {
257
338
  type: 'success',
258
339
  config: {
259
- services: result.services,
340
+ services: servicesResult.services,
341
+ categories: categoriesResult.categories,
260
342
  options: {
261
343
  ...options,
262
- pagingMetadata: result.pagingMetadata,
344
+ pagingMetadata: servicesResult.pagingMetadata,
263
345
  },
264
346
  },
265
347
  };
@@ -1,9 +1,14 @@
1
- /**
2
- * Shared mock data for booking service tests
3
- */
4
1
  import type { Resource } from '@wix/auto_sdk_bookings_availability-time-slots';
5
2
  import type { FormValues } from '@wix/form-public';
6
- import type { ServiceSelection, ServiceSelectionTimeSlot, BookingServiceConfig } from '../services/booking/booking.js';
3
+ import type { ServiceSelection, ServiceSelectionTimeSlot, BookingServiceConfig, BookingService } from '../services/booking/booking.js';
4
+ import type { Signal } from '@wix/services-definitions/core-services/signals';
5
+ export declare function mockSignal<T>(initialValue: T): Signal<T>;
6
+ export declare function createMockSignalsService(): {
7
+ signal: typeof mockSignal;
8
+ computed: <T>(fn: () => T) => Signal<T>;
9
+ effect: (fn: () => void) => void;
10
+ };
11
+ export declare function createMockBookingService(initialSelections?: ServiceSelection[]): BookingService;
7
12
  /**
8
13
  * Mock resource (staff member)
9
14
  */
@@ -1,7 +1,52 @@
1
- /**
2
- * Shared mock data for booking service tests
3
- */
4
1
  import { mockService, mockServiceClass } from './services.js';
2
+ import { vi } from 'vitest';
3
+ export function mockSignal(initialValue) {
4
+ let value = initialValue;
5
+ return {
6
+ get: () => value,
7
+ peek: () => value,
8
+ set: (newValue) => {
9
+ value = newValue;
10
+ },
11
+ };
12
+ }
13
+ export function createMockSignalsService() {
14
+ return {
15
+ signal: mockSignal,
16
+ computed: (fn) => mockSignal(fn()),
17
+ effect: vi.fn((fn) => {
18
+ fn();
19
+ }),
20
+ };
21
+ }
22
+ export function createMockBookingService(initialSelections = []) {
23
+ const serviceSelectionsSignal = mockSignal(initialSelections);
24
+ const locationSignal = mockSignal(null);
25
+ const timezoneSignal = mockSignal('UTC');
26
+ const formSubmissionSignal = mockSignal(null);
27
+ return {
28
+ serviceSelections: serviceSelectionsSignal,
29
+ location: locationSignal,
30
+ timezone: timezoneSignal,
31
+ formSubmission: formSubmissionSignal,
32
+ actions: {
33
+ updateItem: vi.fn(),
34
+ setItem: vi.fn(),
35
+ addItem: vi.fn(),
36
+ removeItem: vi.fn(),
37
+ clearItems: vi.fn(),
38
+ setResourceById: vi.fn(),
39
+ getResourceById: vi.fn(),
40
+ setLocation: vi.fn(),
41
+ clearLocation: vi.fn(),
42
+ setTimezone: vi.fn(),
43
+ setFormSubmission: vi.fn(),
44
+ clearFormSubmission: vi.fn(),
45
+ clearAll: vi.fn(),
46
+ book: vi.fn(),
47
+ },
48
+ };
49
+ }
5
50
  /**
6
51
  * Mock resource (staff member)
7
52
  */
@@ -5,6 +5,7 @@ import type { Service } from '@wix/auto_sdk_bookings_services';
5
5
  import type { ServiceServiceConfig } from '../services/service/service.js';
6
6
  import type { ServiceListServiceConfig } from '../services/service-list/service-list.js';
7
7
  import type { PagingMetadata } from '../api/query-services/index.js';
8
+ import type { Category } from '../api/query-categories/index.js';
8
9
  /**
9
10
  * Booking app ID constant
10
11
  */
@@ -73,3 +74,23 @@ export declare const mockServiceConfig: ServiceServiceConfig;
73
74
  * Helper to create a large list of services for pagination tests
74
75
  */
75
76
  export declare function createLargeServiceList(count: number): Service[];
77
+ /**
78
+ * Mock categories for filter tests
79
+ */
80
+ export declare const mockCategories: Category[];
81
+ /**
82
+ * Empty categories array
83
+ */
84
+ export declare const mockEmptyCategories: Category[];
85
+ /**
86
+ * Service list config with categories for filter testing
87
+ */
88
+ export declare const mockServiceListConfigWithCategories: ServiceListServiceConfig;
89
+ /**
90
+ * Service list config with categories and hasNext for pagination + filter testing
91
+ */
92
+ export declare const mockServiceListConfigWithCategoriesAndMore: ServiceListServiceConfig;
93
+ /**
94
+ * Empty service list config with categories
95
+ */
96
+ export declare const mockEmptyServiceListConfigWithCategories: ServiceListServiceConfig;
@@ -306,3 +306,57 @@ export function createLargeServiceList(count) {
306
306
  },
307
307
  }));
308
308
  }
309
+ /**
310
+ * Mock categories for filter tests
311
+ */
312
+ export const mockCategories = [
313
+ {
314
+ _id: 'category-1',
315
+ name: 'Wellness',
316
+ },
317
+ {
318
+ _id: 'category-2',
319
+ name: 'Fitness',
320
+ },
321
+ {
322
+ _id: 'category-3',
323
+ name: 'Beauty',
324
+ },
325
+ ];
326
+ /**
327
+ * Empty categories array
328
+ */
329
+ export const mockEmptyCategories = [];
330
+ /**
331
+ * Service list config with categories for filter testing
332
+ */
333
+ export const mockServiceListConfigWithCategories = {
334
+ services: mockServices,
335
+ categories: mockCategories,
336
+ options: {
337
+ appId: BOOKING_APP_ID,
338
+ pagingMetadata: mockPagingMetadata,
339
+ },
340
+ };
341
+ /**
342
+ * Service list config with categories and hasNext for pagination + filter testing
343
+ */
344
+ export const mockServiceListConfigWithCategoriesAndMore = {
345
+ services: mockServices,
346
+ categories: mockCategories,
347
+ options: {
348
+ appId: BOOKING_APP_ID,
349
+ pagingMetadata: mockPagingMetadataWithMore,
350
+ },
351
+ };
352
+ /**
353
+ * Empty service list config with categories
354
+ */
355
+ export const mockEmptyServiceListConfigWithCategories = {
356
+ services: [],
357
+ categories: mockCategories,
358
+ options: {
359
+ appId: BOOKING_APP_ID,
360
+ pagingMetadata: mockPagingMetadataEmpty,
361
+ },
362
+ };
@@ -0,0 +1,28 @@
1
+ import type { TimeSlot } from '@wix/auto_sdk_bookings_availability-time-slots';
2
+ import type { TimeSlotListServiceConfig, TimeSlotListServiceAPI } from '../services/time-slot-list/time-slot-list.def.js';
3
+ import type { BookingService } from '../services/booking/booking.js';
4
+ export declare function createTimeSlotListService(overrides?: Partial<TimeSlotListServiceConfig>): {
5
+ service: TimeSlotListServiceAPI;
6
+ bookingService: BookingService;
7
+ };
8
+ export declare function createTimeSlot(overrides?: Partial<TimeSlot>): TimeSlot;
9
+ export declare function createTimeSlots(count: number): TimeSlot[];
10
+ export declare function createTimeSlotListConfig(overrides?: Partial<TimeSlotListServiceConfig>): TimeSlotListServiceConfig;
11
+ export declare const mockTimeSlot: TimeSlot;
12
+ export declare const mockTimeSlotMultipleStaff: TimeSlot;
13
+ export declare const mockTimeSlotNotBookable: TimeSlot;
14
+ export declare const mockTimeSlots: TimeSlot[];
15
+ export declare const mockStartDate: Date;
16
+ export declare const mockEndDate: Date;
17
+ export declare const mockTimezone = "America/New_York";
18
+ export declare const mockTimeSlotListConfig: TimeSlotListServiceConfig;
19
+ export declare const mockEmptyTimeSlotListConfig: TimeSlotListServiceConfig;
20
+ export declare const mockCursorPagingMetadataWithMore: {
21
+ cursors: {
22
+ next: string;
23
+ };
24
+ };
25
+ export declare const mockCursorPagingMetadataNoMore: {
26
+ cursors: {};
27
+ };
28
+ export declare function createMockTimeSlotListService(config?: Partial<TimeSlotListServiceConfig>): TimeSlotListServiceAPI;
@@ -0,0 +1,116 @@
1
+ import { vi } from 'vitest';
2
+ import { TimeSlotListService } from '../services/time-slot-list/time-slot-list.js';
3
+ import { createMockBookingService, createMockSignalsService, mockSignal, } from './booking.js';
4
+ export function createTimeSlotListService(overrides = {}) {
5
+ const config = createTimeSlotListConfig(overrides);
6
+ const mockBookingService = createMockBookingService();
7
+ const mockSignalsService = createMockSignalsService();
8
+ const getService = (definition) => {
9
+ const def = definition;
10
+ if (def.name === 'signals') {
11
+ return mockSignalsService;
12
+ }
13
+ if (def.name === 'booking') {
14
+ return mockBookingService;
15
+ }
16
+ throw new Error(`Unknown service: ${def.name}`);
17
+ };
18
+ const service = TimeSlotListService({
19
+ getService,
20
+ config,
21
+ });
22
+ return {
23
+ service,
24
+ bookingService: mockBookingService,
25
+ };
26
+ }
27
+ export function createTimeSlot(overrides = {}) {
28
+ return {
29
+ localStartDate: '2024-01-15T10:00:00',
30
+ localEndDate: '2024-01-15T11:00:00',
31
+ bookable: true,
32
+ totalCapacity: 10,
33
+ remainingCapacity: 5,
34
+ scheduleId: 'schedule-1',
35
+ location: { _id: 'loc-1', name: 'Main Office' },
36
+ availableResources: [{ resources: [{ _id: 'staff-1', name: 'John Doe' }] }],
37
+ ...overrides,
38
+ };
39
+ }
40
+ export function createTimeSlots(count) {
41
+ return Array.from({ length: count }, (_, i) => createTimeSlot({
42
+ localStartDate: `2024-01-15T${String(9 + (i % 8)).padStart(2, '0')}:00:00`,
43
+ localEndDate: `2024-01-15T${String(10 + (i % 8)).padStart(2, '0')}:00:00`,
44
+ scheduleId: `schedule-${i}`,
45
+ }));
46
+ }
47
+ export function createTimeSlotListConfig(overrides = {}) {
48
+ const { startDate = new Date('2024-01-15T00:00:00'), endDate = new Date('2024-01-22T00:00:00'), timezone = 'America/New_York', timeSlots, ...rest } = overrides;
49
+ return {
50
+ startDate,
51
+ endDate,
52
+ timezone,
53
+ ...rest,
54
+ ...(timeSlots !== undefined ? { timeSlots } : {}),
55
+ };
56
+ }
57
+ export const mockTimeSlot = createTimeSlot();
58
+ export const mockTimeSlotMultipleStaff = createTimeSlot({
59
+ availableResources: [
60
+ {
61
+ resources: [
62
+ { _id: 'staff-1', name: 'John Doe' },
63
+ { _id: 'staff-2', name: 'Jane Smith' },
64
+ ],
65
+ },
66
+ ],
67
+ });
68
+ export const mockTimeSlotNotBookable = createTimeSlot({
69
+ localStartDate: '2024-01-15T14:00:00',
70
+ localEndDate: '2024-01-15T15:00:00',
71
+ bookable: false,
72
+ remainingCapacity: 0,
73
+ location: { _id: 'loc-2', name: 'Downtown Branch' },
74
+ });
75
+ export const mockTimeSlots = [
76
+ mockTimeSlot,
77
+ mockTimeSlotMultipleStaff,
78
+ mockTimeSlotNotBookable,
79
+ ];
80
+ export const mockStartDate = new Date('2024-01-15T00:00:00');
81
+ export const mockEndDate = new Date('2024-01-22T00:00:00');
82
+ export const mockTimezone = 'America/New_York';
83
+ export const mockTimeSlotListConfig = createTimeSlotListConfig({
84
+ timeSlots: mockTimeSlots,
85
+ });
86
+ export const mockEmptyTimeSlotListConfig = createTimeSlotListConfig();
87
+ export const mockCursorPagingMetadataWithMore = {
88
+ cursors: { next: 'next-cursor-token' },
89
+ };
90
+ export const mockCursorPagingMetadataNoMore = { cursors: {} };
91
+ export function createMockTimeSlotListService(config = {}) {
92
+ const timeSlotsSignal = mockSignal(config.timeSlots || []);
93
+ const isLoadingSignal = mockSignal(false);
94
+ const errorSignal = mockSignal(null);
95
+ const hasMoreSignal = mockSignal(false);
96
+ const dateRangeSignal = mockSignal({
97
+ start: config.startDate || mockStartDate,
98
+ end: config.endDate || mockEndDate,
99
+ });
100
+ return {
101
+ timeSlots: timeSlotsSignal,
102
+ isLoading: isLoadingSignal,
103
+ error: errorSignal,
104
+ hasMore: hasMoreSignal,
105
+ dateRange: dateRangeSignal,
106
+ actions: {
107
+ selectSlot: vi.fn(),
108
+ selectStaffMember: vi.fn(),
109
+ clearStaffSelection: vi.fn(),
110
+ loadMore: vi.fn(),
111
+ setDateRange: vi.fn((start, end) => {
112
+ dateRangeSignal.set({ start, end });
113
+ }),
114
+ },
115
+ };
116
+ }
@@ -9,6 +9,7 @@ export { fetchAvailability } from './fetch-availability/index.js';
9
9
  export { createBooking, type CreateBookingOptions, type Booking, type CreateBookingRequest, type CreateBookingResponse, type ParticipantNotification, type ContactDetails, } from './create-booking/index.js';
10
10
  export { createCheckout, ChannelType, type CreateCheckoutRequest, type CreateCheckoutResponse, } from './create-checkout/index.js';
11
11
  export { queryLocations, type QueryLocationsResult, type LocationData, } from './query-locations/index.js';
12
+ export { queryCategories, type QueryCategoriesResult, type Category, } from './query-categories/index.js';
12
13
  /**
13
14
  * PagingMetadata includes:
14
15
  * - limit: Number of items per page
package/dist/api/index.js CHANGED
@@ -9,6 +9,7 @@ export { fetchAvailability } from './fetch-availability/index.js';
9
9
  export { createBooking, } from './create-booking/index.js';
10
10
  export { createCheckout, ChannelType, } from './create-checkout/index.js';
11
11
  export { queryLocations, } from './query-locations/index.js';
12
+ export { queryCategories, } from './query-categories/index.js';
12
13
  /**
13
14
  * PagingMetadata includes:
14
15
  * - limit: Number of items per page
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Query Categories API
3
+ * Fetches booking service categories from Wix Bookings
4
+ *
5
+ * @module API/QueryCategories
6
+ */
7
+ import { categoriesV2 } from '@wix/bookings';
8
+ /**
9
+ * Category type from the Wix Bookings Categories V2 API
10
+ */
11
+ export type Category = categoriesV2.Category;
12
+ /**
13
+ * Result from querying categories
14
+ */
15
+ export interface QueryCategoriesResult {
16
+ /** Array of booking service categories */
17
+ categories: Category[];
18
+ }
19
+ /**
20
+ * Query all booking service categories.
21
+ * Returns an array of categories sorted by creation date (ascending).
22
+ *
23
+ * @returns Promise resolving to categories array
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const { categories } = await queryCategories();
28
+ *
29
+ * categories.forEach(category => {
30
+ * console.log(category.name, category._id);
31
+ * });
32
+ * ```
33
+ */
34
+ export declare function queryCategories(): Promise<QueryCategoriesResult>;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Query Categories API
3
+ * Fetches booking service categories from Wix Bookings
4
+ *
5
+ * @module API/QueryCategories
6
+ */
7
+ import { categoriesV2 } from '@wix/bookings';
8
+ /**
9
+ * Query all booking service categories.
10
+ * Returns an array of categories sorted by creation date (ascending).
11
+ *
12
+ * @returns Promise resolving to categories array
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const { categories } = await queryCategories();
17
+ *
18
+ * categories.forEach(category => {
19
+ * console.log(category.name, category._id);
20
+ * });
21
+ * ```
22
+ */
23
+ export async function queryCategories() {
24
+ const result = await categoriesV2.queryCategories().find();
25
+ return {
26
+ categories: result.items || [],
27
+ };
28
+ }
@@ -74,8 +74,15 @@ const buildServicesQueryRequest = ({ appId, filters, paging, sort, }) => {
74
74
  if (filters?.serviceIds && filters.serviceIds.length > 0) {
75
75
  filter['_id'] = { $in: filters.serviceIds };
76
76
  }
77
- // Add other filters as they are implemented in future phases
78
- // categoryIds, locationIds, staffMemberIds to be added later
77
+ // Add categoryIds filter if provided
78
+ if (filters?.categoryIds && filters.categoryIds.length > 0) {
79
+ filter['category.id'] = { $in: filters.categoryIds };
80
+ }
81
+ // Add locationIds filter if provided
82
+ if (filters?.locationIds && filters.locationIds.length > 0) {
83
+ filter['locations.business.id'] = { $hasSome: filters.locationIds };
84
+ }
85
+ // staffMemberIds to be added later
79
86
  const query = {
80
87
  filter: Object.keys(filter).length > 0 ? filter : undefined,
81
88
  paging: paging || {