@wix/headless-bookings 0.0.19 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/dist/api/{time-slot-list → fetch-availability}/index.d.ts +3 -5
- package/cjs/dist/api/{time-slot-list → fetch-availability}/index.js +18 -13
- package/cjs/dist/api/index.d.ts +1 -1
- package/cjs/dist/api/index.js +1 -1
- package/cjs/dist/react/core/time-slot-list/TimeSlot.js +10 -2
- package/cjs/dist/react/time-slot-list/TimeSlotList.d.ts +41 -4
- package/cjs/dist/react/time-slot-list/TimeSlotList.js +20 -5
- package/cjs/dist/services/booking/booking.d.ts +1 -0
- package/cjs/dist/services/booking/booking.js +4 -0
- package/cjs/dist/services/time-slot-list/time-slot-list.def.d.ts +11 -2
- package/cjs/dist/services/time-slot-list/time-slot-list.js +22 -12
- package/dist/api/{time-slot-list → fetch-availability}/index.d.ts +3 -5
- package/dist/api/{time-slot-list → fetch-availability}/index.js +18 -13
- package/dist/api/index.d.ts +1 -1
- package/dist/api/index.js +1 -1
- package/dist/react/core/time-slot-list/TimeSlot.js +10 -2
- package/dist/react/time-slot-list/TimeSlotList.d.ts +41 -4
- package/dist/react/time-slot-list/TimeSlotList.js +20 -5
- package/dist/services/booking/booking.d.ts +1 -0
- package/dist/services/booking/booking.js +4 -0
- package/dist/services/time-slot-list/time-slot-list.def.d.ts +11 -2
- package/dist/services/time-slot-list/time-slot-list.js +22 -12
- package/package.json +4 -3
|
@@ -15,8 +15,6 @@ export interface FetchAvailabilityOptions {
|
|
|
15
15
|
staffMemberIds?: string[];
|
|
16
16
|
cursorPaging?: CursorPaging;
|
|
17
17
|
viewMode?: ViewMode;
|
|
18
|
-
/** Maximum slots to return per day (useful for calendar views) */
|
|
19
|
-
slotsPerDay?: number;
|
|
20
18
|
/** Filter by bookable status. If undefined, returns both bookable and non-bookable */
|
|
21
19
|
bookable?: boolean;
|
|
22
20
|
/** Filter by booking policy violations */
|
|
@@ -54,20 +52,20 @@ export interface FetchAvailabilityResponse {
|
|
|
54
52
|
* // response.timeSlots are grouped by time
|
|
55
53
|
* ```
|
|
56
54
|
*/
|
|
57
|
-
export declare function fetchAvailability({ services: selectedServices, viewMode, fromLocalDate, toLocalDate, timeZone, locations, staffMemberIds,
|
|
55
|
+
export declare function fetchAvailability({ services: selectedServices, viewMode, fromLocalDate, toLocalDate, timeZone, locations, staffMemberIds, cursor, slotsPerPage, bookable, bookingPolicyViolations, }: {
|
|
58
56
|
services: Service[];
|
|
59
57
|
fromLocalDate: string;
|
|
60
58
|
toLocalDate: string;
|
|
61
59
|
timeZone?: string;
|
|
62
60
|
locations?: Location[];
|
|
63
61
|
staffMemberIds?: string[];
|
|
64
|
-
|
|
62
|
+
cursor?: string;
|
|
65
63
|
viewMode?: ViewMode;
|
|
66
|
-
slotsPerDay?: number;
|
|
67
64
|
bookable?: boolean;
|
|
68
65
|
bookingPolicyViolations?: {
|
|
69
66
|
tooEarlyToBook?: boolean;
|
|
70
67
|
tooLateToBook?: boolean;
|
|
71
68
|
bookOnlineDisabled?: boolean;
|
|
72
69
|
};
|
|
70
|
+
slotsPerPage?: number;
|
|
73
71
|
}): Promise<FetchAvailabilityResponse>;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { listAvailabilityTimeSlots, } from '@wix/auto_sdk_bookings_availability-time-slots';
|
|
6
6
|
import { listEventTimeSlots } from '@wix/auto_sdk_bookings_event-time-slots';
|
|
7
7
|
import { ServiceType } from '@wix/auto_sdk_bookings_services';
|
|
8
|
+
import { ANY_STAFF_MEMBER_ID } from '../../services/booking/booking.js';
|
|
8
9
|
import { ViewMode, } from '../../services/time-slot-list/time-slot-list.def.js';
|
|
9
10
|
// ============================================================================
|
|
10
11
|
// Internal Types
|
|
@@ -15,6 +16,7 @@ var FlowType;
|
|
|
15
16
|
FlowType["MULTI_SERVICE"] = "multi_service";
|
|
16
17
|
FlowType["CLASS"] = "class";
|
|
17
18
|
})(FlowType || (FlowType = {}));
|
|
19
|
+
const STAFF_RESOURCE_TYPE_ID = '1cd44cf8-756f-41c3-bd90-3e2ffcaf1155';
|
|
18
20
|
// ============================================================================
|
|
19
21
|
// Grouping Functions
|
|
20
22
|
// ============================================================================
|
|
@@ -102,16 +104,15 @@ function detectFlow(bookingServices, viewMode) {
|
|
|
102
104
|
* // response.timeSlots are grouped by time
|
|
103
105
|
* ```
|
|
104
106
|
*/
|
|
105
|
-
export async function fetchAvailability({ services: selectedServices, viewMode = ViewMode.CLASSIC, fromLocalDate, toLocalDate, timeZone, locations, staffMemberIds,
|
|
107
|
+
export async function fetchAvailability({ services: selectedServices, viewMode = ViewMode.CLASSIC, fromLocalDate, toLocalDate, timeZone, locations, staffMemberIds, cursor, slotsPerPage, bookable, bookingPolicyViolations, }) {
|
|
106
108
|
const flow = detectFlow(selectedServices, viewMode);
|
|
107
109
|
const serviceIds = selectedServices.map((s) => s._id);
|
|
108
110
|
if (flow === FlowType.SINGLE_SERVICE) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const resourceTypes = staffMemberIds?.length
|
|
111
|
+
const isAnyStaffMemberSelected = staffMemberIds?.length === 1 && staffMemberIds[0] === ANY_STAFF_MEMBER_ID;
|
|
112
|
+
const resourceTypes = staffMemberIds?.length && !isAnyStaffMemberSelected
|
|
112
113
|
? [
|
|
113
114
|
{
|
|
114
|
-
resourceTypeId:
|
|
115
|
+
resourceTypeId: STAFF_RESOURCE_TYPE_ID,
|
|
115
116
|
resourceIds: staffMemberIds,
|
|
116
117
|
},
|
|
117
118
|
]
|
|
@@ -122,11 +123,16 @@ export async function fetchAvailability({ services: selectedServices, viewMode =
|
|
|
122
123
|
toLocalDate,
|
|
123
124
|
timeZone,
|
|
124
125
|
locations,
|
|
125
|
-
cursorPaging
|
|
126
|
+
cursorPaging: {
|
|
127
|
+
cursor,
|
|
128
|
+
limit: slotsPerPage,
|
|
129
|
+
},
|
|
126
130
|
resourceTypes,
|
|
127
|
-
timeSlotsPerDay: slotsPerDay,
|
|
128
131
|
bookable,
|
|
129
132
|
bookingPolicyViolations,
|
|
133
|
+
includeResourceTypeIds: isAnyStaffMemberSelected
|
|
134
|
+
? undefined
|
|
135
|
+
: [STAFF_RESOURCE_TYPE_ID],
|
|
130
136
|
});
|
|
131
137
|
return {
|
|
132
138
|
timeSlots: groupTimeSlotsByTime(response.timeSlots || []),
|
|
@@ -152,13 +158,12 @@ export async function fetchAvailability({ services: selectedServices, viewMode =
|
|
|
152
158
|
toLocalDate,
|
|
153
159
|
timeZone,
|
|
154
160
|
serviceIds: serviceIds.length ? serviceIds : undefined,
|
|
155
|
-
cursorPaging
|
|
161
|
+
cursorPaging: {
|
|
162
|
+
cursor,
|
|
163
|
+
limit: slotsPerPage,
|
|
164
|
+
},
|
|
156
165
|
eventFilter: Object.keys(eventFilter).length ? eventFilter : undefined,
|
|
157
|
-
|
|
158
|
-
// bookable=true means only bookable, so includeNonBookable=false
|
|
159
|
-
// bookable=false means only non-bookable, so includeNonBookable=true
|
|
160
|
-
// bookable=undefined means both, so includeNonBookable=true (default)
|
|
161
|
-
includeNonBookable: bookable === undefined ? undefined : !bookable,
|
|
166
|
+
includeNonBookable: bookable ? !bookable : undefined,
|
|
162
167
|
bookingPolicyViolations,
|
|
163
168
|
});
|
|
164
169
|
return {
|
package/cjs/dist/api/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* @module API
|
|
6
6
|
*/
|
|
7
7
|
export { getServiceById, getServiceBySlug, queryServices, type PagingMetadata, type ServiceListFilters, type QueryServicesRequest, } from './query-services/index.js';
|
|
8
|
-
export { fetchAvailability } from './
|
|
8
|
+
export { fetchAvailability } from './fetch-availability/index.js';
|
|
9
9
|
/**
|
|
10
10
|
* PagingMetadata includes:
|
|
11
11
|
* - limit: Number of items per page
|
package/cjs/dist/api/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* @module API
|
|
6
6
|
*/
|
|
7
7
|
export { getServiceById, getServiceBySlug, queryServices, } from './query-services/index.js';
|
|
8
|
-
export { fetchAvailability } from './
|
|
8
|
+
export { fetchAvailability } from './fetch-availability/index.js';
|
|
9
9
|
/**
|
|
10
10
|
* PagingMetadata includes:
|
|
11
11
|
* - limit: Number of items per page
|
|
@@ -9,6 +9,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
9
9
|
import React from 'react';
|
|
10
10
|
import { useService } from '@wix/services-manager-react';
|
|
11
11
|
import { TimeSlotListServiceDefinition, } from '../../../services/time-slot-list/time-slot-list.def.js';
|
|
12
|
+
import { ANY_STAFF_MEMBER_ID } from '../../../services/booking/booking.js';
|
|
12
13
|
const TimeSlotContext = React.createContext(null);
|
|
13
14
|
/**
|
|
14
15
|
* Hook to access TimeSlot context
|
|
@@ -202,12 +203,13 @@ export function Locations(props) {
|
|
|
202
203
|
const selection = service.selection.get();
|
|
203
204
|
const selectedLocationId = selection?.locationId ?? null;
|
|
204
205
|
const selectedStaffMemberId = selection?.staffMemberId ?? null;
|
|
206
|
+
const isAnyStaffMemberSelected = selectedStaffMemberId === ANY_STAFF_MEMBER_ID;
|
|
205
207
|
// Build locations array from staffByLocation (names already included)
|
|
206
208
|
const locations = timeSlot.staffByLocation.map(({ location, staffMembers }) => {
|
|
207
209
|
// Check if this location is selectable based on selected staff
|
|
208
210
|
// If no staff selected, all locations are selectable
|
|
209
211
|
// If staff selected, only locations where that staff works are selectable
|
|
210
|
-
const isSelectable = selectedStaffMemberId
|
|
212
|
+
const isSelectable = !isAnyStaffMemberSelected && selectedStaffMemberId
|
|
211
213
|
? staffMembers.some((s) => s.id === selectedStaffMemberId)
|
|
212
214
|
: true;
|
|
213
215
|
return {
|
|
@@ -311,12 +313,18 @@ export function StaffMembers(props) {
|
|
|
311
313
|
isSelectable: !selectedLocationId ||
|
|
312
314
|
entries.some((e) => e.locationId === selectedLocationId),
|
|
313
315
|
}));
|
|
316
|
+
const anyStaffOption = {
|
|
317
|
+
id: ANY_STAFF_MEMBER_ID,
|
|
318
|
+
name: 'Any staff member',
|
|
319
|
+
isSelected: selectedStaffMemberId === ANY_STAFF_MEMBER_ID,
|
|
320
|
+
isSelectable: true,
|
|
321
|
+
};
|
|
314
322
|
// Return null if only 1 staff member - nothing to choose from
|
|
315
323
|
if (staffMembers.length <= 1) {
|
|
316
324
|
return null;
|
|
317
325
|
}
|
|
318
326
|
return props.children({
|
|
319
|
-
staffMembers,
|
|
327
|
+
staffMembers: [anyStaffOption, ...staffMembers],
|
|
320
328
|
selectStaffMember: service.actions.selectStaffMember,
|
|
321
329
|
});
|
|
322
330
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import React from 'react';
|
|
6
6
|
import { ListVariant } from '@wix/headless-components/react';
|
|
7
7
|
import { AsChildChildren } from '@wix/headless-utils/react';
|
|
8
|
-
import type { TimeSlotListServiceConfig } from '../../services/time-slot-list/time-slot-list.def.js';
|
|
8
|
+
import type { TimeSlotListServiceConfig, TimeSlot as TimeSlotType } from '../../services/time-slot-list/time-slot-list.def.js';
|
|
9
9
|
export type { StartDateProps as TimeSlotStartDateProps, EndDateProps as TimeSlotEndDateProps, DurationProps as TimeSlotDurationProps, SelectProps as TimeSlotSelectProps, LocationsProps as TimeSlotLocationsProps, LocationRepeaterProps as TimeSlotLocationRepeaterProps, LocationNameProps as TimeSlotLocationNameProps, SelectLocationProps as TimeSlotSelectLocationProps, StaffMembersProps as TimeSlotStaffMembersProps, StaffMemberRepeaterProps as TimeSlotStaffMemberRepeaterProps, StaffMemberNameProps as TimeSlotStaffMemberNameProps, SelectStaffMemberProps as TimeSlotSelectStaffMemberProps, } from './TimeSlot.js';
|
|
10
10
|
/**
|
|
11
11
|
* Render props for TimeSlotList.Timezone component
|
|
@@ -81,17 +81,23 @@ export declare const Root: React.ForwardRefExoticComponent<RootProps & React.Ref
|
|
|
81
81
|
*/
|
|
82
82
|
export interface TimeSlotsProps {
|
|
83
83
|
children: React.ReactNode;
|
|
84
|
+
/** Content to display when no time slots are available */
|
|
84
85
|
emptyState?: React.ReactNode;
|
|
86
|
+
/** Content to display while time slots are loading */
|
|
87
|
+
loadingState?: React.ReactNode;
|
|
85
88
|
className?: string;
|
|
86
89
|
}
|
|
87
90
|
/**
|
|
88
|
-
* Container for the time slots list with empty state support.
|
|
91
|
+
* Container for the time slots list with empty state and loading state support.
|
|
89
92
|
* Wraps GenericList.Items. Follows List Container Level pattern.
|
|
90
93
|
*
|
|
91
94
|
* @component
|
|
92
95
|
* @example
|
|
93
96
|
* ```tsx
|
|
94
|
-
* <TimeSlotList.TimeSlots
|
|
97
|
+
* <TimeSlotList.TimeSlots
|
|
98
|
+
* emptyState={<div>No slots available</div>}
|
|
99
|
+
* loadingState={<div>Loading...</div>}
|
|
100
|
+
* >
|
|
95
101
|
* <TimeSlotList.TimeSlotRepeater>
|
|
96
102
|
* {children}
|
|
97
103
|
* </TimeSlotList.TimeSlotRepeater>
|
|
@@ -99,11 +105,32 @@ export interface TimeSlotsProps {
|
|
|
99
105
|
* ```
|
|
100
106
|
*/
|
|
101
107
|
export declare const TimeSlots: React.ForwardRefExoticComponent<TimeSlotsProps & React.RefAttributes<HTMLElement>>;
|
|
108
|
+
/**
|
|
109
|
+
* Data provided to TimeSlotRepeater function children
|
|
110
|
+
*/
|
|
111
|
+
export interface TimeSlotRepeaterChildProps {
|
|
112
|
+
/** Whether the slot is currently selected */
|
|
113
|
+
isSelected: boolean;
|
|
114
|
+
/** Whether the slot can be booked */
|
|
115
|
+
bookable: boolean;
|
|
116
|
+
/** Start date/time as ISO string */
|
|
117
|
+
startDate: string | undefined;
|
|
118
|
+
/** End date/time as ISO string */
|
|
119
|
+
endDate: string | undefined;
|
|
120
|
+
/** Duration in minutes */
|
|
121
|
+
durationInMinutes: number | null;
|
|
122
|
+
/** The raw time slot data */
|
|
123
|
+
timeSlot: TimeSlotType;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Children type for TimeSlotRepeater - can be ReactNode or function
|
|
127
|
+
*/
|
|
128
|
+
type TimeSlotRepeaterChildren = React.ReactNode | ((props: TimeSlotRepeaterChildProps) => React.ReactNode);
|
|
102
129
|
/**
|
|
103
130
|
* Props for TimeSlotList.TimeSlotRepeater component
|
|
104
131
|
*/
|
|
105
132
|
export interface TimeSlotRepeaterProps {
|
|
106
|
-
children:
|
|
133
|
+
children: TimeSlotRepeaterChildren;
|
|
107
134
|
}
|
|
108
135
|
/**
|
|
109
136
|
* Repeater component that renders each time slot.
|
|
@@ -113,11 +140,21 @@ export interface TimeSlotRepeaterProps {
|
|
|
113
140
|
* @component
|
|
114
141
|
* @example
|
|
115
142
|
* ```tsx
|
|
143
|
+
* // Standard usage with nested components
|
|
116
144
|
* <TimeSlotList.TimeSlotRepeater>
|
|
117
145
|
* <TimeSlot.StartDate />
|
|
118
146
|
* <TimeSlot.Duration />
|
|
119
147
|
* <TimeSlot.Actions.Select />
|
|
120
148
|
* </TimeSlotList.TimeSlotRepeater>
|
|
149
|
+
*
|
|
150
|
+
* // Function children pattern - all data available directly
|
|
151
|
+
* <TimeSlotList.TimeSlotRepeater>
|
|
152
|
+
* {({ isSelected, bookable, startDate }) => (
|
|
153
|
+
* <button disabled={!bookable} className={isSelected ? 'selected' : ''}>
|
|
154
|
+
* {formatTime(startDate)}
|
|
155
|
+
* </button>
|
|
156
|
+
* )}
|
|
157
|
+
* </TimeSlotList.TimeSlotRepeater>
|
|
121
158
|
* ```
|
|
122
159
|
*/
|
|
123
160
|
export declare const TimeSlotRepeater: React.ForwardRefExoticComponent<TimeSlotRepeaterProps & React.RefAttributes<HTMLElement>>;
|
|
@@ -81,13 +81,16 @@ const RootContent = React.forwardRef((props, ref) => {
|
|
|
81
81
|
});
|
|
82
82
|
RootContent.displayName = 'TimeSlotList.RootContent';
|
|
83
83
|
/**
|
|
84
|
-
* Container for the time slots list with empty state support.
|
|
84
|
+
* Container for the time slots list with empty state and loading state support.
|
|
85
85
|
* Wraps GenericList.Items. Follows List Container Level pattern.
|
|
86
86
|
*
|
|
87
87
|
* @component
|
|
88
88
|
* @example
|
|
89
89
|
* ```tsx
|
|
90
|
-
* <TimeSlotList.TimeSlots
|
|
90
|
+
* <TimeSlotList.TimeSlots
|
|
91
|
+
* emptyState={<div>No slots available</div>}
|
|
92
|
+
* loadingState={<div>Loading...</div>}
|
|
93
|
+
* >
|
|
91
94
|
* <TimeSlotList.TimeSlotRepeater>
|
|
92
95
|
* {children}
|
|
93
96
|
* </TimeSlotList.TimeSlotRepeater>
|
|
@@ -101,10 +104,12 @@ export const TimeSlots = React.forwardRef((props, ref) => {
|
|
|
101
104
|
TimeSlots.displayName = 'TimeSlotList.TimeSlots';
|
|
102
105
|
/**
|
|
103
106
|
* Internal component that wraps each time slot item with context provider.
|
|
104
|
-
*
|
|
107
|
+
* Supports both ReactNode and function children patterns.
|
|
105
108
|
*/
|
|
106
109
|
const TimeSlotItemWrapper = ({ timeSlot, children, }) => {
|
|
107
|
-
return (_jsx(TimeSlotProvider, { timeSlot: timeSlot, children: _jsx(CoreTimeSlotInfo, { children: (
|
|
110
|
+
return (_jsx(TimeSlotProvider, { timeSlot: timeSlot, children: _jsx(CoreTimeSlotInfo, { children: (props) => {
|
|
111
|
+
return (_jsx("div", { "data-testid": TestIds.timeSlotListTimeSlot, "data-bookable": props.bookable, "data-selected": props.isSelected, children: typeof children === 'function' ? children(props) : children }));
|
|
112
|
+
} }) }));
|
|
108
113
|
};
|
|
109
114
|
/**
|
|
110
115
|
* Repeater component that renders each time slot.
|
|
@@ -114,16 +119,26 @@ const TimeSlotItemWrapper = ({ timeSlot, children, }) => {
|
|
|
114
119
|
* @component
|
|
115
120
|
* @example
|
|
116
121
|
* ```tsx
|
|
122
|
+
* // Standard usage with nested components
|
|
117
123
|
* <TimeSlotList.TimeSlotRepeater>
|
|
118
124
|
* <TimeSlot.StartDate />
|
|
119
125
|
* <TimeSlot.Duration />
|
|
120
126
|
* <TimeSlot.Actions.Select />
|
|
121
127
|
* </TimeSlotList.TimeSlotRepeater>
|
|
128
|
+
*
|
|
129
|
+
* // Function children pattern - all data available directly
|
|
130
|
+
* <TimeSlotList.TimeSlotRepeater>
|
|
131
|
+
* {({ isSelected, bookable, startDate }) => (
|
|
132
|
+
* <button disabled={!bookable} className={isSelected ? 'selected' : ''}>
|
|
133
|
+
* {formatTime(startDate)}
|
|
134
|
+
* </button>
|
|
135
|
+
* )}
|
|
136
|
+
* </TimeSlotList.TimeSlotRepeater>
|
|
122
137
|
* ```
|
|
123
138
|
*/
|
|
124
139
|
export const TimeSlotRepeater = React.forwardRef((props, ref) => {
|
|
125
140
|
const { children } = props;
|
|
126
|
-
return (_jsx(GenericList.Repeater, { ref: ref, itemWrapper: ({ item: timeSlot
|
|
141
|
+
return (_jsx(GenericList.Repeater, { ref: ref, itemWrapper: ({ item: timeSlot }) => (_jsx(TimeSlotItemWrapper, { timeSlot: timeSlot, children: children }, timeSlot.id)), children: undefined }));
|
|
127
142
|
});
|
|
128
143
|
TimeSlotRepeater.displayName = 'TimeSlotList.TimeSlotRepeater';
|
|
129
144
|
/**
|
|
@@ -7,6 +7,7 @@ import { type Signal } from '@wix/services-definitions/core-services/signals';
|
|
|
7
7
|
import type { Service } from '@wix/auto_sdk_bookings_services';
|
|
8
8
|
import type { Resource, EventInfo } from '@wix/auto_sdk_bookings_availability-time-slots';
|
|
9
9
|
import type { FormValues } from '@wix/form-public';
|
|
10
|
+
export declare const ANY_STAFF_MEMBER_ID = "__any_staff_member__";
|
|
10
11
|
/**
|
|
11
12
|
* Minimal time slot data for service selections
|
|
12
13
|
*/
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
import { defineService, implementService } from '@wix/services-definitions';
|
|
7
7
|
import { SignalsServiceDefinition, } from '@wix/services-definitions/core-services/signals';
|
|
8
8
|
// ============================================================================
|
|
9
|
+
// Constants
|
|
10
|
+
// ============================================================================
|
|
11
|
+
export const ANY_STAFF_MEMBER_ID = '__any_staff_member__';
|
|
12
|
+
// ============================================================================
|
|
9
13
|
// Types
|
|
10
14
|
// ============================================================================
|
|
11
15
|
/**
|
|
@@ -51,8 +51,6 @@ export interface TimeSlotsFilters {
|
|
|
51
51
|
locations?: Location[];
|
|
52
52
|
/** Filter by specific staff member IDs */
|
|
53
53
|
staffMemberIds?: string[];
|
|
54
|
-
/** Maximum slots to return per day (useful for calendar views) */
|
|
55
|
-
slotsPerDay?: number;
|
|
56
54
|
/** Filter by bookable status. If undefined, returns both bookable and non-bookable */
|
|
57
55
|
bookable?: boolean;
|
|
58
56
|
/** Filter by booking policy violations */
|
|
@@ -61,6 +59,17 @@ export interface TimeSlotsFilters {
|
|
|
61
59
|
tooLateToBook?: boolean;
|
|
62
60
|
bookOnlineDisabled?: boolean;
|
|
63
61
|
};
|
|
62
|
+
/**
|
|
63
|
+
* Number of time slots to load per request.
|
|
64
|
+
* Controls how many slots are fetched initially and on each "Load More" action.
|
|
65
|
+
* Default: 1000. Max: 1000.
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* // Load 6 slots at a time
|
|
69
|
+
* filters: { slotsPerPage: 6 }
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
slotsPerPage?: number;
|
|
64
73
|
}
|
|
65
74
|
export interface TimeSlotListServiceConfig {
|
|
66
75
|
/** Services to fetch time slots for */
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { implementService } from '@wix/services-definitions';
|
|
6
6
|
import { SignalsServiceDefinition } from '@wix/services-definitions/core-services/signals';
|
|
7
|
+
import { format } from 'date-fns';
|
|
7
8
|
import { TimeSlotListServiceDefinition, ViewMode, } from './time-slot-list.def.js';
|
|
8
|
-
import { fetchAvailability } from '../../api/
|
|
9
|
-
import { BookingServiceDefinition, } from '../booking/booking.js';
|
|
9
|
+
import { fetchAvailability } from '../../api/fetch-availability/index.js';
|
|
10
|
+
import { BookingServiceDefinition, ANY_STAFF_MEMBER_ID, } from '../booking/booking.js';
|
|
10
11
|
// ============================================================================
|
|
11
12
|
// Helpers
|
|
12
13
|
// ============================================================================
|
|
@@ -22,13 +23,8 @@ function getClientTimezone() {
|
|
|
22
23
|
* Formats a Date to local ISO string (YYYY-MM-DDThh:mm:ss)
|
|
23
24
|
*/
|
|
24
25
|
function toLocalDateString(date) {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
const day = String(date.getDate()).padStart(2, '0');
|
|
28
|
-
const hours = String(date.getHours()).padStart(2, '0');
|
|
29
|
-
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
30
|
-
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
31
|
-
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
|
|
26
|
+
const pattern = "yyyy-MM-dd'T'HH:mm:ss";
|
|
27
|
+
return format(date, pattern);
|
|
32
28
|
}
|
|
33
29
|
/**
|
|
34
30
|
* Merges new grouped slots with existing ones
|
|
@@ -88,8 +84,8 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
|
|
|
88
84
|
const getFilters = () => {
|
|
89
85
|
// Get BookingService selections (these take priority)
|
|
90
86
|
const location = bookingService.location.get();
|
|
91
|
-
const
|
|
92
|
-
const selectedStaffMemberIds =
|
|
87
|
+
const serviceSelections = bookingService.serviceSelections.get();
|
|
88
|
+
const selectedStaffMemberIds = serviceSelections
|
|
93
89
|
.map(({ resources }) => resources.map((r) => r._id))
|
|
94
90
|
.flat();
|
|
95
91
|
const selectedLocation = location ? { _id: location.id } : null;
|
|
@@ -114,6 +110,7 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
|
|
|
114
110
|
isLoadingSignal.set(true);
|
|
115
111
|
errorSignal.set(null);
|
|
116
112
|
cursorSignal.set(undefined);
|
|
113
|
+
timeSlotsSignal.set([]);
|
|
117
114
|
try {
|
|
118
115
|
const response = await fetchAvailability({
|
|
119
116
|
services: selectedServices,
|
|
@@ -121,6 +118,7 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
|
|
|
121
118
|
toLocalDate: toLocalDateString(endDateSignal.get()),
|
|
122
119
|
timeZone: timezoneSignal.get(),
|
|
123
120
|
viewMode: viewModeSignal.get(),
|
|
121
|
+
cursor: cursorSignal.get(),
|
|
124
122
|
...filters,
|
|
125
123
|
});
|
|
126
124
|
timeSlotsSignal.set(response.timeSlots);
|
|
@@ -245,6 +243,18 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
|
|
|
245
243
|
const currentSelections = selectionSignal.get();
|
|
246
244
|
if (!currentSelections?.slot)
|
|
247
245
|
return;
|
|
246
|
+
if (staffMemberId === ANY_STAFF_MEMBER_ID) {
|
|
247
|
+
bookingService.actions.updateItem({
|
|
248
|
+
resources: [
|
|
249
|
+
{
|
|
250
|
+
_id: ANY_STAFF_MEMBER_ID,
|
|
251
|
+
name: 'Any staff member',
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
});
|
|
255
|
+
selectionSignal.set({ ...currentSelections, staffMemberId });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
248
258
|
const slot = currentSelections.slot;
|
|
249
259
|
// Find all locations that have this staff member
|
|
250
260
|
const locationsWithThisStaff = slot.staffByLocation.filter((loc) => loc.staffMembers.some((s) => s.id === staffMemberId));
|
|
@@ -312,7 +322,7 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
|
|
|
312
322
|
fromLocalDate: toLocalDateString(startDateSignal.get()),
|
|
313
323
|
toLocalDate: toLocalDateString(endDateSignal.get()),
|
|
314
324
|
timeZone: timezoneSignal.get(),
|
|
315
|
-
|
|
325
|
+
cursor,
|
|
316
326
|
viewMode: viewModeSignal.get(),
|
|
317
327
|
...filters,
|
|
318
328
|
});
|
|
@@ -15,8 +15,6 @@ export interface FetchAvailabilityOptions {
|
|
|
15
15
|
staffMemberIds?: string[];
|
|
16
16
|
cursorPaging?: CursorPaging;
|
|
17
17
|
viewMode?: ViewMode;
|
|
18
|
-
/** Maximum slots to return per day (useful for calendar views) */
|
|
19
|
-
slotsPerDay?: number;
|
|
20
18
|
/** Filter by bookable status. If undefined, returns both bookable and non-bookable */
|
|
21
19
|
bookable?: boolean;
|
|
22
20
|
/** Filter by booking policy violations */
|
|
@@ -54,20 +52,20 @@ export interface FetchAvailabilityResponse {
|
|
|
54
52
|
* // response.timeSlots are grouped by time
|
|
55
53
|
* ```
|
|
56
54
|
*/
|
|
57
|
-
export declare function fetchAvailability({ services: selectedServices, viewMode, fromLocalDate, toLocalDate, timeZone, locations, staffMemberIds,
|
|
55
|
+
export declare function fetchAvailability({ services: selectedServices, viewMode, fromLocalDate, toLocalDate, timeZone, locations, staffMemberIds, cursor, slotsPerPage, bookable, bookingPolicyViolations, }: {
|
|
58
56
|
services: Service[];
|
|
59
57
|
fromLocalDate: string;
|
|
60
58
|
toLocalDate: string;
|
|
61
59
|
timeZone?: string;
|
|
62
60
|
locations?: Location[];
|
|
63
61
|
staffMemberIds?: string[];
|
|
64
|
-
|
|
62
|
+
cursor?: string;
|
|
65
63
|
viewMode?: ViewMode;
|
|
66
|
-
slotsPerDay?: number;
|
|
67
64
|
bookable?: boolean;
|
|
68
65
|
bookingPolicyViolations?: {
|
|
69
66
|
tooEarlyToBook?: boolean;
|
|
70
67
|
tooLateToBook?: boolean;
|
|
71
68
|
bookOnlineDisabled?: boolean;
|
|
72
69
|
};
|
|
70
|
+
slotsPerPage?: number;
|
|
73
71
|
}): Promise<FetchAvailabilityResponse>;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { listAvailabilityTimeSlots, } from '@wix/auto_sdk_bookings_availability-time-slots';
|
|
6
6
|
import { listEventTimeSlots } from '@wix/auto_sdk_bookings_event-time-slots';
|
|
7
7
|
import { ServiceType } from '@wix/auto_sdk_bookings_services';
|
|
8
|
+
import { ANY_STAFF_MEMBER_ID } from '../../services/booking/booking.js';
|
|
8
9
|
import { ViewMode, } from '../../services/time-slot-list/time-slot-list.def.js';
|
|
9
10
|
// ============================================================================
|
|
10
11
|
// Internal Types
|
|
@@ -15,6 +16,7 @@ var FlowType;
|
|
|
15
16
|
FlowType["MULTI_SERVICE"] = "multi_service";
|
|
16
17
|
FlowType["CLASS"] = "class";
|
|
17
18
|
})(FlowType || (FlowType = {}));
|
|
19
|
+
const STAFF_RESOURCE_TYPE_ID = '1cd44cf8-756f-41c3-bd90-3e2ffcaf1155';
|
|
18
20
|
// ============================================================================
|
|
19
21
|
// Grouping Functions
|
|
20
22
|
// ============================================================================
|
|
@@ -102,16 +104,15 @@ function detectFlow(bookingServices, viewMode) {
|
|
|
102
104
|
* // response.timeSlots are grouped by time
|
|
103
105
|
* ```
|
|
104
106
|
*/
|
|
105
|
-
export async function fetchAvailability({ services: selectedServices, viewMode = ViewMode.CLASSIC, fromLocalDate, toLocalDate, timeZone, locations, staffMemberIds,
|
|
107
|
+
export async function fetchAvailability({ services: selectedServices, viewMode = ViewMode.CLASSIC, fromLocalDate, toLocalDate, timeZone, locations, staffMemberIds, cursor, slotsPerPage, bookable, bookingPolicyViolations, }) {
|
|
106
108
|
const flow = detectFlow(selectedServices, viewMode);
|
|
107
109
|
const serviceIds = selectedServices.map((s) => s._id);
|
|
108
110
|
if (flow === FlowType.SINGLE_SERVICE) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const resourceTypes = staffMemberIds?.length
|
|
111
|
+
const isAnyStaffMemberSelected = staffMemberIds?.length === 1 && staffMemberIds[0] === ANY_STAFF_MEMBER_ID;
|
|
112
|
+
const resourceTypes = staffMemberIds?.length && !isAnyStaffMemberSelected
|
|
112
113
|
? [
|
|
113
114
|
{
|
|
114
|
-
resourceTypeId:
|
|
115
|
+
resourceTypeId: STAFF_RESOURCE_TYPE_ID,
|
|
115
116
|
resourceIds: staffMemberIds,
|
|
116
117
|
},
|
|
117
118
|
]
|
|
@@ -122,11 +123,16 @@ export async function fetchAvailability({ services: selectedServices, viewMode =
|
|
|
122
123
|
toLocalDate,
|
|
123
124
|
timeZone,
|
|
124
125
|
locations,
|
|
125
|
-
cursorPaging
|
|
126
|
+
cursorPaging: {
|
|
127
|
+
cursor,
|
|
128
|
+
limit: slotsPerPage,
|
|
129
|
+
},
|
|
126
130
|
resourceTypes,
|
|
127
|
-
timeSlotsPerDay: slotsPerDay,
|
|
128
131
|
bookable,
|
|
129
132
|
bookingPolicyViolations,
|
|
133
|
+
includeResourceTypeIds: isAnyStaffMemberSelected
|
|
134
|
+
? undefined
|
|
135
|
+
: [STAFF_RESOURCE_TYPE_ID],
|
|
130
136
|
});
|
|
131
137
|
return {
|
|
132
138
|
timeSlots: groupTimeSlotsByTime(response.timeSlots || []),
|
|
@@ -152,13 +158,12 @@ export async function fetchAvailability({ services: selectedServices, viewMode =
|
|
|
152
158
|
toLocalDate,
|
|
153
159
|
timeZone,
|
|
154
160
|
serviceIds: serviceIds.length ? serviceIds : undefined,
|
|
155
|
-
cursorPaging
|
|
161
|
+
cursorPaging: {
|
|
162
|
+
cursor,
|
|
163
|
+
limit: slotsPerPage,
|
|
164
|
+
},
|
|
156
165
|
eventFilter: Object.keys(eventFilter).length ? eventFilter : undefined,
|
|
157
|
-
|
|
158
|
-
// bookable=true means only bookable, so includeNonBookable=false
|
|
159
|
-
// bookable=false means only non-bookable, so includeNonBookable=true
|
|
160
|
-
// bookable=undefined means both, so includeNonBookable=true (default)
|
|
161
|
-
includeNonBookable: bookable === undefined ? undefined : !bookable,
|
|
166
|
+
includeNonBookable: bookable ? !bookable : undefined,
|
|
162
167
|
bookingPolicyViolations,
|
|
163
168
|
});
|
|
164
169
|
return {
|
package/dist/api/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* @module API
|
|
6
6
|
*/
|
|
7
7
|
export { getServiceById, getServiceBySlug, queryServices, type PagingMetadata, type ServiceListFilters, type QueryServicesRequest, } from './query-services/index.js';
|
|
8
|
-
export { fetchAvailability } from './
|
|
8
|
+
export { fetchAvailability } from './fetch-availability/index.js';
|
|
9
9
|
/**
|
|
10
10
|
* PagingMetadata includes:
|
|
11
11
|
* - limit: Number of items per page
|
package/dist/api/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* @module API
|
|
6
6
|
*/
|
|
7
7
|
export { getServiceById, getServiceBySlug, queryServices, } from './query-services/index.js';
|
|
8
|
-
export { fetchAvailability } from './
|
|
8
|
+
export { fetchAvailability } from './fetch-availability/index.js';
|
|
9
9
|
/**
|
|
10
10
|
* PagingMetadata includes:
|
|
11
11
|
* - limit: Number of items per page
|
|
@@ -9,6 +9,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
9
9
|
import React from 'react';
|
|
10
10
|
import { useService } from '@wix/services-manager-react';
|
|
11
11
|
import { TimeSlotListServiceDefinition, } from '../../../services/time-slot-list/time-slot-list.def.js';
|
|
12
|
+
import { ANY_STAFF_MEMBER_ID } from '../../../services/booking/booking.js';
|
|
12
13
|
const TimeSlotContext = React.createContext(null);
|
|
13
14
|
/**
|
|
14
15
|
* Hook to access TimeSlot context
|
|
@@ -202,12 +203,13 @@ export function Locations(props) {
|
|
|
202
203
|
const selection = service.selection.get();
|
|
203
204
|
const selectedLocationId = selection?.locationId ?? null;
|
|
204
205
|
const selectedStaffMemberId = selection?.staffMemberId ?? null;
|
|
206
|
+
const isAnyStaffMemberSelected = selectedStaffMemberId === ANY_STAFF_MEMBER_ID;
|
|
205
207
|
// Build locations array from staffByLocation (names already included)
|
|
206
208
|
const locations = timeSlot.staffByLocation.map(({ location, staffMembers }) => {
|
|
207
209
|
// Check if this location is selectable based on selected staff
|
|
208
210
|
// If no staff selected, all locations are selectable
|
|
209
211
|
// If staff selected, only locations where that staff works are selectable
|
|
210
|
-
const isSelectable = selectedStaffMemberId
|
|
212
|
+
const isSelectable = !isAnyStaffMemberSelected && selectedStaffMemberId
|
|
211
213
|
? staffMembers.some((s) => s.id === selectedStaffMemberId)
|
|
212
214
|
: true;
|
|
213
215
|
return {
|
|
@@ -311,12 +313,18 @@ export function StaffMembers(props) {
|
|
|
311
313
|
isSelectable: !selectedLocationId ||
|
|
312
314
|
entries.some((e) => e.locationId === selectedLocationId),
|
|
313
315
|
}));
|
|
316
|
+
const anyStaffOption = {
|
|
317
|
+
id: ANY_STAFF_MEMBER_ID,
|
|
318
|
+
name: 'Any staff member',
|
|
319
|
+
isSelected: selectedStaffMemberId === ANY_STAFF_MEMBER_ID,
|
|
320
|
+
isSelectable: true,
|
|
321
|
+
};
|
|
314
322
|
// Return null if only 1 staff member - nothing to choose from
|
|
315
323
|
if (staffMembers.length <= 1) {
|
|
316
324
|
return null;
|
|
317
325
|
}
|
|
318
326
|
return props.children({
|
|
319
|
-
staffMembers,
|
|
327
|
+
staffMembers: [anyStaffOption, ...staffMembers],
|
|
320
328
|
selectStaffMember: service.actions.selectStaffMember,
|
|
321
329
|
});
|
|
322
330
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import React from 'react';
|
|
6
6
|
import { ListVariant } from '@wix/headless-components/react';
|
|
7
7
|
import { AsChildChildren } from '@wix/headless-utils/react';
|
|
8
|
-
import type { TimeSlotListServiceConfig } from '../../services/time-slot-list/time-slot-list.def.js';
|
|
8
|
+
import type { TimeSlotListServiceConfig, TimeSlot as TimeSlotType } from '../../services/time-slot-list/time-slot-list.def.js';
|
|
9
9
|
export type { StartDateProps as TimeSlotStartDateProps, EndDateProps as TimeSlotEndDateProps, DurationProps as TimeSlotDurationProps, SelectProps as TimeSlotSelectProps, LocationsProps as TimeSlotLocationsProps, LocationRepeaterProps as TimeSlotLocationRepeaterProps, LocationNameProps as TimeSlotLocationNameProps, SelectLocationProps as TimeSlotSelectLocationProps, StaffMembersProps as TimeSlotStaffMembersProps, StaffMemberRepeaterProps as TimeSlotStaffMemberRepeaterProps, StaffMemberNameProps as TimeSlotStaffMemberNameProps, SelectStaffMemberProps as TimeSlotSelectStaffMemberProps, } from './TimeSlot.js';
|
|
10
10
|
/**
|
|
11
11
|
* Render props for TimeSlotList.Timezone component
|
|
@@ -81,17 +81,23 @@ export declare const Root: React.ForwardRefExoticComponent<RootProps & React.Ref
|
|
|
81
81
|
*/
|
|
82
82
|
export interface TimeSlotsProps {
|
|
83
83
|
children: React.ReactNode;
|
|
84
|
+
/** Content to display when no time slots are available */
|
|
84
85
|
emptyState?: React.ReactNode;
|
|
86
|
+
/** Content to display while time slots are loading */
|
|
87
|
+
loadingState?: React.ReactNode;
|
|
85
88
|
className?: string;
|
|
86
89
|
}
|
|
87
90
|
/**
|
|
88
|
-
* Container for the time slots list with empty state support.
|
|
91
|
+
* Container for the time slots list with empty state and loading state support.
|
|
89
92
|
* Wraps GenericList.Items. Follows List Container Level pattern.
|
|
90
93
|
*
|
|
91
94
|
* @component
|
|
92
95
|
* @example
|
|
93
96
|
* ```tsx
|
|
94
|
-
* <TimeSlotList.TimeSlots
|
|
97
|
+
* <TimeSlotList.TimeSlots
|
|
98
|
+
* emptyState={<div>No slots available</div>}
|
|
99
|
+
* loadingState={<div>Loading...</div>}
|
|
100
|
+
* >
|
|
95
101
|
* <TimeSlotList.TimeSlotRepeater>
|
|
96
102
|
* {children}
|
|
97
103
|
* </TimeSlotList.TimeSlotRepeater>
|
|
@@ -99,11 +105,32 @@ export interface TimeSlotsProps {
|
|
|
99
105
|
* ```
|
|
100
106
|
*/
|
|
101
107
|
export declare const TimeSlots: React.ForwardRefExoticComponent<TimeSlotsProps & React.RefAttributes<HTMLElement>>;
|
|
108
|
+
/**
|
|
109
|
+
* Data provided to TimeSlotRepeater function children
|
|
110
|
+
*/
|
|
111
|
+
export interface TimeSlotRepeaterChildProps {
|
|
112
|
+
/** Whether the slot is currently selected */
|
|
113
|
+
isSelected: boolean;
|
|
114
|
+
/** Whether the slot can be booked */
|
|
115
|
+
bookable: boolean;
|
|
116
|
+
/** Start date/time as ISO string */
|
|
117
|
+
startDate: string | undefined;
|
|
118
|
+
/** End date/time as ISO string */
|
|
119
|
+
endDate: string | undefined;
|
|
120
|
+
/** Duration in minutes */
|
|
121
|
+
durationInMinutes: number | null;
|
|
122
|
+
/** The raw time slot data */
|
|
123
|
+
timeSlot: TimeSlotType;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Children type for TimeSlotRepeater - can be ReactNode or function
|
|
127
|
+
*/
|
|
128
|
+
type TimeSlotRepeaterChildren = React.ReactNode | ((props: TimeSlotRepeaterChildProps) => React.ReactNode);
|
|
102
129
|
/**
|
|
103
130
|
* Props for TimeSlotList.TimeSlotRepeater component
|
|
104
131
|
*/
|
|
105
132
|
export interface TimeSlotRepeaterProps {
|
|
106
|
-
children:
|
|
133
|
+
children: TimeSlotRepeaterChildren;
|
|
107
134
|
}
|
|
108
135
|
/**
|
|
109
136
|
* Repeater component that renders each time slot.
|
|
@@ -113,11 +140,21 @@ export interface TimeSlotRepeaterProps {
|
|
|
113
140
|
* @component
|
|
114
141
|
* @example
|
|
115
142
|
* ```tsx
|
|
143
|
+
* // Standard usage with nested components
|
|
116
144
|
* <TimeSlotList.TimeSlotRepeater>
|
|
117
145
|
* <TimeSlot.StartDate />
|
|
118
146
|
* <TimeSlot.Duration />
|
|
119
147
|
* <TimeSlot.Actions.Select />
|
|
120
148
|
* </TimeSlotList.TimeSlotRepeater>
|
|
149
|
+
*
|
|
150
|
+
* // Function children pattern - all data available directly
|
|
151
|
+
* <TimeSlotList.TimeSlotRepeater>
|
|
152
|
+
* {({ isSelected, bookable, startDate }) => (
|
|
153
|
+
* <button disabled={!bookable} className={isSelected ? 'selected' : ''}>
|
|
154
|
+
* {formatTime(startDate)}
|
|
155
|
+
* </button>
|
|
156
|
+
* )}
|
|
157
|
+
* </TimeSlotList.TimeSlotRepeater>
|
|
121
158
|
* ```
|
|
122
159
|
*/
|
|
123
160
|
export declare const TimeSlotRepeater: React.ForwardRefExoticComponent<TimeSlotRepeaterProps & React.RefAttributes<HTMLElement>>;
|
|
@@ -81,13 +81,16 @@ const RootContent = React.forwardRef((props, ref) => {
|
|
|
81
81
|
});
|
|
82
82
|
RootContent.displayName = 'TimeSlotList.RootContent';
|
|
83
83
|
/**
|
|
84
|
-
* Container for the time slots list with empty state support.
|
|
84
|
+
* Container for the time slots list with empty state and loading state support.
|
|
85
85
|
* Wraps GenericList.Items. Follows List Container Level pattern.
|
|
86
86
|
*
|
|
87
87
|
* @component
|
|
88
88
|
* @example
|
|
89
89
|
* ```tsx
|
|
90
|
-
* <TimeSlotList.TimeSlots
|
|
90
|
+
* <TimeSlotList.TimeSlots
|
|
91
|
+
* emptyState={<div>No slots available</div>}
|
|
92
|
+
* loadingState={<div>Loading...</div>}
|
|
93
|
+
* >
|
|
91
94
|
* <TimeSlotList.TimeSlotRepeater>
|
|
92
95
|
* {children}
|
|
93
96
|
* </TimeSlotList.TimeSlotRepeater>
|
|
@@ -101,10 +104,12 @@ export const TimeSlots = React.forwardRef((props, ref) => {
|
|
|
101
104
|
TimeSlots.displayName = 'TimeSlotList.TimeSlots';
|
|
102
105
|
/**
|
|
103
106
|
* Internal component that wraps each time slot item with context provider.
|
|
104
|
-
*
|
|
107
|
+
* Supports both ReactNode and function children patterns.
|
|
105
108
|
*/
|
|
106
109
|
const TimeSlotItemWrapper = ({ timeSlot, children, }) => {
|
|
107
|
-
return (_jsx(TimeSlotProvider, { timeSlot: timeSlot, children: _jsx(CoreTimeSlotInfo, { children: (
|
|
110
|
+
return (_jsx(TimeSlotProvider, { timeSlot: timeSlot, children: _jsx(CoreTimeSlotInfo, { children: (props) => {
|
|
111
|
+
return (_jsx("div", { "data-testid": TestIds.timeSlotListTimeSlot, "data-bookable": props.bookable, "data-selected": props.isSelected, children: typeof children === 'function' ? children(props) : children }));
|
|
112
|
+
} }) }));
|
|
108
113
|
};
|
|
109
114
|
/**
|
|
110
115
|
* Repeater component that renders each time slot.
|
|
@@ -114,16 +119,26 @@ const TimeSlotItemWrapper = ({ timeSlot, children, }) => {
|
|
|
114
119
|
* @component
|
|
115
120
|
* @example
|
|
116
121
|
* ```tsx
|
|
122
|
+
* // Standard usage with nested components
|
|
117
123
|
* <TimeSlotList.TimeSlotRepeater>
|
|
118
124
|
* <TimeSlot.StartDate />
|
|
119
125
|
* <TimeSlot.Duration />
|
|
120
126
|
* <TimeSlot.Actions.Select />
|
|
121
127
|
* </TimeSlotList.TimeSlotRepeater>
|
|
128
|
+
*
|
|
129
|
+
* // Function children pattern - all data available directly
|
|
130
|
+
* <TimeSlotList.TimeSlotRepeater>
|
|
131
|
+
* {({ isSelected, bookable, startDate }) => (
|
|
132
|
+
* <button disabled={!bookable} className={isSelected ? 'selected' : ''}>
|
|
133
|
+
* {formatTime(startDate)}
|
|
134
|
+
* </button>
|
|
135
|
+
* )}
|
|
136
|
+
* </TimeSlotList.TimeSlotRepeater>
|
|
122
137
|
* ```
|
|
123
138
|
*/
|
|
124
139
|
export const TimeSlotRepeater = React.forwardRef((props, ref) => {
|
|
125
140
|
const { children } = props;
|
|
126
|
-
return (_jsx(GenericList.Repeater, { ref: ref, itemWrapper: ({ item: timeSlot
|
|
141
|
+
return (_jsx(GenericList.Repeater, { ref: ref, itemWrapper: ({ item: timeSlot }) => (_jsx(TimeSlotItemWrapper, { timeSlot: timeSlot, children: children }, timeSlot.id)), children: undefined }));
|
|
127
142
|
});
|
|
128
143
|
TimeSlotRepeater.displayName = 'TimeSlotList.TimeSlotRepeater';
|
|
129
144
|
/**
|
|
@@ -7,6 +7,7 @@ import { type Signal } from '@wix/services-definitions/core-services/signals';
|
|
|
7
7
|
import type { Service } from '@wix/auto_sdk_bookings_services';
|
|
8
8
|
import type { Resource, EventInfo } from '@wix/auto_sdk_bookings_availability-time-slots';
|
|
9
9
|
import type { FormValues } from '@wix/form-public';
|
|
10
|
+
export declare const ANY_STAFF_MEMBER_ID = "__any_staff_member__";
|
|
10
11
|
/**
|
|
11
12
|
* Minimal time slot data for service selections
|
|
12
13
|
*/
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
import { defineService, implementService } from '@wix/services-definitions';
|
|
7
7
|
import { SignalsServiceDefinition, } from '@wix/services-definitions/core-services/signals';
|
|
8
8
|
// ============================================================================
|
|
9
|
+
// Constants
|
|
10
|
+
// ============================================================================
|
|
11
|
+
export const ANY_STAFF_MEMBER_ID = '__any_staff_member__';
|
|
12
|
+
// ============================================================================
|
|
9
13
|
// Types
|
|
10
14
|
// ============================================================================
|
|
11
15
|
/**
|
|
@@ -51,8 +51,6 @@ export interface TimeSlotsFilters {
|
|
|
51
51
|
locations?: Location[];
|
|
52
52
|
/** Filter by specific staff member IDs */
|
|
53
53
|
staffMemberIds?: string[];
|
|
54
|
-
/** Maximum slots to return per day (useful for calendar views) */
|
|
55
|
-
slotsPerDay?: number;
|
|
56
54
|
/** Filter by bookable status. If undefined, returns both bookable and non-bookable */
|
|
57
55
|
bookable?: boolean;
|
|
58
56
|
/** Filter by booking policy violations */
|
|
@@ -61,6 +59,17 @@ export interface TimeSlotsFilters {
|
|
|
61
59
|
tooLateToBook?: boolean;
|
|
62
60
|
bookOnlineDisabled?: boolean;
|
|
63
61
|
};
|
|
62
|
+
/**
|
|
63
|
+
* Number of time slots to load per request.
|
|
64
|
+
* Controls how many slots are fetched initially and on each "Load More" action.
|
|
65
|
+
* Default: 1000. Max: 1000.
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* // Load 6 slots at a time
|
|
69
|
+
* filters: { slotsPerPage: 6 }
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
slotsPerPage?: number;
|
|
64
73
|
}
|
|
65
74
|
export interface TimeSlotListServiceConfig {
|
|
66
75
|
/** Services to fetch time slots for */
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { implementService } from '@wix/services-definitions';
|
|
6
6
|
import { SignalsServiceDefinition } from '@wix/services-definitions/core-services/signals';
|
|
7
|
+
import { format } from 'date-fns';
|
|
7
8
|
import { TimeSlotListServiceDefinition, ViewMode, } from './time-slot-list.def.js';
|
|
8
|
-
import { fetchAvailability } from '../../api/
|
|
9
|
-
import { BookingServiceDefinition, } from '../booking/booking.js';
|
|
9
|
+
import { fetchAvailability } from '../../api/fetch-availability/index.js';
|
|
10
|
+
import { BookingServiceDefinition, ANY_STAFF_MEMBER_ID, } from '../booking/booking.js';
|
|
10
11
|
// ============================================================================
|
|
11
12
|
// Helpers
|
|
12
13
|
// ============================================================================
|
|
@@ -22,13 +23,8 @@ function getClientTimezone() {
|
|
|
22
23
|
* Formats a Date to local ISO string (YYYY-MM-DDThh:mm:ss)
|
|
23
24
|
*/
|
|
24
25
|
function toLocalDateString(date) {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
const day = String(date.getDate()).padStart(2, '0');
|
|
28
|
-
const hours = String(date.getHours()).padStart(2, '0');
|
|
29
|
-
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
30
|
-
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
31
|
-
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
|
|
26
|
+
const pattern = "yyyy-MM-dd'T'HH:mm:ss";
|
|
27
|
+
return format(date, pattern);
|
|
32
28
|
}
|
|
33
29
|
/**
|
|
34
30
|
* Merges new grouped slots with existing ones
|
|
@@ -88,8 +84,8 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
|
|
|
88
84
|
const getFilters = () => {
|
|
89
85
|
// Get BookingService selections (these take priority)
|
|
90
86
|
const location = bookingService.location.get();
|
|
91
|
-
const
|
|
92
|
-
const selectedStaffMemberIds =
|
|
87
|
+
const serviceSelections = bookingService.serviceSelections.get();
|
|
88
|
+
const selectedStaffMemberIds = serviceSelections
|
|
93
89
|
.map(({ resources }) => resources.map((r) => r._id))
|
|
94
90
|
.flat();
|
|
95
91
|
const selectedLocation = location ? { _id: location.id } : null;
|
|
@@ -114,6 +110,7 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
|
|
|
114
110
|
isLoadingSignal.set(true);
|
|
115
111
|
errorSignal.set(null);
|
|
116
112
|
cursorSignal.set(undefined);
|
|
113
|
+
timeSlotsSignal.set([]);
|
|
117
114
|
try {
|
|
118
115
|
const response = await fetchAvailability({
|
|
119
116
|
services: selectedServices,
|
|
@@ -121,6 +118,7 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
|
|
|
121
118
|
toLocalDate: toLocalDateString(endDateSignal.get()),
|
|
122
119
|
timeZone: timezoneSignal.get(),
|
|
123
120
|
viewMode: viewModeSignal.get(),
|
|
121
|
+
cursor: cursorSignal.get(),
|
|
124
122
|
...filters,
|
|
125
123
|
});
|
|
126
124
|
timeSlotsSignal.set(response.timeSlots);
|
|
@@ -245,6 +243,18 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
|
|
|
245
243
|
const currentSelections = selectionSignal.get();
|
|
246
244
|
if (!currentSelections?.slot)
|
|
247
245
|
return;
|
|
246
|
+
if (staffMemberId === ANY_STAFF_MEMBER_ID) {
|
|
247
|
+
bookingService.actions.updateItem({
|
|
248
|
+
resources: [
|
|
249
|
+
{
|
|
250
|
+
_id: ANY_STAFF_MEMBER_ID,
|
|
251
|
+
name: 'Any staff member',
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
});
|
|
255
|
+
selectionSignal.set({ ...currentSelections, staffMemberId });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
248
258
|
const slot = currentSelections.slot;
|
|
249
259
|
// Find all locations that have this staff member
|
|
250
260
|
const locationsWithThisStaff = slot.staffByLocation.filter((loc) => loc.staffMembers.some((s) => s.id === staffMemberId));
|
|
@@ -312,7 +322,7 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
|
|
|
312
322
|
fromLocalDate: toLocalDateString(startDateSignal.get()),
|
|
313
323
|
toLocalDate: toLocalDateString(endDateSignal.get()),
|
|
314
324
|
timeZone: timezoneSignal.get(),
|
|
315
|
-
|
|
325
|
+
cursor,
|
|
316
326
|
viewMode: viewModeSignal.get(),
|
|
317
327
|
...filters,
|
|
318
328
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/headless-bookings",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.21",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
"@wix/headless-media": "0.0.18",
|
|
40
40
|
"@wix/headless-utils": "0.0.8",
|
|
41
41
|
"@wix/services-definitions": "^0.1.4",
|
|
42
|
-
"@wix/services-manager-react": "^0.1.26"
|
|
42
|
+
"@wix/services-manager-react": "^0.1.26",
|
|
43
|
+
"date-fns": "^4.1.0"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
46
|
"@testing-library/dom": "^10.4.0",
|
|
@@ -69,5 +70,5 @@
|
|
|
69
70
|
"groupId": "com.wixpress.headless-components"
|
|
70
71
|
}
|
|
71
72
|
},
|
|
72
|
-
"falconPackageHash": "
|
|
73
|
+
"falconPackageHash": "d2842c53f889bbbdfd049c74e533eef301d158412ec519b08c80c87c"
|
|
73
74
|
}
|