@wix/headless-bookings 0.0.32 → 0.0.34
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/__mocks__/booking.d.ts +23 -17
- package/cjs/dist/__mocks__/booking.js +38 -34
- package/cjs/dist/react/core/booking/Booking.d.ts +3 -2
- package/cjs/dist/react/core/time-slot-list/TimeSlot.js +3 -4
- package/cjs/dist/services/booking/book-action/bookAction.js +3 -2
- package/cjs/dist/services/booking/book-action/buildBookingRequest.js +6 -4
- package/cjs/dist/services/booking/book-action/types.d.ts +3 -2
- package/cjs/dist/services/booking/booking.d.ts +9 -9
- package/cjs/dist/services/index.d.ts +1 -1
- package/cjs/dist/services/time-slot-list/time-slot-list.js +80 -89
- package/dist/__mocks__/booking.d.ts +23 -17
- package/dist/__mocks__/booking.js +38 -34
- package/dist/react/core/booking/Booking.d.ts +3 -2
- package/dist/react/core/time-slot-list/TimeSlot.js +3 -4
- package/dist/services/booking/book-action/bookAction.js +3 -2
- package/dist/services/booking/book-action/buildBookingRequest.js +6 -4
- package/dist/services/booking/book-action/types.d.ts +3 -2
- package/dist/services/booking/booking.d.ts +9 -9
- package/dist/services/index.d.ts +1 -1
- package/dist/services/time-slot-list/time-slot-list.js +80 -89
- package/package.json +2 -2
|
@@ -3,15 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { Resource } from '@wix/auto_sdk_bookings_availability-time-slots';
|
|
5
5
|
import type { FormValues } from '@wix/form-public';
|
|
6
|
-
import type { ServiceSelection, ServiceSelectionTimeSlot,
|
|
7
|
-
/**
|
|
8
|
-
* Mock time slot data
|
|
9
|
-
*/
|
|
10
|
-
export declare const mockTimeSlot: ServiceSelectionTimeSlot;
|
|
11
|
-
/**
|
|
12
|
-
* Mock time slot for afternoon
|
|
13
|
-
*/
|
|
14
|
-
export declare const mockTimeSlotAfternoon: ServiceSelectionTimeSlot;
|
|
6
|
+
import type { ServiceSelection, ServiceSelectionTimeSlot, BookingServiceConfig } from '../services/booking/booking.js';
|
|
15
7
|
/**
|
|
16
8
|
* Mock resource (staff member)
|
|
17
9
|
*/
|
|
@@ -24,6 +16,28 @@ export declare const mockResourceTwo: Resource;
|
|
|
24
16
|
* Mock resource (room)
|
|
25
17
|
*/
|
|
26
18
|
export declare const mockResourceRoom: Resource;
|
|
19
|
+
/**
|
|
20
|
+
* Mock location
|
|
21
|
+
*/
|
|
22
|
+
export declare const mockLocation: {
|
|
23
|
+
_id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Mock location (second)
|
|
28
|
+
*/
|
|
29
|
+
export declare const mockLocationTwo: {
|
|
30
|
+
_id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Mock time slot data
|
|
35
|
+
*/
|
|
36
|
+
export declare const mockTimeSlot: ServiceSelectionTimeSlot;
|
|
37
|
+
/**
|
|
38
|
+
* Mock time slot for afternoon
|
|
39
|
+
*/
|
|
40
|
+
export declare const mockTimeSlotAfternoon: ServiceSelectionTimeSlot;
|
|
27
41
|
/**
|
|
28
42
|
* Mock service selection with all data
|
|
29
43
|
*/
|
|
@@ -36,14 +50,6 @@ export declare const mockServiceSelectionWithoutTimeSlot: ServiceSelection;
|
|
|
36
50
|
* Mock service selection for class
|
|
37
51
|
*/
|
|
38
52
|
export declare const mockServiceSelectionClass: ServiceSelection;
|
|
39
|
-
/**
|
|
40
|
-
* Mock location
|
|
41
|
-
*/
|
|
42
|
-
export declare const mockLocation: Location;
|
|
43
|
-
/**
|
|
44
|
-
* Mock location (second)
|
|
45
|
-
*/
|
|
46
|
-
export declare const mockLocationTwo: Location;
|
|
47
53
|
/**
|
|
48
54
|
* Mock form submission (contact info)
|
|
49
55
|
*/
|
|
@@ -2,26 +2,6 @@
|
|
|
2
2
|
* Shared mock data for booking service tests
|
|
3
3
|
*/
|
|
4
4
|
import { mockService, mockServiceClass } from './services.js';
|
|
5
|
-
/**
|
|
6
|
-
* Mock time slot data
|
|
7
|
-
*/
|
|
8
|
-
export const mockTimeSlot = {
|
|
9
|
-
startDate: '2024-03-15T10:00:00Z',
|
|
10
|
-
endDate: '2024-03-15T11:00:00Z',
|
|
11
|
-
totalCapacity: 10,
|
|
12
|
-
remainingCapacity: 5,
|
|
13
|
-
scheduleId: 'schedule-123',
|
|
14
|
-
};
|
|
15
|
-
/**
|
|
16
|
-
* Mock time slot for afternoon
|
|
17
|
-
*/
|
|
18
|
-
export const mockTimeSlotAfternoon = {
|
|
19
|
-
startDate: '2024-03-15T14:00:00Z',
|
|
20
|
-
endDate: '2024-03-15T15:00:00Z',
|
|
21
|
-
totalCapacity: 10,
|
|
22
|
-
remainingCapacity: 8,
|
|
23
|
-
scheduleId: 'schedule-456',
|
|
24
|
-
};
|
|
25
5
|
/**
|
|
26
6
|
* Mock resource (staff member)
|
|
27
7
|
*/
|
|
@@ -48,6 +28,44 @@ export const mockResourceRoom = {
|
|
|
48
28
|
name: 'Conference Room A',
|
|
49
29
|
tag: 'RESOURCE',
|
|
50
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* Mock location
|
|
33
|
+
*/
|
|
34
|
+
export const mockLocation = {
|
|
35
|
+
_id: 'location-1',
|
|
36
|
+
name: 'Main Office',
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Mock location (second)
|
|
40
|
+
*/
|
|
41
|
+
export const mockLocationTwo = {
|
|
42
|
+
_id: 'location-2',
|
|
43
|
+
name: 'Downtown Branch',
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Mock time slot data
|
|
47
|
+
*/
|
|
48
|
+
export const mockTimeSlot = {
|
|
49
|
+
startDate: '2024-03-15T10:00:00Z',
|
|
50
|
+
endDate: '2024-03-15T11:00:00Z',
|
|
51
|
+
totalCapacity: 10,
|
|
52
|
+
remainingCapacity: 5,
|
|
53
|
+
scheduleId: 'schedule-123',
|
|
54
|
+
resources: [mockResource],
|
|
55
|
+
location: mockLocation,
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Mock time slot for afternoon
|
|
59
|
+
*/
|
|
60
|
+
export const mockTimeSlotAfternoon = {
|
|
61
|
+
startDate: '2024-03-15T14:00:00Z',
|
|
62
|
+
endDate: '2024-03-15T15:00:00Z',
|
|
63
|
+
totalCapacity: 10,
|
|
64
|
+
remainingCapacity: 8,
|
|
65
|
+
scheduleId: 'schedule-456',
|
|
66
|
+
resources: [mockResourceTwo],
|
|
67
|
+
location: mockLocationTwo,
|
|
68
|
+
};
|
|
51
69
|
/**
|
|
52
70
|
* Mock service selection with all data
|
|
53
71
|
*/
|
|
@@ -77,20 +95,6 @@ export const mockServiceSelectionClass = {
|
|
|
77
95
|
timeSlot: mockTimeSlotAfternoon,
|
|
78
96
|
totalParticipants: 3,
|
|
79
97
|
};
|
|
80
|
-
/**
|
|
81
|
-
* Mock location
|
|
82
|
-
*/
|
|
83
|
-
export const mockLocation = {
|
|
84
|
-
id: 'location-1',
|
|
85
|
-
name: 'Main Office',
|
|
86
|
-
};
|
|
87
|
-
/**
|
|
88
|
-
* Mock location (second)
|
|
89
|
-
*/
|
|
90
|
-
export const mockLocationTwo = {
|
|
91
|
-
id: 'location-2',
|
|
92
|
-
name: 'Downtown Branch',
|
|
93
|
-
};
|
|
94
98
|
/**
|
|
95
99
|
* Mock form submission (contact info)
|
|
96
100
|
*/
|
|
@@ -3,8 +3,9 @@
|
|
|
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 ServiceSelection, type
|
|
6
|
+
import { type BookingServiceConfig, type ServiceSelection, type BookingServiceActions } from '../../../services/booking/booking.js';
|
|
7
7
|
import type { FormValues } from '@wix/form-public';
|
|
8
|
+
import type { Location as TimeSlotLocationType } from '@wix/auto_sdk_bookings_availability-time-slots';
|
|
8
9
|
/**
|
|
9
10
|
* Props for Booking.Root component
|
|
10
11
|
*/
|
|
@@ -34,7 +35,7 @@ export declare function Root(props: RootProps): React.ReactNode;
|
|
|
34
35
|
*/
|
|
35
36
|
export interface BookingData {
|
|
36
37
|
serviceSelections: ServiceSelection[];
|
|
37
|
-
location:
|
|
38
|
+
location: TimeSlotLocationType | null;
|
|
38
39
|
timezone: string | undefined;
|
|
39
40
|
formSubmission: FormValues | null;
|
|
40
41
|
actions: BookingServiceActions;
|
|
@@ -31,9 +31,8 @@ export function TimeSlotProvider(props) {
|
|
|
31
31
|
const service = useService(TimeSlotListServiceDefinition);
|
|
32
32
|
const bookingService = useService(BookingServiceDefinition);
|
|
33
33
|
const selectedTimeSlot = bookingService.serviceSelections.get()[0]?.timeSlot;
|
|
34
|
-
const selectedLocationId = bookingService.location.get()?.id;
|
|
35
34
|
const isSelected = selectedTimeSlot?.startDate === timeSlot.localStartDate &&
|
|
36
|
-
|
|
35
|
+
selectedTimeSlot?.location?._id === timeSlot.location?._id;
|
|
37
36
|
const selectTimeSlot = () => {
|
|
38
37
|
service.actions.selectSlot(timeSlot);
|
|
39
38
|
};
|
|
@@ -132,7 +131,7 @@ export function Actions(props) {
|
|
|
132
131
|
const service = useService(TimeSlotListServiceDefinition);
|
|
133
132
|
const bookingService = useService(BookingServiceDefinition);
|
|
134
133
|
const bookable = timeSlot.bookable ?? false;
|
|
135
|
-
const selectedStaffMemberId = bookingService.serviceSelections.get()[0]?.resources?.[0]?._id;
|
|
134
|
+
const selectedStaffMemberId = bookingService.serviceSelections.get()[0]?.timeSlot?.resources?.[0]?._id;
|
|
136
135
|
const staffFromSlot = timeSlot.availableResources[0].resources.map((r) => r._id);
|
|
137
136
|
const shouldShowClearStaffSelection = !!selectedStaffMemberId && staffFromSlot.length > 1;
|
|
138
137
|
const clearStaffSelection = shouldShowClearStaffSelection
|
|
@@ -169,7 +168,7 @@ export function StaffMembers(props) {
|
|
|
169
168
|
const { timeSlot } = useTimeSlotContext();
|
|
170
169
|
const service = useService(TimeSlotListServiceDefinition);
|
|
171
170
|
const bookingService = useService(BookingServiceDefinition);
|
|
172
|
-
const selectedStaffMemberId = bookingService.serviceSelections.get()[0]?.resources?.[0]?._id;
|
|
171
|
+
const selectedStaffMemberId = bookingService.serviceSelections.get()[0]?.timeSlot?.resources?.[0]?._id;
|
|
173
172
|
const staffMembers = timeSlot.availableResources[0].resources;
|
|
174
173
|
// Return null if only 1 staff member - nothing to choose from
|
|
175
174
|
if (staffMembers.length <= 1) {
|
|
@@ -43,8 +43,9 @@ export async function executeBookAction(params) {
|
|
|
43
43
|
throw new Error('Failed to create booking');
|
|
44
44
|
}
|
|
45
45
|
// Step 2: Create checkout
|
|
46
|
-
// businessLocationId
|
|
47
|
-
const
|
|
46
|
+
// businessLocationId: prefer timeSlot.location, fallback to global location
|
|
47
|
+
const firstSelection = params.serviceSelections[0];
|
|
48
|
+
const businessLocationId = firstSelection?.timeSlot?.location?._id ?? undefined;
|
|
48
49
|
const checkoutRequest = buildCheckoutRequest({
|
|
49
50
|
bookingId,
|
|
50
51
|
contactDetails,
|
|
@@ -46,8 +46,10 @@ export function buildBookingRequest(params) {
|
|
|
46
46
|
if (!item.timeSlot) {
|
|
47
47
|
throw new Error('No time slot selected for service selection');
|
|
48
48
|
}
|
|
49
|
-
// Get resource
|
|
50
|
-
const resource = item.resources?.[0];
|
|
49
|
+
// Get resource: prefer timeSlot.resources, fallback to item.resources
|
|
50
|
+
const resource = item.timeSlot.resources?.[0] ?? item.resources?.[0];
|
|
51
|
+
// Get location: prefer timeSlot.location, fallback to params.location
|
|
52
|
+
const slotLocation = item.timeSlot.location ?? location;
|
|
51
53
|
return {
|
|
52
54
|
booking: {
|
|
53
55
|
// Payment option derived from service payment settings
|
|
@@ -66,8 +68,8 @@ export function buildBookingRequest(params) {
|
|
|
66
68
|
resource: resource
|
|
67
69
|
? { _id: resource._id, name: resource.name }
|
|
68
70
|
: undefined,
|
|
69
|
-
location:
|
|
70
|
-
? { _id:
|
|
71
|
+
location: slotLocation
|
|
72
|
+
? { _id: slotLocation._id, name: slotLocation.name }
|
|
71
73
|
: undefined,
|
|
72
74
|
},
|
|
73
75
|
},
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Types for the Book action
|
|
3
3
|
*/
|
|
4
|
-
import type { ServiceSelection
|
|
4
|
+
import type { ServiceSelection } from '../booking.js';
|
|
5
5
|
import type { FormValues } from '@wix/form-public';
|
|
6
|
+
import type { Location as TimeSlotLocationType } from '@wix/auto_sdk_bookings_availability-time-slots';
|
|
6
7
|
/**
|
|
7
8
|
* Result returned from the book action
|
|
8
9
|
*/
|
|
@@ -21,7 +22,7 @@ export interface BookingError {
|
|
|
21
22
|
*/
|
|
22
23
|
export interface BookActionParams {
|
|
23
24
|
serviceSelections: ServiceSelection[];
|
|
24
|
-
location:
|
|
25
|
+
location: TimeSlotLocationType | null;
|
|
25
26
|
formSubmission: FormValues | null;
|
|
26
27
|
timezone?: string;
|
|
27
28
|
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Persists across page navigation in SPA until page refresh
|
|
5
5
|
*/
|
|
6
6
|
import { type Signal } from '@wix/services-definitions/core-services/signals';
|
|
7
|
-
import type { Service } from '@wix/auto_sdk_bookings_services';
|
|
8
|
-
import type { Resource, EventInfo } from '@wix/auto_sdk_bookings_availability-time-slots';
|
|
7
|
+
import type { Service, Location as ServiceLocationType } from '@wix/auto_sdk_bookings_services';
|
|
8
|
+
import type { Resource, EventInfo, Location as TimeSlotLocationType } from '@wix/auto_sdk_bookings_availability-time-slots';
|
|
9
9
|
import type { FormValues } from '@wix/form-public';
|
|
10
10
|
import type { BookResult } from './book-action/types.js';
|
|
11
11
|
export declare const ANY_STAFF_MEMBER_ID = "__any_staff_member__";
|
|
@@ -19,6 +19,10 @@ export interface ServiceSelectionTimeSlot {
|
|
|
19
19
|
remainingCapacity?: number;
|
|
20
20
|
scheduleId?: string;
|
|
21
21
|
eventInfo?: EventInfo;
|
|
22
|
+
/** Resources available in this time slot (populated when slot is selected) */
|
|
23
|
+
resources?: Resource[];
|
|
24
|
+
/** Location of this time slot (populated when slot is selected) */
|
|
25
|
+
location?: TimeSlotLocationType;
|
|
22
26
|
}
|
|
23
27
|
/**
|
|
24
28
|
* ServiceSelection contains the service AND its related selections
|
|
@@ -35,10 +39,6 @@ export interface ServiceSelection {
|
|
|
35
39
|
/** Number of participants for this service selection */
|
|
36
40
|
totalParticipants?: number;
|
|
37
41
|
}
|
|
38
|
-
export interface Location {
|
|
39
|
-
id: string;
|
|
40
|
-
name?: string;
|
|
41
|
-
}
|
|
42
42
|
/**
|
|
43
43
|
* BookingService actions - setters only, no API calls
|
|
44
44
|
*/
|
|
@@ -57,7 +57,7 @@ export interface BookingServiceActions {
|
|
|
57
57
|
setResourceById(resourceId: string, resource: Resource, instanceId?: string): void;
|
|
58
58
|
/** Gets a resource by ID from a service selection. If no instanceId provided, uses the first item. */
|
|
59
59
|
getResourceById(resourceId: string, instanceId?: string): Resource | undefined;
|
|
60
|
-
setLocation(location:
|
|
60
|
+
setLocation(location: ServiceLocationType | null): void;
|
|
61
61
|
clearLocation(): void;
|
|
62
62
|
setTimezone(timezone: string): void;
|
|
63
63
|
setFormSubmission(values: FormValues | null): void;
|
|
@@ -73,7 +73,7 @@ export interface BookingService {
|
|
|
73
73
|
/** Signal containing the service selections (services with their related data) */
|
|
74
74
|
serviceSelections: Signal<ServiceSelection[]>;
|
|
75
75
|
/** Signal containing the selected location */
|
|
76
|
-
location: Signal<
|
|
76
|
+
location: Signal<ServiceLocationType | null>;
|
|
77
77
|
/** Signal containing the selected timezone */
|
|
78
78
|
timezone: Signal<string | undefined>;
|
|
79
79
|
/** Signal containing the form submission */
|
|
@@ -97,7 +97,7 @@ export interface BookingServiceConfig {
|
|
|
97
97
|
/** Optional initial service selections to pre-populate */
|
|
98
98
|
serviceSelections?: ServiceSelection[];
|
|
99
99
|
/** Optional initial location */
|
|
100
|
-
location?:
|
|
100
|
+
location?: ServiceLocationType | null;
|
|
101
101
|
/** Optional initial timezone */
|
|
102
102
|
timezone?: string;
|
|
103
103
|
/** Optional initial form submission */
|
|
@@ -3,7 +3,7 @@
|
|
|
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
|
|
6
|
+
export { BookingService, BookingServiceDefinition, type BookingServiceConfig, type BookingServiceActions, type ServiceSelection, type ServiceSelectionTimeSlot, } from './booking/booking.js';
|
|
7
7
|
export { TimeSlotListService, TimeSlotListServiceDefinition, ViewMode, loadTimeSlotListConfig, type TimeSlotListServiceAPI, type TimeSlotListServiceActions, type TimeSlotListServiceConfig, type LoadTimeSlotListConfigOptions, type LoadTimeSlotListConfigResult, } from './time-slot-list/index.js';
|
|
8
8
|
export { ServiceListService, ServiceListServiceDefinition, type ServiceListServiceAPI, type ServiceListServiceConfig, type ServiceListFilters, type ServiceListActions, type QueryOptions, loadServiceListServiceInitialData, type SuccessServiceListServiceConfigResult, type ErrorServiceListServiceConfigResult, } from './service-list/service-list.js';
|
|
9
9
|
export { executeBookAction, canBook, buildBookingRequest, buildCheckoutRequest, type CanBookParams, type BookResult, type BookingError, type BookActionParams, type BookChildProps, type BookProps, type BuildCheckoutParams, } from './booking/book-action/index.js';
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Manages time slot fetching, selection, and state
|
|
4
4
|
*/
|
|
5
5
|
import { implementService } from '@wix/services-definitions';
|
|
6
|
-
import { SignalsServiceDefinition } from '@wix/services-definitions/core-services/signals';
|
|
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/fetch-availability/index.js';
|
|
9
9
|
import { BookingServiceDefinition, ANY_STAFF_MEMBER_ID, } from '../booking/booking.js';
|
|
@@ -44,27 +44,58 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
|
|
|
44
44
|
}
|
|
45
45
|
return config.services ?? [];
|
|
46
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* Helper: Get filters combining config with booking overrides
|
|
49
|
+
* Uses location and resources from booking if they exist (not from the slot)
|
|
50
|
+
*/
|
|
51
|
+
const getFilters = () => {
|
|
52
|
+
const baseFilters = config.filters ?? {};
|
|
53
|
+
const selectedStaffMembersIds = bookingService.serviceSelections
|
|
54
|
+
.get()[0]
|
|
55
|
+
?.resources?.map((resource) => resource._id);
|
|
56
|
+
const staffMemberIds = selectedStaffMembersIds?.length
|
|
57
|
+
? selectedStaffMembersIds
|
|
58
|
+
: baseFilters.staffMemberIds;
|
|
59
|
+
const selectedLocation = bookingService.location.get();
|
|
60
|
+
const location = selectedLocation
|
|
61
|
+
? {
|
|
62
|
+
id: selectedLocation._id,
|
|
63
|
+
locationType: selectedLocation.type,
|
|
64
|
+
}
|
|
65
|
+
: baseFilters.location;
|
|
66
|
+
return {
|
|
67
|
+
...baseFilters,
|
|
68
|
+
location,
|
|
69
|
+
staffMemberIds,
|
|
70
|
+
};
|
|
71
|
+
};
|
|
47
72
|
/**
|
|
48
73
|
* Fetch time slots from API
|
|
49
|
-
*
|
|
74
|
+
* If cursor is provided, appends to existing slots (pagination)
|
|
75
|
+
* If no cursor, resets state and fetches fresh slots
|
|
50
76
|
*/
|
|
51
|
-
const fetchTimeSlots = async () => {
|
|
52
|
-
|
|
77
|
+
const fetchTimeSlots = async ({ isLoadMore } = { isLoadMore: false }) => {
|
|
78
|
+
if (isLoadingSignal.get() || (isLoadMore && !cursorSignal.get())) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const cursor = isLoadMore ? cursorSignal.get() : undefined;
|
|
53
82
|
isLoadingSignal.set(true);
|
|
54
83
|
errorSignal.set(null);
|
|
55
|
-
|
|
56
|
-
|
|
84
|
+
if (!isLoadMore) {
|
|
85
|
+
timeSlotsSignal.set([]);
|
|
86
|
+
}
|
|
57
87
|
try {
|
|
58
88
|
const response = await fetchAvailability({
|
|
59
|
-
services:
|
|
89
|
+
services: getServices(),
|
|
60
90
|
fromLocalDate: startDateSignal.get(),
|
|
61
91
|
toLocalDate: endDateSignal.get(),
|
|
62
92
|
timeZone: bookingService.timezone.get(),
|
|
63
93
|
viewMode: viewModeSignal.get(),
|
|
64
|
-
cursor
|
|
65
|
-
...
|
|
94
|
+
cursor,
|
|
95
|
+
...getFilters(),
|
|
66
96
|
});
|
|
67
|
-
|
|
97
|
+
const newSlots = response.timeSlots;
|
|
98
|
+
timeSlotsSignal.set(isLoadMore ? [...timeSlotsSignal.get(), ...newSlots] : newSlots);
|
|
68
99
|
const nextCursor = response.cursorPagingMetadata?.cursors?.next ?? undefined;
|
|
69
100
|
cursorSignal.set(nextCursor);
|
|
70
101
|
hasMoreSignal.set(!!nextCursor);
|
|
@@ -72,7 +103,9 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
|
|
|
72
103
|
catch (error) {
|
|
73
104
|
const message = error instanceof Error
|
|
74
105
|
? error.message
|
|
75
|
-
:
|
|
106
|
+
: isLoadMore
|
|
107
|
+
? 'Failed to load more time slots'
|
|
108
|
+
: 'Failed to load time slots';
|
|
76
109
|
errorSignal.set(message);
|
|
77
110
|
}
|
|
78
111
|
finally {
|
|
@@ -86,26 +119,23 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
|
|
|
86
119
|
* - Auto-selects staff only when there is exactly one staff option in this slot
|
|
87
120
|
*/
|
|
88
121
|
const selectSlot = (slot) => {
|
|
122
|
+
const staffMembersFromSlot = slot.availableResources[0].resources.map((r) => ({
|
|
123
|
+
_id: r._id,
|
|
124
|
+
name: r.name,
|
|
125
|
+
}));
|
|
126
|
+
const resources = staffMembersFromSlot.length === 1 ? staffMembersFromSlot : [];
|
|
89
127
|
const timeSlot = {
|
|
90
128
|
startDate: slot.localStartDate,
|
|
91
129
|
endDate: slot.localEndDate,
|
|
92
130
|
totalCapacity: slot.totalCapacity ?? undefined,
|
|
93
131
|
remainingCapacity: slot.remainingCapacity ?? undefined,
|
|
94
132
|
scheduleId: slot.scheduleId ?? undefined,
|
|
95
|
-
eventInfo: slot.eventInfo
|
|
133
|
+
eventInfo: slot.eventInfo,
|
|
134
|
+
location: slot.location,
|
|
135
|
+
resources,
|
|
96
136
|
};
|
|
97
|
-
bookingService.actions.setLocation({
|
|
98
|
-
id: slot.location?._id,
|
|
99
|
-
name: slot.location?.name ?? undefined,
|
|
100
|
-
});
|
|
101
|
-
const staffMembersFromSlot = slot.availableResources[0].resources.map((r) => ({
|
|
102
|
-
id: r._id,
|
|
103
|
-
name: r.name,
|
|
104
|
-
}));
|
|
105
|
-
const resources = staffMembersFromSlot.length === 1 ? staffMembersFromSlot : [];
|
|
106
137
|
bookingService.actions.updateItem({
|
|
107
138
|
timeSlot,
|
|
108
|
-
resources,
|
|
109
139
|
});
|
|
110
140
|
};
|
|
111
141
|
/**
|
|
@@ -113,82 +143,46 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
|
|
|
113
143
|
* Updates the booking service with the selected resource
|
|
114
144
|
*/
|
|
115
145
|
const selectStaffMember = (staffMemberId) => {
|
|
116
|
-
const
|
|
117
|
-
if (!
|
|
146
|
+
const selectedTimeSlot = bookingService.serviceSelections.get()[0]?.timeSlot;
|
|
147
|
+
if (!selectedTimeSlot)
|
|
118
148
|
return;
|
|
119
|
-
const
|
|
149
|
+
const getStaffName = (id) => {
|
|
150
|
+
if (id === ANY_STAFF_MEMBER_ID)
|
|
151
|
+
return 'Any staff member';
|
|
120
152
|
const slots = timeSlotsSignal.get();
|
|
121
153
|
return (slots
|
|
122
154
|
.flatMap((slot) => slot.availableResources[0].resources)
|
|
123
155
|
.find((r) => r._id === id)?.name ?? undefined);
|
|
124
156
|
};
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
},
|
|
132
|
-
],
|
|
133
|
-
});
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
157
|
+
const resources = [
|
|
158
|
+
{
|
|
159
|
+
_id: staffMemberId,
|
|
160
|
+
name: getStaffName(staffMemberId),
|
|
161
|
+
},
|
|
162
|
+
];
|
|
136
163
|
bookingService.actions.updateItem({
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
},
|
|
142
|
-
],
|
|
164
|
+
timeSlot: {
|
|
165
|
+
...selectedTimeSlot,
|
|
166
|
+
resources,
|
|
167
|
+
},
|
|
143
168
|
});
|
|
144
169
|
};
|
|
145
170
|
/**
|
|
146
171
|
* Action: Clear staff selection while keeping the selected time slot
|
|
147
172
|
*/
|
|
148
173
|
const clearStaffSelection = () => {
|
|
149
|
-
const
|
|
150
|
-
if (!
|
|
174
|
+
const selectedTimeSlot = bookingService.serviceSelections.get()[0]?.timeSlot;
|
|
175
|
+
if (!selectedTimeSlot)
|
|
151
176
|
return;
|
|
152
177
|
bookingService.actions.updateItem({
|
|
153
|
-
resources: [],
|
|
178
|
+
timeSlot: { ...selectedTimeSlot, resources: [] },
|
|
154
179
|
});
|
|
155
180
|
};
|
|
156
181
|
/**
|
|
157
182
|
* Action: Load more slots (pagination)
|
|
158
183
|
*/
|
|
159
184
|
const loadMore = async () => {
|
|
160
|
-
|
|
161
|
-
if (!cursor || isLoadingSignal.get())
|
|
162
|
-
return;
|
|
163
|
-
const selectedServices = getServices();
|
|
164
|
-
isLoadingSignal.set(true);
|
|
165
|
-
try {
|
|
166
|
-
const response = await fetchAvailability({
|
|
167
|
-
services: selectedServices,
|
|
168
|
-
fromLocalDate: startDateSignal.get(),
|
|
169
|
-
toLocalDate: endDateSignal.get(),
|
|
170
|
-
timeZone: bookingService.timezone.get(),
|
|
171
|
-
cursor,
|
|
172
|
-
viewMode: viewModeSignal.get(),
|
|
173
|
-
...config.filters,
|
|
174
|
-
});
|
|
175
|
-
timeSlotsSignal.set([
|
|
176
|
-
...timeSlotsSignal.get(),
|
|
177
|
-
...response.timeSlots,
|
|
178
|
-
]);
|
|
179
|
-
const nextCursor = response.cursorPagingMetadata?.cursors?.next ?? undefined;
|
|
180
|
-
cursorSignal.set(nextCursor);
|
|
181
|
-
hasMoreSignal.set(!!nextCursor);
|
|
182
|
-
}
|
|
183
|
-
catch (error) {
|
|
184
|
-
const message = error instanceof Error
|
|
185
|
-
? error.message
|
|
186
|
-
: 'Failed to load more time slots';
|
|
187
|
-
errorSignal.set(message);
|
|
188
|
-
}
|
|
189
|
-
finally {
|
|
190
|
-
isLoadingSignal.set(false);
|
|
191
|
-
}
|
|
185
|
+
return fetchTimeSlots({ isLoadMore: true });
|
|
192
186
|
};
|
|
193
187
|
/**
|
|
194
188
|
* Action: Update date range and refetch slots
|
|
@@ -202,22 +196,19 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
|
|
|
202
196
|
}
|
|
203
197
|
startDateSignal.set(newStartDate);
|
|
204
198
|
endDateSignal.set(newEndDate);
|
|
205
|
-
fetchTimeSlots();
|
|
206
199
|
};
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
// Subscribe to timezone changes from BookingService to auto-refetch
|
|
212
|
-
// NOTE: This should depend ONLY on bookingService.timezone.
|
|
213
|
-
let firstTimezoneEffectRun = true;
|
|
200
|
+
const resourcesSignal = signalsService.computed(() => {
|
|
201
|
+
return bookingService.serviceSelections.get()[0]?.resources;
|
|
202
|
+
});
|
|
203
|
+
let isFirstRun = true;
|
|
214
204
|
signalsService.effect(() => {
|
|
215
205
|
bookingService.timezone.get();
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
206
|
+
bookingService.location.get();
|
|
207
|
+
resourcesSignal.get();
|
|
208
|
+
if (!isFirstRun || !config.timeSlots) {
|
|
209
|
+
fetchTimeSlots();
|
|
219
210
|
}
|
|
220
|
-
|
|
211
|
+
isFirstRun = false;
|
|
221
212
|
});
|
|
222
213
|
// Return service API
|
|
223
214
|
return {
|
|
@@ -3,15 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { Resource } from '@wix/auto_sdk_bookings_availability-time-slots';
|
|
5
5
|
import type { FormValues } from '@wix/form-public';
|
|
6
|
-
import type { ServiceSelection, ServiceSelectionTimeSlot,
|
|
7
|
-
/**
|
|
8
|
-
* Mock time slot data
|
|
9
|
-
*/
|
|
10
|
-
export declare const mockTimeSlot: ServiceSelectionTimeSlot;
|
|
11
|
-
/**
|
|
12
|
-
* Mock time slot for afternoon
|
|
13
|
-
*/
|
|
14
|
-
export declare const mockTimeSlotAfternoon: ServiceSelectionTimeSlot;
|
|
6
|
+
import type { ServiceSelection, ServiceSelectionTimeSlot, BookingServiceConfig } from '../services/booking/booking.js';
|
|
15
7
|
/**
|
|
16
8
|
* Mock resource (staff member)
|
|
17
9
|
*/
|
|
@@ -24,6 +16,28 @@ export declare const mockResourceTwo: Resource;
|
|
|
24
16
|
* Mock resource (room)
|
|
25
17
|
*/
|
|
26
18
|
export declare const mockResourceRoom: Resource;
|
|
19
|
+
/**
|
|
20
|
+
* Mock location
|
|
21
|
+
*/
|
|
22
|
+
export declare const mockLocation: {
|
|
23
|
+
_id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Mock location (second)
|
|
28
|
+
*/
|
|
29
|
+
export declare const mockLocationTwo: {
|
|
30
|
+
_id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Mock time slot data
|
|
35
|
+
*/
|
|
36
|
+
export declare const mockTimeSlot: ServiceSelectionTimeSlot;
|
|
37
|
+
/**
|
|
38
|
+
* Mock time slot for afternoon
|
|
39
|
+
*/
|
|
40
|
+
export declare const mockTimeSlotAfternoon: ServiceSelectionTimeSlot;
|
|
27
41
|
/**
|
|
28
42
|
* Mock service selection with all data
|
|
29
43
|
*/
|
|
@@ -36,14 +50,6 @@ export declare const mockServiceSelectionWithoutTimeSlot: ServiceSelection;
|
|
|
36
50
|
* Mock service selection for class
|
|
37
51
|
*/
|
|
38
52
|
export declare const mockServiceSelectionClass: ServiceSelection;
|
|
39
|
-
/**
|
|
40
|
-
* Mock location
|
|
41
|
-
*/
|
|
42
|
-
export declare const mockLocation: Location;
|
|
43
|
-
/**
|
|
44
|
-
* Mock location (second)
|
|
45
|
-
*/
|
|
46
|
-
export declare const mockLocationTwo: Location;
|
|
47
53
|
/**
|
|
48
54
|
* Mock form submission (contact info)
|
|
49
55
|
*/
|
|
@@ -2,26 +2,6 @@
|
|
|
2
2
|
* Shared mock data for booking service tests
|
|
3
3
|
*/
|
|
4
4
|
import { mockService, mockServiceClass } from './services.js';
|
|
5
|
-
/**
|
|
6
|
-
* Mock time slot data
|
|
7
|
-
*/
|
|
8
|
-
export const mockTimeSlot = {
|
|
9
|
-
startDate: '2024-03-15T10:00:00Z',
|
|
10
|
-
endDate: '2024-03-15T11:00:00Z',
|
|
11
|
-
totalCapacity: 10,
|
|
12
|
-
remainingCapacity: 5,
|
|
13
|
-
scheduleId: 'schedule-123',
|
|
14
|
-
};
|
|
15
|
-
/**
|
|
16
|
-
* Mock time slot for afternoon
|
|
17
|
-
*/
|
|
18
|
-
export const mockTimeSlotAfternoon = {
|
|
19
|
-
startDate: '2024-03-15T14:00:00Z',
|
|
20
|
-
endDate: '2024-03-15T15:00:00Z',
|
|
21
|
-
totalCapacity: 10,
|
|
22
|
-
remainingCapacity: 8,
|
|
23
|
-
scheduleId: 'schedule-456',
|
|
24
|
-
};
|
|
25
5
|
/**
|
|
26
6
|
* Mock resource (staff member)
|
|
27
7
|
*/
|
|
@@ -48,6 +28,44 @@ export const mockResourceRoom = {
|
|
|
48
28
|
name: 'Conference Room A',
|
|
49
29
|
tag: 'RESOURCE',
|
|
50
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* Mock location
|
|
33
|
+
*/
|
|
34
|
+
export const mockLocation = {
|
|
35
|
+
_id: 'location-1',
|
|
36
|
+
name: 'Main Office',
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Mock location (second)
|
|
40
|
+
*/
|
|
41
|
+
export const mockLocationTwo = {
|
|
42
|
+
_id: 'location-2',
|
|
43
|
+
name: 'Downtown Branch',
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Mock time slot data
|
|
47
|
+
*/
|
|
48
|
+
export const mockTimeSlot = {
|
|
49
|
+
startDate: '2024-03-15T10:00:00Z',
|
|
50
|
+
endDate: '2024-03-15T11:00:00Z',
|
|
51
|
+
totalCapacity: 10,
|
|
52
|
+
remainingCapacity: 5,
|
|
53
|
+
scheduleId: 'schedule-123',
|
|
54
|
+
resources: [mockResource],
|
|
55
|
+
location: mockLocation,
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Mock time slot for afternoon
|
|
59
|
+
*/
|
|
60
|
+
export const mockTimeSlotAfternoon = {
|
|
61
|
+
startDate: '2024-03-15T14:00:00Z',
|
|
62
|
+
endDate: '2024-03-15T15:00:00Z',
|
|
63
|
+
totalCapacity: 10,
|
|
64
|
+
remainingCapacity: 8,
|
|
65
|
+
scheduleId: 'schedule-456',
|
|
66
|
+
resources: [mockResourceTwo],
|
|
67
|
+
location: mockLocationTwo,
|
|
68
|
+
};
|
|
51
69
|
/**
|
|
52
70
|
* Mock service selection with all data
|
|
53
71
|
*/
|
|
@@ -77,20 +95,6 @@ export const mockServiceSelectionClass = {
|
|
|
77
95
|
timeSlot: mockTimeSlotAfternoon,
|
|
78
96
|
totalParticipants: 3,
|
|
79
97
|
};
|
|
80
|
-
/**
|
|
81
|
-
* Mock location
|
|
82
|
-
*/
|
|
83
|
-
export const mockLocation = {
|
|
84
|
-
id: 'location-1',
|
|
85
|
-
name: 'Main Office',
|
|
86
|
-
};
|
|
87
|
-
/**
|
|
88
|
-
* Mock location (second)
|
|
89
|
-
*/
|
|
90
|
-
export const mockLocationTwo = {
|
|
91
|
-
id: 'location-2',
|
|
92
|
-
name: 'Downtown Branch',
|
|
93
|
-
};
|
|
94
98
|
/**
|
|
95
99
|
* Mock form submission (contact info)
|
|
96
100
|
*/
|
|
@@ -3,8 +3,9 @@
|
|
|
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 ServiceSelection, type
|
|
6
|
+
import { type BookingServiceConfig, type ServiceSelection, type BookingServiceActions } from '../../../services/booking/booking.js';
|
|
7
7
|
import type { FormValues } from '@wix/form-public';
|
|
8
|
+
import type { Location as TimeSlotLocationType } from '@wix/auto_sdk_bookings_availability-time-slots';
|
|
8
9
|
/**
|
|
9
10
|
* Props for Booking.Root component
|
|
10
11
|
*/
|
|
@@ -34,7 +35,7 @@ export declare function Root(props: RootProps): React.ReactNode;
|
|
|
34
35
|
*/
|
|
35
36
|
export interface BookingData {
|
|
36
37
|
serviceSelections: ServiceSelection[];
|
|
37
|
-
location:
|
|
38
|
+
location: TimeSlotLocationType | null;
|
|
38
39
|
timezone: string | undefined;
|
|
39
40
|
formSubmission: FormValues | null;
|
|
40
41
|
actions: BookingServiceActions;
|
|
@@ -31,9 +31,8 @@ export function TimeSlotProvider(props) {
|
|
|
31
31
|
const service = useService(TimeSlotListServiceDefinition);
|
|
32
32
|
const bookingService = useService(BookingServiceDefinition);
|
|
33
33
|
const selectedTimeSlot = bookingService.serviceSelections.get()[0]?.timeSlot;
|
|
34
|
-
const selectedLocationId = bookingService.location.get()?.id;
|
|
35
34
|
const isSelected = selectedTimeSlot?.startDate === timeSlot.localStartDate &&
|
|
36
|
-
|
|
35
|
+
selectedTimeSlot?.location?._id === timeSlot.location?._id;
|
|
37
36
|
const selectTimeSlot = () => {
|
|
38
37
|
service.actions.selectSlot(timeSlot);
|
|
39
38
|
};
|
|
@@ -132,7 +131,7 @@ export function Actions(props) {
|
|
|
132
131
|
const service = useService(TimeSlotListServiceDefinition);
|
|
133
132
|
const bookingService = useService(BookingServiceDefinition);
|
|
134
133
|
const bookable = timeSlot.bookable ?? false;
|
|
135
|
-
const selectedStaffMemberId = bookingService.serviceSelections.get()[0]?.resources?.[0]?._id;
|
|
134
|
+
const selectedStaffMemberId = bookingService.serviceSelections.get()[0]?.timeSlot?.resources?.[0]?._id;
|
|
136
135
|
const staffFromSlot = timeSlot.availableResources[0].resources.map((r) => r._id);
|
|
137
136
|
const shouldShowClearStaffSelection = !!selectedStaffMemberId && staffFromSlot.length > 1;
|
|
138
137
|
const clearStaffSelection = shouldShowClearStaffSelection
|
|
@@ -169,7 +168,7 @@ export function StaffMembers(props) {
|
|
|
169
168
|
const { timeSlot } = useTimeSlotContext();
|
|
170
169
|
const service = useService(TimeSlotListServiceDefinition);
|
|
171
170
|
const bookingService = useService(BookingServiceDefinition);
|
|
172
|
-
const selectedStaffMemberId = bookingService.serviceSelections.get()[0]?.resources?.[0]?._id;
|
|
171
|
+
const selectedStaffMemberId = bookingService.serviceSelections.get()[0]?.timeSlot?.resources?.[0]?._id;
|
|
173
172
|
const staffMembers = timeSlot.availableResources[0].resources;
|
|
174
173
|
// Return null if only 1 staff member - nothing to choose from
|
|
175
174
|
if (staffMembers.length <= 1) {
|
|
@@ -43,8 +43,9 @@ export async function executeBookAction(params) {
|
|
|
43
43
|
throw new Error('Failed to create booking');
|
|
44
44
|
}
|
|
45
45
|
// Step 2: Create checkout
|
|
46
|
-
// businessLocationId
|
|
47
|
-
const
|
|
46
|
+
// businessLocationId: prefer timeSlot.location, fallback to global location
|
|
47
|
+
const firstSelection = params.serviceSelections[0];
|
|
48
|
+
const businessLocationId = firstSelection?.timeSlot?.location?._id ?? undefined;
|
|
48
49
|
const checkoutRequest = buildCheckoutRequest({
|
|
49
50
|
bookingId,
|
|
50
51
|
contactDetails,
|
|
@@ -46,8 +46,10 @@ export function buildBookingRequest(params) {
|
|
|
46
46
|
if (!item.timeSlot) {
|
|
47
47
|
throw new Error('No time slot selected for service selection');
|
|
48
48
|
}
|
|
49
|
-
// Get resource
|
|
50
|
-
const resource = item.resources?.[0];
|
|
49
|
+
// Get resource: prefer timeSlot.resources, fallback to item.resources
|
|
50
|
+
const resource = item.timeSlot.resources?.[0] ?? item.resources?.[0];
|
|
51
|
+
// Get location: prefer timeSlot.location, fallback to params.location
|
|
52
|
+
const slotLocation = item.timeSlot.location ?? location;
|
|
51
53
|
return {
|
|
52
54
|
booking: {
|
|
53
55
|
// Payment option derived from service payment settings
|
|
@@ -66,8 +68,8 @@ export function buildBookingRequest(params) {
|
|
|
66
68
|
resource: resource
|
|
67
69
|
? { _id: resource._id, name: resource.name }
|
|
68
70
|
: undefined,
|
|
69
|
-
location:
|
|
70
|
-
? { _id:
|
|
71
|
+
location: slotLocation
|
|
72
|
+
? { _id: slotLocation._id, name: slotLocation.name }
|
|
71
73
|
: undefined,
|
|
72
74
|
},
|
|
73
75
|
},
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Types for the Book action
|
|
3
3
|
*/
|
|
4
|
-
import type { ServiceSelection
|
|
4
|
+
import type { ServiceSelection } from '../booking.js';
|
|
5
5
|
import type { FormValues } from '@wix/form-public';
|
|
6
|
+
import type { Location as TimeSlotLocationType } from '@wix/auto_sdk_bookings_availability-time-slots';
|
|
6
7
|
/**
|
|
7
8
|
* Result returned from the book action
|
|
8
9
|
*/
|
|
@@ -21,7 +22,7 @@ export interface BookingError {
|
|
|
21
22
|
*/
|
|
22
23
|
export interface BookActionParams {
|
|
23
24
|
serviceSelections: ServiceSelection[];
|
|
24
|
-
location:
|
|
25
|
+
location: TimeSlotLocationType | null;
|
|
25
26
|
formSubmission: FormValues | null;
|
|
26
27
|
timezone?: string;
|
|
27
28
|
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Persists across page navigation in SPA until page refresh
|
|
5
5
|
*/
|
|
6
6
|
import { type Signal } from '@wix/services-definitions/core-services/signals';
|
|
7
|
-
import type { Service } from '@wix/auto_sdk_bookings_services';
|
|
8
|
-
import type { Resource, EventInfo } from '@wix/auto_sdk_bookings_availability-time-slots';
|
|
7
|
+
import type { Service, Location as ServiceLocationType } from '@wix/auto_sdk_bookings_services';
|
|
8
|
+
import type { Resource, EventInfo, Location as TimeSlotLocationType } from '@wix/auto_sdk_bookings_availability-time-slots';
|
|
9
9
|
import type { FormValues } from '@wix/form-public';
|
|
10
10
|
import type { BookResult } from './book-action/types.js';
|
|
11
11
|
export declare const ANY_STAFF_MEMBER_ID = "__any_staff_member__";
|
|
@@ -19,6 +19,10 @@ export interface ServiceSelectionTimeSlot {
|
|
|
19
19
|
remainingCapacity?: number;
|
|
20
20
|
scheduleId?: string;
|
|
21
21
|
eventInfo?: EventInfo;
|
|
22
|
+
/** Resources available in this time slot (populated when slot is selected) */
|
|
23
|
+
resources?: Resource[];
|
|
24
|
+
/** Location of this time slot (populated when slot is selected) */
|
|
25
|
+
location?: TimeSlotLocationType;
|
|
22
26
|
}
|
|
23
27
|
/**
|
|
24
28
|
* ServiceSelection contains the service AND its related selections
|
|
@@ -35,10 +39,6 @@ export interface ServiceSelection {
|
|
|
35
39
|
/** Number of participants for this service selection */
|
|
36
40
|
totalParticipants?: number;
|
|
37
41
|
}
|
|
38
|
-
export interface Location {
|
|
39
|
-
id: string;
|
|
40
|
-
name?: string;
|
|
41
|
-
}
|
|
42
42
|
/**
|
|
43
43
|
* BookingService actions - setters only, no API calls
|
|
44
44
|
*/
|
|
@@ -57,7 +57,7 @@ export interface BookingServiceActions {
|
|
|
57
57
|
setResourceById(resourceId: string, resource: Resource, instanceId?: string): void;
|
|
58
58
|
/** Gets a resource by ID from a service selection. If no instanceId provided, uses the first item. */
|
|
59
59
|
getResourceById(resourceId: string, instanceId?: string): Resource | undefined;
|
|
60
|
-
setLocation(location:
|
|
60
|
+
setLocation(location: ServiceLocationType | null): void;
|
|
61
61
|
clearLocation(): void;
|
|
62
62
|
setTimezone(timezone: string): void;
|
|
63
63
|
setFormSubmission(values: FormValues | null): void;
|
|
@@ -73,7 +73,7 @@ export interface BookingService {
|
|
|
73
73
|
/** Signal containing the service selections (services with their related data) */
|
|
74
74
|
serviceSelections: Signal<ServiceSelection[]>;
|
|
75
75
|
/** Signal containing the selected location */
|
|
76
|
-
location: Signal<
|
|
76
|
+
location: Signal<ServiceLocationType | null>;
|
|
77
77
|
/** Signal containing the selected timezone */
|
|
78
78
|
timezone: Signal<string | undefined>;
|
|
79
79
|
/** Signal containing the form submission */
|
|
@@ -97,7 +97,7 @@ export interface BookingServiceConfig {
|
|
|
97
97
|
/** Optional initial service selections to pre-populate */
|
|
98
98
|
serviceSelections?: ServiceSelection[];
|
|
99
99
|
/** Optional initial location */
|
|
100
|
-
location?:
|
|
100
|
+
location?: ServiceLocationType | null;
|
|
101
101
|
/** Optional initial timezone */
|
|
102
102
|
timezone?: string;
|
|
103
103
|
/** Optional initial form submission */
|
package/dist/services/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
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
|
|
6
|
+
export { BookingService, BookingServiceDefinition, type BookingServiceConfig, type BookingServiceActions, type ServiceSelection, type ServiceSelectionTimeSlot, } from './booking/booking.js';
|
|
7
7
|
export { TimeSlotListService, TimeSlotListServiceDefinition, ViewMode, loadTimeSlotListConfig, type TimeSlotListServiceAPI, type TimeSlotListServiceActions, type TimeSlotListServiceConfig, type LoadTimeSlotListConfigOptions, type LoadTimeSlotListConfigResult, } from './time-slot-list/index.js';
|
|
8
8
|
export { ServiceListService, ServiceListServiceDefinition, type ServiceListServiceAPI, type ServiceListServiceConfig, type ServiceListFilters, type ServiceListActions, type QueryOptions, loadServiceListServiceInitialData, type SuccessServiceListServiceConfigResult, type ErrorServiceListServiceConfigResult, } from './service-list/service-list.js';
|
|
9
9
|
export { executeBookAction, canBook, buildBookingRequest, buildCheckoutRequest, type CanBookParams, type BookResult, type BookingError, type BookActionParams, type BookChildProps, type BookProps, type BuildCheckoutParams, } from './booking/book-action/index.js';
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Manages time slot fetching, selection, and state
|
|
4
4
|
*/
|
|
5
5
|
import { implementService } from '@wix/services-definitions';
|
|
6
|
-
import { SignalsServiceDefinition } from '@wix/services-definitions/core-services/signals';
|
|
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/fetch-availability/index.js';
|
|
9
9
|
import { BookingServiceDefinition, ANY_STAFF_MEMBER_ID, } from '../booking/booking.js';
|
|
@@ -44,27 +44,58 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
|
|
|
44
44
|
}
|
|
45
45
|
return config.services ?? [];
|
|
46
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* Helper: Get filters combining config with booking overrides
|
|
49
|
+
* Uses location and resources from booking if they exist (not from the slot)
|
|
50
|
+
*/
|
|
51
|
+
const getFilters = () => {
|
|
52
|
+
const baseFilters = config.filters ?? {};
|
|
53
|
+
const selectedStaffMembersIds = bookingService.serviceSelections
|
|
54
|
+
.get()[0]
|
|
55
|
+
?.resources?.map((resource) => resource._id);
|
|
56
|
+
const staffMemberIds = selectedStaffMembersIds?.length
|
|
57
|
+
? selectedStaffMembersIds
|
|
58
|
+
: baseFilters.staffMemberIds;
|
|
59
|
+
const selectedLocation = bookingService.location.get();
|
|
60
|
+
const location = selectedLocation
|
|
61
|
+
? {
|
|
62
|
+
id: selectedLocation._id,
|
|
63
|
+
locationType: selectedLocation.type,
|
|
64
|
+
}
|
|
65
|
+
: baseFilters.location;
|
|
66
|
+
return {
|
|
67
|
+
...baseFilters,
|
|
68
|
+
location,
|
|
69
|
+
staffMemberIds,
|
|
70
|
+
};
|
|
71
|
+
};
|
|
47
72
|
/**
|
|
48
73
|
* Fetch time slots from API
|
|
49
|
-
*
|
|
74
|
+
* If cursor is provided, appends to existing slots (pagination)
|
|
75
|
+
* If no cursor, resets state and fetches fresh slots
|
|
50
76
|
*/
|
|
51
|
-
const fetchTimeSlots = async () => {
|
|
52
|
-
|
|
77
|
+
const fetchTimeSlots = async ({ isLoadMore } = { isLoadMore: false }) => {
|
|
78
|
+
if (isLoadingSignal.get() || (isLoadMore && !cursorSignal.get())) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const cursor = isLoadMore ? cursorSignal.get() : undefined;
|
|
53
82
|
isLoadingSignal.set(true);
|
|
54
83
|
errorSignal.set(null);
|
|
55
|
-
|
|
56
|
-
|
|
84
|
+
if (!isLoadMore) {
|
|
85
|
+
timeSlotsSignal.set([]);
|
|
86
|
+
}
|
|
57
87
|
try {
|
|
58
88
|
const response = await fetchAvailability({
|
|
59
|
-
services:
|
|
89
|
+
services: getServices(),
|
|
60
90
|
fromLocalDate: startDateSignal.get(),
|
|
61
91
|
toLocalDate: endDateSignal.get(),
|
|
62
92
|
timeZone: bookingService.timezone.get(),
|
|
63
93
|
viewMode: viewModeSignal.get(),
|
|
64
|
-
cursor
|
|
65
|
-
...
|
|
94
|
+
cursor,
|
|
95
|
+
...getFilters(),
|
|
66
96
|
});
|
|
67
|
-
|
|
97
|
+
const newSlots = response.timeSlots;
|
|
98
|
+
timeSlotsSignal.set(isLoadMore ? [...timeSlotsSignal.get(), ...newSlots] : newSlots);
|
|
68
99
|
const nextCursor = response.cursorPagingMetadata?.cursors?.next ?? undefined;
|
|
69
100
|
cursorSignal.set(nextCursor);
|
|
70
101
|
hasMoreSignal.set(!!nextCursor);
|
|
@@ -72,7 +103,9 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
|
|
|
72
103
|
catch (error) {
|
|
73
104
|
const message = error instanceof Error
|
|
74
105
|
? error.message
|
|
75
|
-
:
|
|
106
|
+
: isLoadMore
|
|
107
|
+
? 'Failed to load more time slots'
|
|
108
|
+
: 'Failed to load time slots';
|
|
76
109
|
errorSignal.set(message);
|
|
77
110
|
}
|
|
78
111
|
finally {
|
|
@@ -86,26 +119,23 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
|
|
|
86
119
|
* - Auto-selects staff only when there is exactly one staff option in this slot
|
|
87
120
|
*/
|
|
88
121
|
const selectSlot = (slot) => {
|
|
122
|
+
const staffMembersFromSlot = slot.availableResources[0].resources.map((r) => ({
|
|
123
|
+
_id: r._id,
|
|
124
|
+
name: r.name,
|
|
125
|
+
}));
|
|
126
|
+
const resources = staffMembersFromSlot.length === 1 ? staffMembersFromSlot : [];
|
|
89
127
|
const timeSlot = {
|
|
90
128
|
startDate: slot.localStartDate,
|
|
91
129
|
endDate: slot.localEndDate,
|
|
92
130
|
totalCapacity: slot.totalCapacity ?? undefined,
|
|
93
131
|
remainingCapacity: slot.remainingCapacity ?? undefined,
|
|
94
132
|
scheduleId: slot.scheduleId ?? undefined,
|
|
95
|
-
eventInfo: slot.eventInfo
|
|
133
|
+
eventInfo: slot.eventInfo,
|
|
134
|
+
location: slot.location,
|
|
135
|
+
resources,
|
|
96
136
|
};
|
|
97
|
-
bookingService.actions.setLocation({
|
|
98
|
-
id: slot.location?._id,
|
|
99
|
-
name: slot.location?.name ?? undefined,
|
|
100
|
-
});
|
|
101
|
-
const staffMembersFromSlot = slot.availableResources[0].resources.map((r) => ({
|
|
102
|
-
id: r._id,
|
|
103
|
-
name: r.name,
|
|
104
|
-
}));
|
|
105
|
-
const resources = staffMembersFromSlot.length === 1 ? staffMembersFromSlot : [];
|
|
106
137
|
bookingService.actions.updateItem({
|
|
107
138
|
timeSlot,
|
|
108
|
-
resources,
|
|
109
139
|
});
|
|
110
140
|
};
|
|
111
141
|
/**
|
|
@@ -113,82 +143,46 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
|
|
|
113
143
|
* Updates the booking service with the selected resource
|
|
114
144
|
*/
|
|
115
145
|
const selectStaffMember = (staffMemberId) => {
|
|
116
|
-
const
|
|
117
|
-
if (!
|
|
146
|
+
const selectedTimeSlot = bookingService.serviceSelections.get()[0]?.timeSlot;
|
|
147
|
+
if (!selectedTimeSlot)
|
|
118
148
|
return;
|
|
119
|
-
const
|
|
149
|
+
const getStaffName = (id) => {
|
|
150
|
+
if (id === ANY_STAFF_MEMBER_ID)
|
|
151
|
+
return 'Any staff member';
|
|
120
152
|
const slots = timeSlotsSignal.get();
|
|
121
153
|
return (slots
|
|
122
154
|
.flatMap((slot) => slot.availableResources[0].resources)
|
|
123
155
|
.find((r) => r._id === id)?.name ?? undefined);
|
|
124
156
|
};
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
},
|
|
132
|
-
],
|
|
133
|
-
});
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
157
|
+
const resources = [
|
|
158
|
+
{
|
|
159
|
+
_id: staffMemberId,
|
|
160
|
+
name: getStaffName(staffMemberId),
|
|
161
|
+
},
|
|
162
|
+
];
|
|
136
163
|
bookingService.actions.updateItem({
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
},
|
|
142
|
-
],
|
|
164
|
+
timeSlot: {
|
|
165
|
+
...selectedTimeSlot,
|
|
166
|
+
resources,
|
|
167
|
+
},
|
|
143
168
|
});
|
|
144
169
|
};
|
|
145
170
|
/**
|
|
146
171
|
* Action: Clear staff selection while keeping the selected time slot
|
|
147
172
|
*/
|
|
148
173
|
const clearStaffSelection = () => {
|
|
149
|
-
const
|
|
150
|
-
if (!
|
|
174
|
+
const selectedTimeSlot = bookingService.serviceSelections.get()[0]?.timeSlot;
|
|
175
|
+
if (!selectedTimeSlot)
|
|
151
176
|
return;
|
|
152
177
|
bookingService.actions.updateItem({
|
|
153
|
-
resources: [],
|
|
178
|
+
timeSlot: { ...selectedTimeSlot, resources: [] },
|
|
154
179
|
});
|
|
155
180
|
};
|
|
156
181
|
/**
|
|
157
182
|
* Action: Load more slots (pagination)
|
|
158
183
|
*/
|
|
159
184
|
const loadMore = async () => {
|
|
160
|
-
|
|
161
|
-
if (!cursor || isLoadingSignal.get())
|
|
162
|
-
return;
|
|
163
|
-
const selectedServices = getServices();
|
|
164
|
-
isLoadingSignal.set(true);
|
|
165
|
-
try {
|
|
166
|
-
const response = await fetchAvailability({
|
|
167
|
-
services: selectedServices,
|
|
168
|
-
fromLocalDate: startDateSignal.get(),
|
|
169
|
-
toLocalDate: endDateSignal.get(),
|
|
170
|
-
timeZone: bookingService.timezone.get(),
|
|
171
|
-
cursor,
|
|
172
|
-
viewMode: viewModeSignal.get(),
|
|
173
|
-
...config.filters,
|
|
174
|
-
});
|
|
175
|
-
timeSlotsSignal.set([
|
|
176
|
-
...timeSlotsSignal.get(),
|
|
177
|
-
...response.timeSlots,
|
|
178
|
-
]);
|
|
179
|
-
const nextCursor = response.cursorPagingMetadata?.cursors?.next ?? undefined;
|
|
180
|
-
cursorSignal.set(nextCursor);
|
|
181
|
-
hasMoreSignal.set(!!nextCursor);
|
|
182
|
-
}
|
|
183
|
-
catch (error) {
|
|
184
|
-
const message = error instanceof Error
|
|
185
|
-
? error.message
|
|
186
|
-
: 'Failed to load more time slots';
|
|
187
|
-
errorSignal.set(message);
|
|
188
|
-
}
|
|
189
|
-
finally {
|
|
190
|
-
isLoadingSignal.set(false);
|
|
191
|
-
}
|
|
185
|
+
return fetchTimeSlots({ isLoadMore: true });
|
|
192
186
|
};
|
|
193
187
|
/**
|
|
194
188
|
* Action: Update date range and refetch slots
|
|
@@ -202,22 +196,19 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
|
|
|
202
196
|
}
|
|
203
197
|
startDateSignal.set(newStartDate);
|
|
204
198
|
endDateSignal.set(newEndDate);
|
|
205
|
-
fetchTimeSlots();
|
|
206
199
|
};
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
// Subscribe to timezone changes from BookingService to auto-refetch
|
|
212
|
-
// NOTE: This should depend ONLY on bookingService.timezone.
|
|
213
|
-
let firstTimezoneEffectRun = true;
|
|
200
|
+
const resourcesSignal = signalsService.computed(() => {
|
|
201
|
+
return bookingService.serviceSelections.get()[0]?.resources;
|
|
202
|
+
});
|
|
203
|
+
let isFirstRun = true;
|
|
214
204
|
signalsService.effect(() => {
|
|
215
205
|
bookingService.timezone.get();
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
206
|
+
bookingService.location.get();
|
|
207
|
+
resourcesSignal.get();
|
|
208
|
+
if (!isFirstRun || !config.timeSlots) {
|
|
209
|
+
fetchTimeSlots();
|
|
219
210
|
}
|
|
220
|
-
|
|
211
|
+
isFirstRun = false;
|
|
221
212
|
});
|
|
222
213
|
// Return service API
|
|
223
214
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/headless-bookings",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.34",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -72,5 +72,5 @@
|
|
|
72
72
|
"groupId": "com.wixpress.headless-components"
|
|
73
73
|
}
|
|
74
74
|
},
|
|
75
|
-
"falconPackageHash": "
|
|
75
|
+
"falconPackageHash": "b195003cbc18c0477da02f42dd5e5409c483f2de627832cbdb859424"
|
|
76
76
|
}
|