@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.
- package/cjs/dist/react/core/time-slot-list/TimeSlotList.js +3 -2
- package/cjs/dist/react/payment/Payment.js +1 -5
- package/cjs/dist/react/service/Service.d.ts +39 -0
- package/cjs/dist/react/service/Service.js +58 -40
- package/cjs/dist/services/time-slot-list/index.d.ts +1 -1
- package/cjs/dist/services/time-slot-list/time-slot-list.def.d.ts +5 -2
- package/cjs/dist/services/time-slot-list/time-slot-list.js +93 -66
- package/dist/react/core/time-slot-list/TimeSlotList.js +3 -2
- package/dist/react/payment/Payment.js +1 -5
- package/dist/react/service/Service.d.ts +39 -0
- package/dist/react/service/Service.js +58 -40
- package/dist/services/time-slot-list/index.d.ts +1 -1
- package/dist/services/time-slot-list/time-slot-list.def.d.ts +5 -2
- package/dist/services/time-slot-list/time-slot-list.js +93 -66
- package/package.json +2 -2
|
@@ -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:
|
|
111
|
-
endDate:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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:
|
|
611
|
+
onClick: handleClick,
|
|
602
612
|
disabled: !bookable,
|
|
603
613
|
selected,
|
|
604
614
|
bookable,
|
|
605
615
|
requiresAvailability,
|
|
606
|
-
}, children: _jsx("button", { onClick:
|
|
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:
|
|
664
|
+
onClick: handleClick,
|
|
651
665
|
disabled: isDisabled,
|
|
652
666
|
selected,
|
|
653
667
|
bookable,
|
|
654
668
|
requiresAvailability,
|
|
655
|
-
}, children: _jsx("button", { onClick:
|
|
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:
|
|
717
|
+
onClick: handleClick,
|
|
700
718
|
disabled: isDisabled,
|
|
701
719
|
selected,
|
|
702
720
|
bookable,
|
|
703
721
|
requiresAvailability,
|
|
704
|
-
}, children: _jsx("button", { onClick:
|
|
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
|
-
|
|
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
|
|
33
|
-
|
|
57
|
+
const dateRangeSignal = signalsService.signal({
|
|
58
|
+
start: config.startDate,
|
|
59
|
+
end: config.endDate,
|
|
60
|
+
});
|
|
34
61
|
const cursorSignal = signalsService.signal(undefined);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
89
|
+
cursorSignal.set(undefined);
|
|
90
|
+
timeSlotsSignal.set([]);
|
|
87
91
|
try {
|
|
88
92
|
const response = await fetchAvailability({
|
|
89
|
-
services:
|
|
90
|
-
fromLocalDate: startDateSignal.get(),
|
|
91
|
-
toLocalDate: endDateSignal.get(),
|
|
93
|
+
services: servicesSignal.get(),
|
|
92
94
|
timeZone: bookingService.timezone.get(),
|
|
93
95
|
viewMode: viewModeSignal.get(),
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
fromLocalDate: dateRangeSignal.get().start,
|
|
97
|
+
toLocalDate: dateRangeSignal.get().end,
|
|
98
|
+
...filtersSignal.get(),
|
|
96
99
|
});
|
|
97
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
|
218
|
+
* Action: Update date range (effect will handle refetch)
|
|
189
219
|
*/
|
|
190
220
|
const setDateRange = (newStartDate, newEndDate) => {
|
|
191
|
-
const currentStartDate =
|
|
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
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
233
|
+
if (isFirstRun) {
|
|
234
|
+
isFirstRun = false;
|
|
235
|
+
if (config.timeSlots) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
210
238
|
}
|
|
211
|
-
|
|
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
|
-
|
|
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:
|
|
111
|
-
endDate:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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:
|
|
611
|
+
onClick: handleClick,
|
|
602
612
|
disabled: !bookable,
|
|
603
613
|
selected,
|
|
604
614
|
bookable,
|
|
605
615
|
requiresAvailability,
|
|
606
|
-
}, children: _jsx("button", { onClick:
|
|
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:
|
|
664
|
+
onClick: handleClick,
|
|
651
665
|
disabled: isDisabled,
|
|
652
666
|
selected,
|
|
653
667
|
bookable,
|
|
654
668
|
requiresAvailability,
|
|
655
|
-
}, children: _jsx("button", { onClick:
|
|
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:
|
|
717
|
+
onClick: handleClick,
|
|
700
718
|
disabled: isDisabled,
|
|
701
719
|
selected,
|
|
702
720
|
bookable,
|
|
703
721
|
requiresAvailability,
|
|
704
|
-
}, children: _jsx("button", { onClick:
|
|
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
|
-
|
|
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
|
|
33
|
-
|
|
57
|
+
const dateRangeSignal = signalsService.signal({
|
|
58
|
+
start: config.startDate,
|
|
59
|
+
end: config.endDate,
|
|
60
|
+
});
|
|
34
61
|
const cursorSignal = signalsService.signal(undefined);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
89
|
+
cursorSignal.set(undefined);
|
|
90
|
+
timeSlotsSignal.set([]);
|
|
87
91
|
try {
|
|
88
92
|
const response = await fetchAvailability({
|
|
89
|
-
services:
|
|
90
|
-
fromLocalDate: startDateSignal.get(),
|
|
91
|
-
toLocalDate: endDateSignal.get(),
|
|
93
|
+
services: servicesSignal.get(),
|
|
92
94
|
timeZone: bookingService.timezone.get(),
|
|
93
95
|
viewMode: viewModeSignal.get(),
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
fromLocalDate: dateRangeSignal.get().start,
|
|
97
|
+
toLocalDate: dateRangeSignal.get().end,
|
|
98
|
+
...filtersSignal.get(),
|
|
96
99
|
});
|
|
97
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
|
218
|
+
* Action: Update date range (effect will handle refetch)
|
|
189
219
|
*/
|
|
190
220
|
const setDateRange = (newStartDate, newEndDate) => {
|
|
191
|
-
const currentStartDate =
|
|
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
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
233
|
+
if (isFirstRun) {
|
|
234
|
+
isFirstRun = false;
|
|
235
|
+
if (config.timeSlots) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
210
238
|
}
|
|
211
|
-
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
75
|
+
"falconPackageHash": "ecae9bfd11fe7e50bf6e5eeb5ad0b3e9763b64d5cfa1aa1477176483"
|
|
76
76
|
}
|