@wix/headless-bookings 0.0.34 → 0.0.36

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.
@@ -106,9 +106,10 @@ export function ListData(props) {
106
106
  */
107
107
  export function DateRange(props) {
108
108
  const service = useService(TimeSlotListServiceDefinition);
109
+ const dateRange = service.dateRange.get();
109
110
  return props.children({
110
- startDate: service.startDate.get(),
111
- endDate: service.endDate.get(),
111
+ startDate: dateRange.start,
112
+ endDate: dateRange.end,
112
113
  setDateRange: service.actions.setDateRange,
113
114
  });
114
115
  }
@@ -57,11 +57,7 @@ var TestIds;
57
57
  export const Root = React.forwardRef((props, ref) => {
58
58
  const { children, paymentServiceConfig, slotServices, numberOfParticipants, asChild, className, ...attrs } = props;
59
59
  return (_jsx(CorePayment.Root, { paymentServiceConfig: paymentServiceConfig, slotServices: slotServices, numberOfParticipants: numberOfParticipants, children: ({ isLoading, hasError }) => {
60
- // Handle render function children regardless of asChild
61
- const resolvedChildren = typeof children === 'function'
62
- ? children({ isLoading, hasError })
63
- : children;
64
- return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.paymentRoot, "data-loading": isLoading, "data-has-error": hasError, customElement: resolvedChildren, customElementProps: { isLoading, hasError }, ...attrs, children: _jsx("div", { children: React.isValidElement(resolvedChildren) ? resolvedChildren : null }) }));
60
+ return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.paymentRoot, "data-loading": isLoading, "data-has-error": hasError, customElement: children, customElementProps: { isLoading, hasError }, ...attrs, children: children }));
65
61
  } }));
66
62
  });
67
63
  Root.displayName = 'Payment.Root';
@@ -577,6 +577,7 @@ export interface RawProps {
577
577
  * @component
578
578
  * @example
579
579
  * ```tsx
580
+ * // Using render prop pattern to access raw service data
580
581
  * <Service.Raw>
581
582
  * {({ service, isLoading, error }) => (
582
583
  * <div>
@@ -586,6 +587,11 @@ export interface RawProps {
586
587
  * </div>
587
588
  * )}
588
589
  * </Service.Raw>
590
+ *
591
+ * // With asChild pattern
592
+ * <Service.Raw asChild>
593
+ * <CustomServiceDisplay />
594
+ * </Service.Raw>
589
595
  * ```
590
596
  */
591
597
  export declare const Raw: React.ForwardRefExoticComponent<RawProps & React.RefAttributes<HTMLElement>>;
@@ -607,6 +613,7 @@ export declare namespace Actions {
607
613
  }) => React.ReactNode);
608
614
  className?: string;
609
615
  label?: string;
616
+ onClicked?: (service: Service) => void;
610
617
  }
611
618
  /**
612
619
  * Select action component - replaces selection with only this service.
@@ -622,6 +629,16 @@ export declare namespace Actions {
622
629
  * <Button variant="primary">Select Service</Button>
623
630
  * </Service.Actions.Select>
624
631
  *
632
+ * // With onClicked callback for navigation
633
+ * <Service.Actions.Select
634
+ * asChild
635
+ * onClicked={(service) => {
636
+ * router.push(`/booking/time-slots/${service._id}`);
637
+ * }}
638
+ * >
639
+ * <Button variant="primary">Select Service</Button>
640
+ * </Service.Actions.Select>
641
+ *
625
642
  * // Using render prop pattern to access bookable state
626
643
  * <Service.Actions.Select>
627
644
  * {({ bookable, selected, onClick }) => (
@@ -647,6 +664,7 @@ export declare namespace Actions {
647
664
  }) => React.ReactNode);
648
665
  className?: string;
649
666
  label?: string;
667
+ onClicked?: (service: Service) => void;
650
668
  }
651
669
  /**
652
670
  * Add action component - adds service to selection array.
@@ -662,6 +680,16 @@ export declare namespace Actions {
662
680
  * <Button variant="secondary">Add Service</Button>
663
681
  * </Service.Actions.Add>
664
682
  *
683
+ * // With onClicked callback for custom behavior after adding
684
+ * <Service.Actions.Add
685
+ * asChild
686
+ * onClicked={(service) => {
687
+ * console.log(`Added service: ${service.name}`);
688
+ * }}
689
+ * >
690
+ * <Button variant="secondary">Add Service</Button>
691
+ * </Service.Actions.Add>
692
+ *
665
693
  * // Using render prop pattern to access bookable state
666
694
  * <Service.Actions.Add>
667
695
  * {({ bookable, selected, onClick }) => (
@@ -687,6 +715,7 @@ export declare namespace Actions {
687
715
  }) => React.ReactNode);
688
716
  className?: string;
689
717
  label?: string;
718
+ onClicked?: (service: Service) => void;
690
719
  }
691
720
  /**
692
721
  * Remove action component - removes service from selection array.
@@ -702,6 +731,16 @@ export declare namespace Actions {
702
731
  * <Button variant="destructive">Remove Service</Button>
703
732
  * </Service.Actions.Remove>
704
733
  *
734
+ * // With onClicked callback for custom behavior after removing
735
+ * <Service.Actions.Remove
736
+ * asChild
737
+ * onClicked={(service) => {
738
+ * console.log(`Removed service: ${service.name}`);
739
+ * }}
740
+ * >
741
+ * <Button variant="destructive">Remove Service</Button>
742
+ * </Service.Actions.Remove>
743
+ *
705
744
  * // Using render prop pattern to access bookable state
706
745
  * <Service.Actions.Remove>
707
746
  * {({ bookable, selected, onClick }) => (
@@ -539,6 +539,7 @@ StaffMemberRepeater.displayName = 'Service.StaffMembers.StaffMemberRepeater';
539
539
  * @component
540
540
  * @example
541
541
  * ```tsx
542
+ * // Using render prop pattern to access raw service data
542
543
  * <Service.Raw>
543
544
  * {({ service, isLoading, error }) => (
544
545
  * <div>
@@ -548,11 +549,16 @@ StaffMemberRepeater.displayName = 'Service.StaffMembers.StaffMemberRepeater';
548
549
  * </div>
549
550
  * )}
550
551
  * </Service.Raw>
552
+ *
553
+ * // With asChild pattern
554
+ * <Service.Raw asChild>
555
+ * <CustomServiceDisplay />
556
+ * </Service.Raw>
551
557
  * ```
552
558
  */
553
559
  export const Raw = React.forwardRef((props, ref) => {
554
560
  const { asChild, children, className } = props;
555
- return (_jsx(CoreService.Service, { children: ({ service, isLoading, error }) => (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.serviceRaw, customElement: children, customElementProps: { service, isLoading, error }, children: _jsxs("div", { children: [isLoading && _jsx("div", { children: "Loading..." }), error && _jsxs("div", { children: ["Error: ", error] }), service && _jsx("pre", { children: JSON.stringify(service, null, 2) })] }) })) }));
561
+ return (_jsx(CoreService.Service, { children: ({ service, isLoading, error }) => (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.serviceRaw, customElement: children, customElementProps: { service, isLoading, error }, children: children })) }));
556
562
  });
557
563
  Raw.displayName = 'Service.Raw';
558
564
  /**
@@ -574,6 +580,16 @@ export var Actions;
574
580
  * <Button variant="primary">Select Service</Button>
575
581
  * </Service.Actions.Select>
576
582
  *
583
+ * // With onClicked callback for navigation
584
+ * <Service.Actions.Select
585
+ * asChild
586
+ * onClicked={(service) => {
587
+ * router.push(`/booking/time-slots/${service._id}`);
588
+ * }}
589
+ * >
590
+ * <Button variant="primary">Select Service</Button>
591
+ * </Service.Actions.Select>
592
+ *
577
593
  * // Using render prop pattern to access bookable state
578
594
  * <Service.Actions.Select>
579
595
  * {({ bookable, selected, onClick }) => (
@@ -585,25 +601,19 @@ export var Actions;
585
601
  * ```
586
602
  */
587
603
  Actions.Select = React.forwardRef((props, ref) => {
588
- const { asChild, children, className, label = 'Select Service' } = props;
604
+ const { asChild, children, className, label = 'Select Service', onClicked, } = props;
589
605
  return (_jsx(CoreService.Actions, { children: ({ select, rawService, bookable, selected, requiresAvailability, }) => {
590
- // If children is a function, call it with bookable, selected, requiresAvailability and other props
591
- if (typeof children === 'function') {
592
- return children({
593
- bookable,
594
- selected,
595
- requiresAvailability,
596
- onClick: select,
597
- service: rawService,
598
- });
599
- }
606
+ const handleClick = () => {
607
+ select();
608
+ onClicked?.(rawService);
609
+ };
600
610
  return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.serviceActionSelect, "data-selected": selected, "data-bookable": bookable, "data-requires-availability": requiresAvailability, customElement: children, customElementProps: {
601
- onClick: select,
611
+ onClick: handleClick,
602
612
  disabled: !bookable,
603
613
  selected,
604
614
  bookable,
605
615
  requiresAvailability,
606
- }, children: _jsx("button", { onClick: select, disabled: !bookable, "data-selected": selected, "data-bookable": bookable, "data-requires-availability": requiresAvailability, children: label }) }));
616
+ }, children: _jsx("button", { onClick: handleClick, disabled: !bookable, "data-selected": selected, "data-bookable": bookable, "data-requires-availability": requiresAvailability, children: label }) }));
607
617
  } }));
608
618
  });
609
619
  Actions.Select.displayName = 'Service.Actions.Select';
@@ -621,6 +631,16 @@ export var Actions;
621
631
  * <Button variant="secondary">Add Service</Button>
622
632
  * </Service.Actions.Add>
623
633
  *
634
+ * // With onClicked callback for custom behavior after adding
635
+ * <Service.Actions.Add
636
+ * asChild
637
+ * onClicked={(service) => {
638
+ * console.log(`Added service: ${service.name}`);
639
+ * }}
640
+ * >
641
+ * <Button variant="secondary">Add Service</Button>
642
+ * </Service.Actions.Add>
643
+ *
624
644
  * // Using render prop pattern to access bookable state
625
645
  * <Service.Actions.Add>
626
646
  * {({ bookable, selected, onClick }) => (
@@ -632,27 +652,21 @@ export var Actions;
632
652
  * ```
633
653
  */
634
654
  Actions.Add = React.forwardRef((props, ref) => {
635
- const { asChild, children, className, label = 'Add Service' } = props;
655
+ const { asChild, children, className, label = 'Add Service', onClicked, } = props;
636
656
  return (_jsx(CoreService.Actions, { children: ({ add, rawService, bookable, selected, requiresAvailability }) => {
637
- // If children is a function, call it with bookable, selected, requiresAvailability and other props
638
- if (typeof children === 'function') {
639
- return children({
640
- bookable,
641
- selected,
642
- requiresAvailability,
643
- onClick: add,
644
- service: rawService,
645
- });
646
- }
647
657
  // Disabled when already selected (can't add again) or not bookable
648
658
  const isDisabled = selected || !bookable;
659
+ const handleClick = () => {
660
+ add();
661
+ onClicked?.(rawService);
662
+ };
649
663
  return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.serviceActionAdd, "data-selected": selected, "data-bookable": bookable, "data-requires-availability": requiresAvailability, customElement: children, customElementProps: {
650
- onClick: add,
664
+ onClick: handleClick,
651
665
  disabled: isDisabled,
652
666
  selected,
653
667
  bookable,
654
668
  requiresAvailability,
655
- }, children: _jsx("button", { onClick: add, disabled: isDisabled, "data-selected": selected, "data-bookable": bookable, "data-requires-availability": requiresAvailability, children: label }) }));
669
+ }, children: _jsx("button", { onClick: handleClick, disabled: isDisabled, "data-selected": selected, "data-bookable": bookable, "data-requires-availability": requiresAvailability, children: label }) }));
656
670
  } }));
657
671
  });
658
672
  Actions.Add.displayName = 'Service.Actions.Add';
@@ -670,6 +684,16 @@ export var Actions;
670
684
  * <Button variant="destructive">Remove Service</Button>
671
685
  * </Service.Actions.Remove>
672
686
  *
687
+ * // With onClicked callback for custom behavior after removing
688
+ * <Service.Actions.Remove
689
+ * asChild
690
+ * onClicked={(service) => {
691
+ * console.log(`Removed service: ${service.name}`);
692
+ * }}
693
+ * >
694
+ * <Button variant="destructive">Remove Service</Button>
695
+ * </Service.Actions.Remove>
696
+ *
673
697
  * // Using render prop pattern to access bookable state
674
698
  * <Service.Actions.Remove>
675
699
  * {({ bookable, selected, onClick }) => (
@@ -681,27 +705,21 @@ export var Actions;
681
705
  * ```
682
706
  */
683
707
  Actions.Remove = React.forwardRef((props, ref) => {
684
- const { asChild, children, className, label = 'Remove Service' } = props;
708
+ const { asChild, children, className, label = 'Remove Service', onClicked, } = props;
685
709
  return (_jsx(CoreService.Actions, { children: ({ remove, rawService, bookable, selected, requiresAvailability, }) => {
686
- // If children is a function, call it with bookable, selected, requiresAvailability and other props
687
- if (typeof children === 'function') {
688
- return children({
689
- bookable,
690
- selected,
691
- requiresAvailability,
692
- onClick: remove,
693
- service: rawService,
694
- });
695
- }
696
710
  // Disabled when not selected (nothing to remove) or not bookable
697
711
  const isDisabled = !selected || !bookable;
712
+ const handleClick = () => {
713
+ remove();
714
+ onClicked?.(rawService);
715
+ };
698
716
  return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.serviceActionRemove, "data-selected": selected, "data-bookable": bookable, "data-requires-availability": requiresAvailability, customElement: children, customElementProps: {
699
- onClick: remove,
717
+ onClick: handleClick,
700
718
  disabled: isDisabled,
701
719
  selected,
702
720
  bookable,
703
721
  requiresAvailability,
704
- }, children: _jsx("button", { onClick: remove, disabled: isDisabled, "data-selected": selected, "data-bookable": bookable, "data-requires-availability": requiresAvailability, children: label }) }));
722
+ }, children: _jsx("button", { onClick: handleClick, disabled: isDisabled, "data-selected": selected, "data-bookable": bookable, "data-requires-availability": requiresAvailability, children: label }) }));
705
723
  } }));
706
724
  });
707
725
  Actions.Remove.displayName = 'Service.Actions.Remove';
@@ -2,5 +2,5 @@
2
2
  * TimeSlotList Service
3
3
  * Main export file for time slot list service
4
4
  */
5
- export { TimeSlotListServiceDefinition, ViewMode, type TimeSlotListServiceAPI, type TimeSlotListServiceActions, type TimeSlotListServiceConfig, type TimeSlotsFilters, } from './time-slot-list.def.js';
5
+ export { TimeSlotListServiceDefinition, ViewMode, type DateRange, type TimeSlotListServiceAPI, type TimeSlotListServiceActions, type TimeSlotListServiceConfig, type TimeSlotsFilters, } from './time-slot-list.def.js';
6
6
  export { TimeSlotListService, loadTimeSlotListConfig, type LoadTimeSlotListConfigOptions, type LoadTimeSlotListConfigResult, } from './time-slot-list.js';
@@ -77,13 +77,16 @@ export interface TimeSlotListServiceActions {
77
77
  loadMore: () => Promise<void>;
78
78
  setDateRange: (start: Date, end: Date) => void;
79
79
  }
80
+ export interface DateRange {
81
+ start: Date;
82
+ end: Date;
83
+ }
80
84
  export interface TimeSlotListServiceAPI {
81
85
  timeSlots: Signal<TimeSlot[]>;
82
86
  isLoading: Signal<boolean>;
83
87
  error: Signal<string | null>;
84
88
  hasMore: Signal<boolean>;
85
- startDate: Signal<Date>;
86
- endDate: Signal<Date>;
89
+ dateRange: Signal<DateRange>;
87
90
  actions: TimeSlotListServiceActions;
88
91
  }
89
92
  export declare const TimeSlotListServiceDefinition: string & {
@@ -15,6 +15,31 @@ function getClientTimezone() {
15
15
  ? Intl.DateTimeFormat().resolvedOptions().timeZone
16
16
  : undefined;
17
17
  }
18
+ /** Compare two string arrays for equality (order-independent) */
19
+ function arraysEqual(a, b) {
20
+ if (a.length !== b.length)
21
+ return false;
22
+ const sortedA = [...a].sort();
23
+ const sortedB = [...b].sort();
24
+ return sortedA.every((val, i) => val === sortedB[i]);
25
+ }
26
+ /** Create a memoized value that only updates when IDs change */
27
+ function createMemoizedByIds({ fallback, getValue, extractId, }) {
28
+ let cachedIds = [];
29
+ let cachedValue = fallback;
30
+ return () => {
31
+ const value = getValue();
32
+ const items = value ?? [];
33
+ const newIds = items.length && typeof items[0] === 'string'
34
+ ? items
35
+ : items.map(extractId);
36
+ if (!arraysEqual(newIds, cachedIds)) {
37
+ cachedIds = newIds;
38
+ cachedValue = items.length ? value : fallback;
39
+ }
40
+ return cachedValue;
41
+ };
42
+ }
18
43
  // ============================================================================
19
44
  // Service Implementation
20
45
  // ============================================================================
@@ -29,33 +54,22 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
29
54
  const hasMoreSignal = signalsService.signal(false);
30
55
  // Internal only - viewMode is auto-detected based on service types
31
56
  const viewModeSignal = signalsService.signal(ViewMode.CLASSIC);
32
- const startDateSignal = signalsService.signal(config.startDate);
33
- const endDateSignal = signalsService.signal(config.endDate);
57
+ const dateRangeSignal = signalsService.signal({
58
+ start: config.startDate,
59
+ end: config.endDate,
60
+ });
34
61
  const cursorSignal = signalsService.signal(undefined);
35
- /**
36
- * Helper: Get services from config or BookingService
37
- */
38
- const getServices = () => {
39
- const selectedServices = bookingService.serviceSelections
40
- .get()
41
- .map((s) => s.service);
42
- if (selectedServices.length) {
43
- return selectedServices;
44
- }
45
- return config.services ?? [];
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 = () => {
62
+ const servicesSignal = signalsService.computed(createMemoizedByIds({
63
+ fallback: config.services ?? [],
64
+ getValue: () => bookingService.serviceSelections.get().map((s) => s.service),
65
+ extractId: (service) => service._id,
66
+ }));
67
+ const preSelectedStaffSignal = signalsService.computed(createMemoizedByIds({
68
+ fallback: config.filters?.staffMemberIds,
69
+ getValue: () => (bookingService.serviceSelections.get()[0]?.resources ?? []).map((resource) => resource._id),
70
+ }));
71
+ const filtersSignal = signalsService.computed(() => {
52
72
  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
73
  const selectedLocation = bookingService.location.get();
60
74
  const location = selectedLocation
61
75
  ? {
@@ -66,36 +80,24 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
66
80
  return {
67
81
  ...baseFilters,
68
82
  location,
69
- staffMemberIds,
83
+ staffMemberIds: preSelectedStaffSignal.get(),
70
84
  };
71
- };
72
- /**
73
- * Fetch time slots from API
74
- * If cursor is provided, appends to existing slots (pagination)
75
- * If no cursor, resets state and fetches fresh slots
76
- */
77
- const fetchTimeSlots = async ({ isLoadMore } = { isLoadMore: false }) => {
78
- if (isLoadingSignal.get() || (isLoadMore && !cursorSignal.get())) {
79
- return;
80
- }
81
- const cursor = isLoadMore ? cursorSignal.get() : undefined;
85
+ });
86
+ const fetchTimeSlots = async () => {
82
87
  isLoadingSignal.set(true);
83
88
  errorSignal.set(null);
84
- if (!isLoadMore) {
85
- timeSlotsSignal.set([]);
86
- }
89
+ cursorSignal.set(undefined);
90
+ timeSlotsSignal.set([]);
87
91
  try {
88
92
  const response = await fetchAvailability({
89
- services: getServices(),
90
- fromLocalDate: startDateSignal.get(),
91
- toLocalDate: endDateSignal.get(),
93
+ services: servicesSignal.get(),
92
94
  timeZone: bookingService.timezone.get(),
93
95
  viewMode: viewModeSignal.get(),
94
- cursor,
95
- ...getFilters(),
96
+ fromLocalDate: dateRangeSignal.get().start,
97
+ toLocalDate: dateRangeSignal.get().end,
98
+ ...filtersSignal.get(),
96
99
  });
97
- const newSlots = response.timeSlots;
98
- timeSlotsSignal.set(isLoadMore ? [...timeSlotsSignal.get(), ...newSlots] : newSlots);
100
+ timeSlotsSignal.set(response.timeSlots);
99
101
  const nextCursor = response.cursorPagingMetadata?.cursors?.next ?? undefined;
100
102
  cursorSignal.set(nextCursor);
101
103
  hasMoreSignal.set(!!nextCursor);
@@ -103,9 +105,7 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
103
105
  catch (error) {
104
106
  const message = error instanceof Error
105
107
  ? error.message
106
- : isLoadMore
107
- ? 'Failed to load more time slots'
108
- : 'Failed to load time slots';
108
+ : 'Failed to load time slots';
109
109
  errorSignal.set(message);
110
110
  }
111
111
  finally {
@@ -182,33 +182,61 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
182
182
  * Action: Load more slots (pagination)
183
183
  */
184
184
  const loadMore = async () => {
185
- return fetchTimeSlots({ isLoadMore: true });
185
+ const cursor = cursorSignal.get();
186
+ if (!cursor) {
187
+ return;
188
+ }
189
+ isLoadingSignal.set(true);
190
+ errorSignal.set(null);
191
+ try {
192
+ const response = await fetchAvailability({
193
+ services: servicesSignal.get(),
194
+ fromLocalDate: dateRangeSignal.get().start,
195
+ toLocalDate: dateRangeSignal.get().end,
196
+ timeZone: bookingService.timezone.get(),
197
+ viewMode: viewModeSignal.get(),
198
+ cursor,
199
+ ...filtersSignal.get(),
200
+ });
201
+ const newSlots = response.timeSlots;
202
+ timeSlotsSignal.set([...timeSlotsSignal.get(), ...newSlots]);
203
+ const nextCursor = response.cursorPagingMetadata?.cursors?.next ?? undefined;
204
+ cursorSignal.set(nextCursor);
205
+ hasMoreSignal.set(!!nextCursor);
206
+ }
207
+ catch (error) {
208
+ const message = error instanceof Error
209
+ ? error.message
210
+ : 'Failed to load more time slots';
211
+ errorSignal.set(message);
212
+ }
213
+ finally {
214
+ isLoadingSignal.set(false);
215
+ }
186
216
  };
187
217
  /**
188
- * Action: Update date range and refetch slots
218
+ * Action: Update date range (effect will handle refetch)
189
219
  */
190
220
  const setDateRange = (newStartDate, newEndDate) => {
191
- const currentStartDate = startDateSignal.get();
192
- const currentEndDate = endDateSignal.get();
221
+ const { start: currentStartDate, end: currentEndDate } = dateRangeSignal.get();
193
222
  if (currentStartDate.getTime() === newStartDate.getTime() &&
194
223
  currentEndDate.getTime() === newEndDate.getTime()) {
195
224
  return;
196
225
  }
197
- startDateSignal.set(newStartDate);
198
- endDateSignal.set(newEndDate);
226
+ dateRangeSignal.set({ start: newStartDate, end: newEndDate });
199
227
  };
200
- const resourcesSignal = signalsService.computed(() => {
201
- return bookingService.serviceSelections.get()[0]?.resources;
202
- });
203
228
  let isFirstRun = true;
204
229
  signalsService.effect(() => {
230
+ filtersSignal.get();
231
+ dateRangeSignal.get();
205
232
  bookingService.timezone.get();
206
- bookingService.location.get();
207
- resourcesSignal.get();
208
- if (!isFirstRun || !config.timeSlots) {
209
- fetchTimeSlots();
233
+ if (isFirstRun) {
234
+ isFirstRun = false;
235
+ if (config.timeSlots) {
236
+ return;
237
+ }
210
238
  }
211
- isFirstRun = false;
239
+ fetchTimeSlots();
212
240
  });
213
241
  // Return service API
214
242
  return {
@@ -218,8 +246,7 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
218
246
  error: errorSignal,
219
247
  hasMore: hasMoreSignal,
220
248
  // Configuration Signals (viewMode is internal only)
221
- startDate: startDateSignal,
222
- endDate: endDateSignal,
249
+ dateRange: dateRangeSignal,
223
250
  // Actions
224
251
  actions: {
225
252
  selectSlot,
@@ -106,9 +106,10 @@ export function ListData(props) {
106
106
  */
107
107
  export function DateRange(props) {
108
108
  const service = useService(TimeSlotListServiceDefinition);
109
+ const dateRange = service.dateRange.get();
109
110
  return props.children({
110
- startDate: service.startDate.get(),
111
- endDate: service.endDate.get(),
111
+ startDate: dateRange.start,
112
+ endDate: dateRange.end,
112
113
  setDateRange: service.actions.setDateRange,
113
114
  });
114
115
  }
@@ -57,11 +57,7 @@ var TestIds;
57
57
  export const Root = React.forwardRef((props, ref) => {
58
58
  const { children, paymentServiceConfig, slotServices, numberOfParticipants, asChild, className, ...attrs } = props;
59
59
  return (_jsx(CorePayment.Root, { paymentServiceConfig: paymentServiceConfig, slotServices: slotServices, numberOfParticipants: numberOfParticipants, children: ({ isLoading, hasError }) => {
60
- // Handle render function children regardless of asChild
61
- const resolvedChildren = typeof children === 'function'
62
- ? children({ isLoading, hasError })
63
- : children;
64
- return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.paymentRoot, "data-loading": isLoading, "data-has-error": hasError, customElement: resolvedChildren, customElementProps: { isLoading, hasError }, ...attrs, children: _jsx("div", { children: React.isValidElement(resolvedChildren) ? resolvedChildren : null }) }));
60
+ return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.paymentRoot, "data-loading": isLoading, "data-has-error": hasError, customElement: children, customElementProps: { isLoading, hasError }, ...attrs, children: children }));
65
61
  } }));
66
62
  });
67
63
  Root.displayName = 'Payment.Root';
@@ -577,6 +577,7 @@ export interface RawProps {
577
577
  * @component
578
578
  * @example
579
579
  * ```tsx
580
+ * // Using render prop pattern to access raw service data
580
581
  * <Service.Raw>
581
582
  * {({ service, isLoading, error }) => (
582
583
  * <div>
@@ -586,6 +587,11 @@ export interface RawProps {
586
587
  * </div>
587
588
  * )}
588
589
  * </Service.Raw>
590
+ *
591
+ * // With asChild pattern
592
+ * <Service.Raw asChild>
593
+ * <CustomServiceDisplay />
594
+ * </Service.Raw>
589
595
  * ```
590
596
  */
591
597
  export declare const Raw: React.ForwardRefExoticComponent<RawProps & React.RefAttributes<HTMLElement>>;
@@ -607,6 +613,7 @@ export declare namespace Actions {
607
613
  }) => React.ReactNode);
608
614
  className?: string;
609
615
  label?: string;
616
+ onClicked?: (service: Service) => void;
610
617
  }
611
618
  /**
612
619
  * Select action component - replaces selection with only this service.
@@ -622,6 +629,16 @@ export declare namespace Actions {
622
629
  * <Button variant="primary">Select Service</Button>
623
630
  * </Service.Actions.Select>
624
631
  *
632
+ * // With onClicked callback for navigation
633
+ * <Service.Actions.Select
634
+ * asChild
635
+ * onClicked={(service) => {
636
+ * router.push(`/booking/time-slots/${service._id}`);
637
+ * }}
638
+ * >
639
+ * <Button variant="primary">Select Service</Button>
640
+ * </Service.Actions.Select>
641
+ *
625
642
  * // Using render prop pattern to access bookable state
626
643
  * <Service.Actions.Select>
627
644
  * {({ bookable, selected, onClick }) => (
@@ -647,6 +664,7 @@ export declare namespace Actions {
647
664
  }) => React.ReactNode);
648
665
  className?: string;
649
666
  label?: string;
667
+ onClicked?: (service: Service) => void;
650
668
  }
651
669
  /**
652
670
  * Add action component - adds service to selection array.
@@ -662,6 +680,16 @@ export declare namespace Actions {
662
680
  * <Button variant="secondary">Add Service</Button>
663
681
  * </Service.Actions.Add>
664
682
  *
683
+ * // With onClicked callback for custom behavior after adding
684
+ * <Service.Actions.Add
685
+ * asChild
686
+ * onClicked={(service) => {
687
+ * console.log(`Added service: ${service.name}`);
688
+ * }}
689
+ * >
690
+ * <Button variant="secondary">Add Service</Button>
691
+ * </Service.Actions.Add>
692
+ *
665
693
  * // Using render prop pattern to access bookable state
666
694
  * <Service.Actions.Add>
667
695
  * {({ bookable, selected, onClick }) => (
@@ -687,6 +715,7 @@ export declare namespace Actions {
687
715
  }) => React.ReactNode);
688
716
  className?: string;
689
717
  label?: string;
718
+ onClicked?: (service: Service) => void;
690
719
  }
691
720
  /**
692
721
  * Remove action component - removes service from selection array.
@@ -702,6 +731,16 @@ export declare namespace Actions {
702
731
  * <Button variant="destructive">Remove Service</Button>
703
732
  * </Service.Actions.Remove>
704
733
  *
734
+ * // With onClicked callback for custom behavior after removing
735
+ * <Service.Actions.Remove
736
+ * asChild
737
+ * onClicked={(service) => {
738
+ * console.log(`Removed service: ${service.name}`);
739
+ * }}
740
+ * >
741
+ * <Button variant="destructive">Remove Service</Button>
742
+ * </Service.Actions.Remove>
743
+ *
705
744
  * // Using render prop pattern to access bookable state
706
745
  * <Service.Actions.Remove>
707
746
  * {({ bookable, selected, onClick }) => (
@@ -539,6 +539,7 @@ StaffMemberRepeater.displayName = 'Service.StaffMembers.StaffMemberRepeater';
539
539
  * @component
540
540
  * @example
541
541
  * ```tsx
542
+ * // Using render prop pattern to access raw service data
542
543
  * <Service.Raw>
543
544
  * {({ service, isLoading, error }) => (
544
545
  * <div>
@@ -548,11 +549,16 @@ StaffMemberRepeater.displayName = 'Service.StaffMembers.StaffMemberRepeater';
548
549
  * </div>
549
550
  * )}
550
551
  * </Service.Raw>
552
+ *
553
+ * // With asChild pattern
554
+ * <Service.Raw asChild>
555
+ * <CustomServiceDisplay />
556
+ * </Service.Raw>
551
557
  * ```
552
558
  */
553
559
  export const Raw = React.forwardRef((props, ref) => {
554
560
  const { asChild, children, className } = props;
555
- return (_jsx(CoreService.Service, { children: ({ service, isLoading, error }) => (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.serviceRaw, customElement: children, customElementProps: { service, isLoading, error }, children: _jsxs("div", { children: [isLoading && _jsx("div", { children: "Loading..." }), error && _jsxs("div", { children: ["Error: ", error] }), service && _jsx("pre", { children: JSON.stringify(service, null, 2) })] }) })) }));
561
+ return (_jsx(CoreService.Service, { children: ({ service, isLoading, error }) => (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.serviceRaw, customElement: children, customElementProps: { service, isLoading, error }, children: children })) }));
556
562
  });
557
563
  Raw.displayName = 'Service.Raw';
558
564
  /**
@@ -574,6 +580,16 @@ export var Actions;
574
580
  * <Button variant="primary">Select Service</Button>
575
581
  * </Service.Actions.Select>
576
582
  *
583
+ * // With onClicked callback for navigation
584
+ * <Service.Actions.Select
585
+ * asChild
586
+ * onClicked={(service) => {
587
+ * router.push(`/booking/time-slots/${service._id}`);
588
+ * }}
589
+ * >
590
+ * <Button variant="primary">Select Service</Button>
591
+ * </Service.Actions.Select>
592
+ *
577
593
  * // Using render prop pattern to access bookable state
578
594
  * <Service.Actions.Select>
579
595
  * {({ bookable, selected, onClick }) => (
@@ -585,25 +601,19 @@ export var Actions;
585
601
  * ```
586
602
  */
587
603
  Actions.Select = React.forwardRef((props, ref) => {
588
- const { asChild, children, className, label = 'Select Service' } = props;
604
+ const { asChild, children, className, label = 'Select Service', onClicked, } = props;
589
605
  return (_jsx(CoreService.Actions, { children: ({ select, rawService, bookable, selected, requiresAvailability, }) => {
590
- // If children is a function, call it with bookable, selected, requiresAvailability and other props
591
- if (typeof children === 'function') {
592
- return children({
593
- bookable,
594
- selected,
595
- requiresAvailability,
596
- onClick: select,
597
- service: rawService,
598
- });
599
- }
606
+ const handleClick = () => {
607
+ select();
608
+ onClicked?.(rawService);
609
+ };
600
610
  return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.serviceActionSelect, "data-selected": selected, "data-bookable": bookable, "data-requires-availability": requiresAvailability, customElement: children, customElementProps: {
601
- onClick: select,
611
+ onClick: handleClick,
602
612
  disabled: !bookable,
603
613
  selected,
604
614
  bookable,
605
615
  requiresAvailability,
606
- }, children: _jsx("button", { onClick: select, disabled: !bookable, "data-selected": selected, "data-bookable": bookable, "data-requires-availability": requiresAvailability, children: label }) }));
616
+ }, children: _jsx("button", { onClick: handleClick, disabled: !bookable, "data-selected": selected, "data-bookable": bookable, "data-requires-availability": requiresAvailability, children: label }) }));
607
617
  } }));
608
618
  });
609
619
  Actions.Select.displayName = 'Service.Actions.Select';
@@ -621,6 +631,16 @@ export var Actions;
621
631
  * <Button variant="secondary">Add Service</Button>
622
632
  * </Service.Actions.Add>
623
633
  *
634
+ * // With onClicked callback for custom behavior after adding
635
+ * <Service.Actions.Add
636
+ * asChild
637
+ * onClicked={(service) => {
638
+ * console.log(`Added service: ${service.name}`);
639
+ * }}
640
+ * >
641
+ * <Button variant="secondary">Add Service</Button>
642
+ * </Service.Actions.Add>
643
+ *
624
644
  * // Using render prop pattern to access bookable state
625
645
  * <Service.Actions.Add>
626
646
  * {({ bookable, selected, onClick }) => (
@@ -632,27 +652,21 @@ export var Actions;
632
652
  * ```
633
653
  */
634
654
  Actions.Add = React.forwardRef((props, ref) => {
635
- const { asChild, children, className, label = 'Add Service' } = props;
655
+ const { asChild, children, className, label = 'Add Service', onClicked, } = props;
636
656
  return (_jsx(CoreService.Actions, { children: ({ add, rawService, bookable, selected, requiresAvailability }) => {
637
- // If children is a function, call it with bookable, selected, requiresAvailability and other props
638
- if (typeof children === 'function') {
639
- return children({
640
- bookable,
641
- selected,
642
- requiresAvailability,
643
- onClick: add,
644
- service: rawService,
645
- });
646
- }
647
657
  // Disabled when already selected (can't add again) or not bookable
648
658
  const isDisabled = selected || !bookable;
659
+ const handleClick = () => {
660
+ add();
661
+ onClicked?.(rawService);
662
+ };
649
663
  return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.serviceActionAdd, "data-selected": selected, "data-bookable": bookable, "data-requires-availability": requiresAvailability, customElement: children, customElementProps: {
650
- onClick: add,
664
+ onClick: handleClick,
651
665
  disabled: isDisabled,
652
666
  selected,
653
667
  bookable,
654
668
  requiresAvailability,
655
- }, children: _jsx("button", { onClick: add, disabled: isDisabled, "data-selected": selected, "data-bookable": bookable, "data-requires-availability": requiresAvailability, children: label }) }));
669
+ }, children: _jsx("button", { onClick: handleClick, disabled: isDisabled, "data-selected": selected, "data-bookable": bookable, "data-requires-availability": requiresAvailability, children: label }) }));
656
670
  } }));
657
671
  });
658
672
  Actions.Add.displayName = 'Service.Actions.Add';
@@ -670,6 +684,16 @@ export var Actions;
670
684
  * <Button variant="destructive">Remove Service</Button>
671
685
  * </Service.Actions.Remove>
672
686
  *
687
+ * // With onClicked callback for custom behavior after removing
688
+ * <Service.Actions.Remove
689
+ * asChild
690
+ * onClicked={(service) => {
691
+ * console.log(`Removed service: ${service.name}`);
692
+ * }}
693
+ * >
694
+ * <Button variant="destructive">Remove Service</Button>
695
+ * </Service.Actions.Remove>
696
+ *
673
697
  * // Using render prop pattern to access bookable state
674
698
  * <Service.Actions.Remove>
675
699
  * {({ bookable, selected, onClick }) => (
@@ -681,27 +705,21 @@ export var Actions;
681
705
  * ```
682
706
  */
683
707
  Actions.Remove = React.forwardRef((props, ref) => {
684
- const { asChild, children, className, label = 'Remove Service' } = props;
708
+ const { asChild, children, className, label = 'Remove Service', onClicked, } = props;
685
709
  return (_jsx(CoreService.Actions, { children: ({ remove, rawService, bookable, selected, requiresAvailability, }) => {
686
- // If children is a function, call it with bookable, selected, requiresAvailability and other props
687
- if (typeof children === 'function') {
688
- return children({
689
- bookable,
690
- selected,
691
- requiresAvailability,
692
- onClick: remove,
693
- service: rawService,
694
- });
695
- }
696
710
  // Disabled when not selected (nothing to remove) or not bookable
697
711
  const isDisabled = !selected || !bookable;
712
+ const handleClick = () => {
713
+ remove();
714
+ onClicked?.(rawService);
715
+ };
698
716
  return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.serviceActionRemove, "data-selected": selected, "data-bookable": bookable, "data-requires-availability": requiresAvailability, customElement: children, customElementProps: {
699
- onClick: remove,
717
+ onClick: handleClick,
700
718
  disabled: isDisabled,
701
719
  selected,
702
720
  bookable,
703
721
  requiresAvailability,
704
- }, children: _jsx("button", { onClick: remove, disabled: isDisabled, "data-selected": selected, "data-bookable": bookable, "data-requires-availability": requiresAvailability, children: label }) }));
722
+ }, children: _jsx("button", { onClick: handleClick, disabled: isDisabled, "data-selected": selected, "data-bookable": bookable, "data-requires-availability": requiresAvailability, children: label }) }));
705
723
  } }));
706
724
  });
707
725
  Actions.Remove.displayName = 'Service.Actions.Remove';
@@ -2,5 +2,5 @@
2
2
  * TimeSlotList Service
3
3
  * Main export file for time slot list service
4
4
  */
5
- export { TimeSlotListServiceDefinition, ViewMode, type TimeSlotListServiceAPI, type TimeSlotListServiceActions, type TimeSlotListServiceConfig, type TimeSlotsFilters, } from './time-slot-list.def.js';
5
+ export { TimeSlotListServiceDefinition, ViewMode, type DateRange, type TimeSlotListServiceAPI, type TimeSlotListServiceActions, type TimeSlotListServiceConfig, type TimeSlotsFilters, } from './time-slot-list.def.js';
6
6
  export { TimeSlotListService, loadTimeSlotListConfig, type LoadTimeSlotListConfigOptions, type LoadTimeSlotListConfigResult, } from './time-slot-list.js';
@@ -77,13 +77,16 @@ export interface TimeSlotListServiceActions {
77
77
  loadMore: () => Promise<void>;
78
78
  setDateRange: (start: Date, end: Date) => void;
79
79
  }
80
+ export interface DateRange {
81
+ start: Date;
82
+ end: Date;
83
+ }
80
84
  export interface TimeSlotListServiceAPI {
81
85
  timeSlots: Signal<TimeSlot[]>;
82
86
  isLoading: Signal<boolean>;
83
87
  error: Signal<string | null>;
84
88
  hasMore: Signal<boolean>;
85
- startDate: Signal<Date>;
86
- endDate: Signal<Date>;
89
+ dateRange: Signal<DateRange>;
87
90
  actions: TimeSlotListServiceActions;
88
91
  }
89
92
  export declare const TimeSlotListServiceDefinition: string & {
@@ -15,6 +15,31 @@ function getClientTimezone() {
15
15
  ? Intl.DateTimeFormat().resolvedOptions().timeZone
16
16
  : undefined;
17
17
  }
18
+ /** Compare two string arrays for equality (order-independent) */
19
+ function arraysEqual(a, b) {
20
+ if (a.length !== b.length)
21
+ return false;
22
+ const sortedA = [...a].sort();
23
+ const sortedB = [...b].sort();
24
+ return sortedA.every((val, i) => val === sortedB[i]);
25
+ }
26
+ /** Create a memoized value that only updates when IDs change */
27
+ function createMemoizedByIds({ fallback, getValue, extractId, }) {
28
+ let cachedIds = [];
29
+ let cachedValue = fallback;
30
+ return () => {
31
+ const value = getValue();
32
+ const items = value ?? [];
33
+ const newIds = items.length && typeof items[0] === 'string'
34
+ ? items
35
+ : items.map(extractId);
36
+ if (!arraysEqual(newIds, cachedIds)) {
37
+ cachedIds = newIds;
38
+ cachedValue = items.length ? value : fallback;
39
+ }
40
+ return cachedValue;
41
+ };
42
+ }
18
43
  // ============================================================================
19
44
  // Service Implementation
20
45
  // ============================================================================
@@ -29,33 +54,22 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
29
54
  const hasMoreSignal = signalsService.signal(false);
30
55
  // Internal only - viewMode is auto-detected based on service types
31
56
  const viewModeSignal = signalsService.signal(ViewMode.CLASSIC);
32
- const startDateSignal = signalsService.signal(config.startDate);
33
- const endDateSignal = signalsService.signal(config.endDate);
57
+ const dateRangeSignal = signalsService.signal({
58
+ start: config.startDate,
59
+ end: config.endDate,
60
+ });
34
61
  const cursorSignal = signalsService.signal(undefined);
35
- /**
36
- * Helper: Get services from config or BookingService
37
- */
38
- const getServices = () => {
39
- const selectedServices = bookingService.serviceSelections
40
- .get()
41
- .map((s) => s.service);
42
- if (selectedServices.length) {
43
- return selectedServices;
44
- }
45
- return config.services ?? [];
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 = () => {
62
+ const servicesSignal = signalsService.computed(createMemoizedByIds({
63
+ fallback: config.services ?? [],
64
+ getValue: () => bookingService.serviceSelections.get().map((s) => s.service),
65
+ extractId: (service) => service._id,
66
+ }));
67
+ const preSelectedStaffSignal = signalsService.computed(createMemoizedByIds({
68
+ fallback: config.filters?.staffMemberIds,
69
+ getValue: () => (bookingService.serviceSelections.get()[0]?.resources ?? []).map((resource) => resource._id),
70
+ }));
71
+ const filtersSignal = signalsService.computed(() => {
52
72
  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
73
  const selectedLocation = bookingService.location.get();
60
74
  const location = selectedLocation
61
75
  ? {
@@ -66,36 +80,24 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
66
80
  return {
67
81
  ...baseFilters,
68
82
  location,
69
- staffMemberIds,
83
+ staffMemberIds: preSelectedStaffSignal.get(),
70
84
  };
71
- };
72
- /**
73
- * Fetch time slots from API
74
- * If cursor is provided, appends to existing slots (pagination)
75
- * If no cursor, resets state and fetches fresh slots
76
- */
77
- const fetchTimeSlots = async ({ isLoadMore } = { isLoadMore: false }) => {
78
- if (isLoadingSignal.get() || (isLoadMore && !cursorSignal.get())) {
79
- return;
80
- }
81
- const cursor = isLoadMore ? cursorSignal.get() : undefined;
85
+ });
86
+ const fetchTimeSlots = async () => {
82
87
  isLoadingSignal.set(true);
83
88
  errorSignal.set(null);
84
- if (!isLoadMore) {
85
- timeSlotsSignal.set([]);
86
- }
89
+ cursorSignal.set(undefined);
90
+ timeSlotsSignal.set([]);
87
91
  try {
88
92
  const response = await fetchAvailability({
89
- services: getServices(),
90
- fromLocalDate: startDateSignal.get(),
91
- toLocalDate: endDateSignal.get(),
93
+ services: servicesSignal.get(),
92
94
  timeZone: bookingService.timezone.get(),
93
95
  viewMode: viewModeSignal.get(),
94
- cursor,
95
- ...getFilters(),
96
+ fromLocalDate: dateRangeSignal.get().start,
97
+ toLocalDate: dateRangeSignal.get().end,
98
+ ...filtersSignal.get(),
96
99
  });
97
- const newSlots = response.timeSlots;
98
- timeSlotsSignal.set(isLoadMore ? [...timeSlotsSignal.get(), ...newSlots] : newSlots);
100
+ timeSlotsSignal.set(response.timeSlots);
99
101
  const nextCursor = response.cursorPagingMetadata?.cursors?.next ?? undefined;
100
102
  cursorSignal.set(nextCursor);
101
103
  hasMoreSignal.set(!!nextCursor);
@@ -103,9 +105,7 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
103
105
  catch (error) {
104
106
  const message = error instanceof Error
105
107
  ? error.message
106
- : isLoadMore
107
- ? 'Failed to load more time slots'
108
- : 'Failed to load time slots';
108
+ : 'Failed to load time slots';
109
109
  errorSignal.set(message);
110
110
  }
111
111
  finally {
@@ -182,33 +182,61 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
182
182
  * Action: Load more slots (pagination)
183
183
  */
184
184
  const loadMore = async () => {
185
- return fetchTimeSlots({ isLoadMore: true });
185
+ const cursor = cursorSignal.get();
186
+ if (!cursor) {
187
+ return;
188
+ }
189
+ isLoadingSignal.set(true);
190
+ errorSignal.set(null);
191
+ try {
192
+ const response = await fetchAvailability({
193
+ services: servicesSignal.get(),
194
+ fromLocalDate: dateRangeSignal.get().start,
195
+ toLocalDate: dateRangeSignal.get().end,
196
+ timeZone: bookingService.timezone.get(),
197
+ viewMode: viewModeSignal.get(),
198
+ cursor,
199
+ ...filtersSignal.get(),
200
+ });
201
+ const newSlots = response.timeSlots;
202
+ timeSlotsSignal.set([...timeSlotsSignal.get(), ...newSlots]);
203
+ const nextCursor = response.cursorPagingMetadata?.cursors?.next ?? undefined;
204
+ cursorSignal.set(nextCursor);
205
+ hasMoreSignal.set(!!nextCursor);
206
+ }
207
+ catch (error) {
208
+ const message = error instanceof Error
209
+ ? error.message
210
+ : 'Failed to load more time slots';
211
+ errorSignal.set(message);
212
+ }
213
+ finally {
214
+ isLoadingSignal.set(false);
215
+ }
186
216
  };
187
217
  /**
188
- * Action: Update date range and refetch slots
218
+ * Action: Update date range (effect will handle refetch)
189
219
  */
190
220
  const setDateRange = (newStartDate, newEndDate) => {
191
- const currentStartDate = startDateSignal.get();
192
- const currentEndDate = endDateSignal.get();
221
+ const { start: currentStartDate, end: currentEndDate } = dateRangeSignal.get();
193
222
  if (currentStartDate.getTime() === newStartDate.getTime() &&
194
223
  currentEndDate.getTime() === newEndDate.getTime()) {
195
224
  return;
196
225
  }
197
- startDateSignal.set(newStartDate);
198
- endDateSignal.set(newEndDate);
226
+ dateRangeSignal.set({ start: newStartDate, end: newEndDate });
199
227
  };
200
- const resourcesSignal = signalsService.computed(() => {
201
- return bookingService.serviceSelections.get()[0]?.resources;
202
- });
203
228
  let isFirstRun = true;
204
229
  signalsService.effect(() => {
230
+ filtersSignal.get();
231
+ dateRangeSignal.get();
205
232
  bookingService.timezone.get();
206
- bookingService.location.get();
207
- resourcesSignal.get();
208
- if (!isFirstRun || !config.timeSlots) {
209
- fetchTimeSlots();
233
+ if (isFirstRun) {
234
+ isFirstRun = false;
235
+ if (config.timeSlots) {
236
+ return;
237
+ }
210
238
  }
211
- isFirstRun = false;
239
+ fetchTimeSlots();
212
240
  });
213
241
  // Return service API
214
242
  return {
@@ -218,8 +246,7 @@ export const TimeSlotListService = implementService.withConfig()(TimeSlotListSer
218
246
  error: errorSignal,
219
247
  hasMore: hasMoreSignal,
220
248
  // Configuration Signals (viewMode is internal only)
221
- startDate: startDateSignal,
222
- endDate: endDateSignal,
249
+ dateRange: dateRangeSignal,
223
250
  // Actions
224
251
  actions: {
225
252
  selectSlot,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/headless-bookings",
3
- "version": "0.0.34",
3
+ "version": "0.0.36",
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": "b195003cbc18c0477da02f42dd5e5409c483f2de627832cbdb859424"
75
+ "falconPackageHash": "ecae9bfd11fe7e50bf6e5eeb5ad0b3e9763b64d5cfa1aa1477176483"
76
76
  }