@wix/headless-restaurants-olo 0.0.45 → 0.0.47
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/FulfillmentDetails.d.ts +73 -0
- package/cjs/dist/react/FulfillmentDetails.js +108 -7
- package/cjs/dist/react/core/FulfillmentDetails.d.ts +40 -0
- package/cjs/dist/react/core/FulfillmentDetails.js +64 -5
- package/cjs/dist/services/fulfillment-details-service.d.ts +5 -0
- package/cjs/dist/services/fulfillment-details-service.js +97 -1
- package/dist/react/FulfillmentDetails.d.ts +73 -0
- package/dist/react/FulfillmentDetails.js +108 -7
- package/dist/react/core/FulfillmentDetails.d.ts +40 -0
- package/dist/react/core/FulfillmentDetails.js +64 -5
- package/dist/services/fulfillment-details-service.d.ts +5 -0
- package/dist/services/fulfillment-details-service.js +97 -1
- package/package.json +6 -6
|
@@ -381,6 +381,8 @@ interface AddressPickerProps extends Omit<React.ComponentPropsWithoutRef<'div'>,
|
|
|
381
381
|
predictionSecondaryTextClassName?: string;
|
|
382
382
|
/** Optional class name for the prediction description */
|
|
383
383
|
predictionDescriptionClassName?: string;
|
|
384
|
+
/** Optional class name for the error message */
|
|
385
|
+
errorClassName?: string;
|
|
384
386
|
/** Minimum input length before showing predictions (default: 2) */
|
|
385
387
|
minInputLength?: number;
|
|
386
388
|
/** Children render prop that receives the address picker props */
|
|
@@ -392,6 +394,7 @@ interface AddressPickerProps extends Omit<React.ComponentPropsWithoutRef<'div'>,
|
|
|
392
394
|
onPredictionSelect: (prediction: AddressPrediction) => Promise<void>;
|
|
393
395
|
isDelivery: boolean;
|
|
394
396
|
dispatchType: DispatchType | null;
|
|
397
|
+
error: string | null;
|
|
395
398
|
}> | React.ReactNode;
|
|
396
399
|
}
|
|
397
400
|
export declare const AddressPicker: React.ForwardRefExoticComponent<AddressPickerProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -441,4 +444,74 @@ interface SaveButtonProps extends Omit<React.ComponentPropsWithoutRef<'button'>,
|
|
|
441
444
|
* ```
|
|
442
445
|
*/
|
|
443
446
|
export declare const SaveButton: React.ForwardRefExoticComponent<SaveButtonProps & React.RefAttributes<HTMLButtonElement>>;
|
|
447
|
+
interface DispatchTypeSelectorProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'children'> {
|
|
448
|
+
/** Child components that will have access to dispatch type context */
|
|
449
|
+
children: React.ReactNode;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Container component for dispatch type selection (pickup or delivery)
|
|
453
|
+
* Provides context for child components to access dispatch type data
|
|
454
|
+
* Does not render if no dispatch types are available
|
|
455
|
+
*
|
|
456
|
+
* @example
|
|
457
|
+
* ```tsx
|
|
458
|
+
* <FulfillmentDetails.DispatchTypeSelector>
|
|
459
|
+
* <FulfillmentDetails.DispatchTypeOptions emptyState={<div>No options</div>}>
|
|
460
|
+
* <FulfillmentDetails.DispatchTypeOptionRepeater>
|
|
461
|
+
* {({ type, isSelected, selectDispatchType }) => (
|
|
462
|
+
* <button onClick={() => selectDispatchType(type)}>
|
|
463
|
+
* {type} {isSelected && '✓'}
|
|
464
|
+
* </button>
|
|
465
|
+
* )}
|
|
466
|
+
* </FulfillmentDetails.DispatchTypeOptionRepeater>
|
|
467
|
+
* </FulfillmentDetails.DispatchTypeOptions>
|
|
468
|
+
* </FulfillmentDetails.DispatchTypeSelector>
|
|
469
|
+
* ```
|
|
470
|
+
*/
|
|
471
|
+
export declare const DispatchTypeSelector: React.ForwardRefExoticComponent<DispatchTypeSelectorProps & React.RefAttributes<HTMLDivElement>>;
|
|
472
|
+
interface DispatchTypeOptionsProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'children'> {
|
|
473
|
+
/** Child components to render when dispatch types are available */
|
|
474
|
+
children: React.ReactNode;
|
|
475
|
+
/** Optional content to display when no dispatch types are available */
|
|
476
|
+
emptyState?: React.ReactNode;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Container for dispatch type options list with empty state support
|
|
480
|
+
*
|
|
481
|
+
* @example
|
|
482
|
+
* ```tsx
|
|
483
|
+
* <FulfillmentDetails.DispatchTypeOptions emptyState={<div>No options available</div>}>
|
|
484
|
+
* <FulfillmentDetails.DispatchTypeOptionRepeater>
|
|
485
|
+
* {({ type }) => <div>{type}</div>}
|
|
486
|
+
* </FulfillmentDetails.DispatchTypeOptionRepeater>
|
|
487
|
+
* </FulfillmentDetails.DispatchTypeOptions>
|
|
488
|
+
* ```
|
|
489
|
+
*/
|
|
490
|
+
export declare const DispatchTypeOptions: React.ForwardRefExoticComponent<DispatchTypeOptionsProps & React.RefAttributes<HTMLDivElement>>;
|
|
491
|
+
interface DispatchTypeOptionRepeaterProps {
|
|
492
|
+
/** Render prop that receives each dispatch type option data */
|
|
493
|
+
children: (props: {
|
|
494
|
+
type: DispatchType;
|
|
495
|
+
isSelected: boolean;
|
|
496
|
+
selectDispatchType: (type: DispatchType) => void;
|
|
497
|
+
}) => React.ReactNode;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Repeater component that renders children for each available dispatch type
|
|
501
|
+
*
|
|
502
|
+
* @example
|
|
503
|
+
* ```tsx
|
|
504
|
+
* <FulfillmentDetails.DispatchTypeOptionRepeater>
|
|
505
|
+
* {({ type, isSelected, selectDispatchType }) => (
|
|
506
|
+
* <button
|
|
507
|
+
* onClick={() => selectDispatchType(type)}
|
|
508
|
+
* data-selected={isSelected}
|
|
509
|
+
* >
|
|
510
|
+
* {type}
|
|
511
|
+
* </button>
|
|
512
|
+
* )}
|
|
513
|
+
* </FulfillmentDetails.DispatchTypeOptionRepeater>
|
|
514
|
+
* ```
|
|
515
|
+
*/
|
|
516
|
+
export declare const DispatchTypeOptionRepeater: React.FC<DispatchTypeOptionRepeaterProps>;
|
|
444
517
|
export {};
|
|
@@ -13,6 +13,14 @@ function useTimeSlotContext() {
|
|
|
13
13
|
}
|
|
14
14
|
return context;
|
|
15
15
|
}
|
|
16
|
+
const DispatchTypeContext = React.createContext(null);
|
|
17
|
+
function useDispatchTypeContext() {
|
|
18
|
+
const context = React.useContext(DispatchTypeContext);
|
|
19
|
+
if (!context) {
|
|
20
|
+
throw new Error('useDispatchTypeContext must be used within a FulfillmentDetails.DispatchTypeSelector component');
|
|
21
|
+
}
|
|
22
|
+
return context;
|
|
23
|
+
}
|
|
16
24
|
// ========================================
|
|
17
25
|
// FULFILLMENT DETAILS HEADLESS COMPONENTS
|
|
18
26
|
// ========================================
|
|
@@ -32,6 +40,9 @@ var TestIds;
|
|
|
32
40
|
TestIds["addressPickerPredictions"] = "fulfillment-address-picker-predictions";
|
|
33
41
|
TestIds["addressPickerPrediction"] = "fulfillment-address-picker-prediction";
|
|
34
42
|
TestIds["saveButton"] = "fulfillment-save-button";
|
|
43
|
+
TestIds["dispatchTypeSelector"] = "fulfillment-dispatch-type-selector";
|
|
44
|
+
TestIds["dispatchTypeOptions"] = "fulfillment-dispatch-type-options";
|
|
45
|
+
TestIds["dispatchTypeOption"] = "fulfillment-dispatch-type-option";
|
|
35
46
|
})(TestIds || (TestIds = {}));
|
|
36
47
|
/**
|
|
37
48
|
* Root headless component for Fulfillment Details
|
|
@@ -482,17 +493,14 @@ const AddressPickerInput = React.memo(({ inputValue, onInputChange, predictions,
|
|
|
482
493
|
prevProps.onPredictionSelect === nextProps.onPredictionSelect &&
|
|
483
494
|
!predictionsChanged);
|
|
484
495
|
});
|
|
485
|
-
export const AddressPicker = React.forwardRef(({ children, asChild, className, placeholder = 'Enter delivery address', inputClassName, predictionsClassName, predictionClassName, predictionMainTextClassName, predictionSecondaryTextClassName, predictionDescriptionClassName, minInputLength = 2, ...rest }, ref) => {
|
|
486
|
-
return (_jsx(CoreFulfillmentDetails.AddressPicker, { children: ({ inputValue, onInputChange, predictions, isLoading, onPredictionSelect, isDelivery, dispatchType, }) => {
|
|
487
|
-
// Don't render anything if not delivery
|
|
488
|
-
if (!isDelivery) {
|
|
489
|
-
return null;
|
|
490
|
-
}
|
|
496
|
+
export const AddressPicker = React.forwardRef(({ children, asChild, className, placeholder = 'Enter delivery address', inputClassName, predictionsClassName, predictionClassName, predictionMainTextClassName, predictionSecondaryTextClassName, predictionDescriptionClassName, errorClassName, minInputLength = 2, ...rest }, ref) => {
|
|
497
|
+
return (_jsx(CoreFulfillmentDetails.AddressPicker, { children: ({ inputValue, onInputChange, predictions, isLoading, onPredictionSelect, isDelivery, dispatchType, error, }) => {
|
|
491
498
|
// Memoize defaultContent to prevent recreation when only predictions change
|
|
492
499
|
// This ensures the input element maintains focus
|
|
493
500
|
// Note: predictions is passed to AddressPickerInput which handles it internally,
|
|
494
501
|
// so we don't need to recreate defaultContent when only predictions change
|
|
495
|
-
|
|
502
|
+
// IMPORTANT: useMemo must be called before any conditional returns to follow Rules of Hooks
|
|
503
|
+
const defaultContent = useMemo(() => (_jsxs("div", { ref: ref, className: className, "data-testid": TestIds.addressPicker, ...rest, children: [_jsx(AddressPickerInput, { inputValue: inputValue, onInputChange: onInputChange, predictions: predictions, isLoading: isLoading, placeholder: placeholder, inputClassName: inputClassName, predictionsClassName: predictionsClassName, predictionClassName: predictionClassName, predictionMainTextClassName: predictionMainTextClassName, predictionSecondaryTextClassName: predictionSecondaryTextClassName, predictionDescriptionClassName: predictionDescriptionClassName, onPredictionSelect: onPredictionSelect }), error && (_jsx("div", { className: errorClassName, "data-testid": `${TestIds.addressPicker}-error`, children: error }))] })),
|
|
496
504
|
// Include predictions in deps so AddressPickerInput receives updates
|
|
497
505
|
// AddressPickerInput is memoized and will only re-render when necessary
|
|
498
506
|
[
|
|
@@ -508,9 +516,15 @@ export const AddressPicker = React.forwardRef(({ children, asChild, className, p
|
|
|
508
516
|
predictionMainTextClassName,
|
|
509
517
|
predictionSecondaryTextClassName,
|
|
510
518
|
predictionDescriptionClassName,
|
|
519
|
+
errorClassName,
|
|
520
|
+
error,
|
|
511
521
|
className,
|
|
512
522
|
// Note: rest and ref are intentionally excluded as they should be stable
|
|
513
523
|
]);
|
|
524
|
+
// Don't render anything if not delivery
|
|
525
|
+
if (!isDelivery) {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
514
528
|
return (_jsx(AsChildSlot, { asChild: asChild, customElement: children, customElementProps: {
|
|
515
529
|
inputValue,
|
|
516
530
|
onInputChange,
|
|
@@ -519,6 +533,7 @@ export const AddressPicker = React.forwardRef(({ children, asChild, className, p
|
|
|
519
533
|
onPredictionSelect,
|
|
520
534
|
isDelivery,
|
|
521
535
|
dispatchType,
|
|
536
|
+
error,
|
|
522
537
|
}, content: defaultContent, children: defaultContent }));
|
|
523
538
|
} }));
|
|
524
539
|
});
|
|
@@ -562,3 +577,89 @@ export const SaveButton = React.forwardRef(({ children, asChild, className, savi
|
|
|
562
577
|
} }));
|
|
563
578
|
});
|
|
564
579
|
SaveButton.displayName = 'FulfillmentDetails.SaveButton';
|
|
580
|
+
/**
|
|
581
|
+
* Container component for dispatch type selection (pickup or delivery)
|
|
582
|
+
* Provides context for child components to access dispatch type data
|
|
583
|
+
* Does not render if no dispatch types are available
|
|
584
|
+
*
|
|
585
|
+
* @example
|
|
586
|
+
* ```tsx
|
|
587
|
+
* <FulfillmentDetails.DispatchTypeSelector>
|
|
588
|
+
* <FulfillmentDetails.DispatchTypeOptions emptyState={<div>No options</div>}>
|
|
589
|
+
* <FulfillmentDetails.DispatchTypeOptionRepeater>
|
|
590
|
+
* {({ type, isSelected, selectDispatchType }) => (
|
|
591
|
+
* <button onClick={() => selectDispatchType(type)}>
|
|
592
|
+
* {type} {isSelected && '✓'}
|
|
593
|
+
* </button>
|
|
594
|
+
* )}
|
|
595
|
+
* </FulfillmentDetails.DispatchTypeOptionRepeater>
|
|
596
|
+
* </FulfillmentDetails.DispatchTypeOptions>
|
|
597
|
+
* </FulfillmentDetails.DispatchTypeSelector>
|
|
598
|
+
* ```
|
|
599
|
+
*/
|
|
600
|
+
export const DispatchTypeSelector = React.forwardRef(({ children, className, ...rest }, ref) => {
|
|
601
|
+
return (_jsx(CoreFulfillmentDetails.DispatchTypeSelector, { children: ({ availableTypes, selectedType, selectDispatchType, hasTypes }) => {
|
|
602
|
+
if (!hasTypes)
|
|
603
|
+
return null;
|
|
604
|
+
const contextValue = {
|
|
605
|
+
availableTypes: availableTypes.map(({ type, isSelected }) => ({
|
|
606
|
+
type,
|
|
607
|
+
isSelected,
|
|
608
|
+
})),
|
|
609
|
+
selectedType,
|
|
610
|
+
selectDispatchType,
|
|
611
|
+
hasTypes,
|
|
612
|
+
};
|
|
613
|
+
return (_jsx(DispatchTypeContext.Provider, { value: contextValue, children: _jsx("div", { ref: ref, className: className, "data-testid": TestIds.dispatchTypeSelector, ...rest, children: children }) }));
|
|
614
|
+
} }));
|
|
615
|
+
});
|
|
616
|
+
DispatchTypeSelector.displayName = 'FulfillmentDetails.DispatchTypeSelector';
|
|
617
|
+
/**
|
|
618
|
+
* Container for dispatch type options list with empty state support
|
|
619
|
+
*
|
|
620
|
+
* @example
|
|
621
|
+
* ```tsx
|
|
622
|
+
* <FulfillmentDetails.DispatchTypeOptions emptyState={<div>No options available</div>}>
|
|
623
|
+
* <FulfillmentDetails.DispatchTypeOptionRepeater>
|
|
624
|
+
* {({ type }) => <div>{type}</div>}
|
|
625
|
+
* </FulfillmentDetails.DispatchTypeOptionRepeater>
|
|
626
|
+
* </FulfillmentDetails.DispatchTypeOptions>
|
|
627
|
+
* ```
|
|
628
|
+
*/
|
|
629
|
+
export const DispatchTypeOptions = React.forwardRef(({ children, emptyState, className, ...rest }, ref) => {
|
|
630
|
+
const { hasTypes } = useDispatchTypeContext();
|
|
631
|
+
if (!hasTypes) {
|
|
632
|
+
return emptyState || null;
|
|
633
|
+
}
|
|
634
|
+
return (_jsx("div", { ref: ref, className: className, "data-testid": TestIds.dispatchTypeOptions, ...rest, children: children }));
|
|
635
|
+
});
|
|
636
|
+
DispatchTypeOptions.displayName = 'FulfillmentDetails.DispatchTypeOptions';
|
|
637
|
+
/**
|
|
638
|
+
* Repeater component that renders children for each available dispatch type
|
|
639
|
+
*
|
|
640
|
+
* @example
|
|
641
|
+
* ```tsx
|
|
642
|
+
* <FulfillmentDetails.DispatchTypeOptionRepeater>
|
|
643
|
+
* {({ type, isSelected, selectDispatchType }) => (
|
|
644
|
+
* <button
|
|
645
|
+
* onClick={() => selectDispatchType(type)}
|
|
646
|
+
* data-selected={isSelected}
|
|
647
|
+
* >
|
|
648
|
+
* {type}
|
|
649
|
+
* </button>
|
|
650
|
+
* )}
|
|
651
|
+
* </FulfillmentDetails.DispatchTypeOptionRepeater>
|
|
652
|
+
* ```
|
|
653
|
+
*/
|
|
654
|
+
export const DispatchTypeOptionRepeater = ({ children }) => {
|
|
655
|
+
const { availableTypes, selectDispatchType, hasTypes } = useDispatchTypeContext();
|
|
656
|
+
if (!hasTypes)
|
|
657
|
+
return null;
|
|
658
|
+
return (_jsx(_Fragment, { children: availableTypes.map(({ type, isSelected }) => (_jsx(React.Fragment, { children: children({
|
|
659
|
+
type,
|
|
660
|
+
isSelected,
|
|
661
|
+
selectDispatchType,
|
|
662
|
+
}) }, type))) }));
|
|
663
|
+
};
|
|
664
|
+
DispatchTypeOptionRepeater.displayName =
|
|
665
|
+
'FulfillmentDetails.DispatchTypeOptionRepeater';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { type FulfillmentDetailsServiceConfig } from '../../services/fulfillment-details-service.js';
|
|
3
3
|
import { DispatchType, FulfillmentTypeEnum, TimeSlot } from '../../types/fulfillments-types.js';
|
|
4
|
+
import { StreetAddress } from '../../types/operation.js';
|
|
4
5
|
export interface RootProps {
|
|
5
6
|
children: React.ReactNode;
|
|
6
7
|
fulfillmentDetailsServiceConfig?: FulfillmentDetailsServiceConfig;
|
|
@@ -97,6 +98,8 @@ export interface DeliveryAddressFields {
|
|
|
97
98
|
country?: string;
|
|
98
99
|
/** Formatted full address string */
|
|
99
100
|
formattedAddress?: string;
|
|
101
|
+
/** Street address */
|
|
102
|
+
streetAddress?: StreetAddress;
|
|
100
103
|
}
|
|
101
104
|
export interface DeliveryAddressProps {
|
|
102
105
|
children: (props: {
|
|
@@ -168,6 +171,8 @@ export interface AddressPickerProps {
|
|
|
168
171
|
isDelivery: boolean;
|
|
169
172
|
/** Current dispatch type */
|
|
170
173
|
dispatchType: DispatchType | null;
|
|
174
|
+
/** Error message from the service */
|
|
175
|
+
error: string | null;
|
|
171
176
|
}) => React.ReactNode;
|
|
172
177
|
}
|
|
173
178
|
/**
|
|
@@ -237,3 +242,38 @@ export interface SaveButtonProps {
|
|
|
237
242
|
* ```
|
|
238
243
|
*/
|
|
239
244
|
export declare const SaveButton: React.FC<SaveButtonProps>;
|
|
245
|
+
export interface DispatchTypeSelectorProps {
|
|
246
|
+
children: (props: {
|
|
247
|
+
availableTypes: Array<{
|
|
248
|
+
type: DispatchType;
|
|
249
|
+
isSelected: boolean;
|
|
250
|
+
}>;
|
|
251
|
+
selectedType: DispatchType | null;
|
|
252
|
+
selectDispatchType: (type: DispatchType) => void;
|
|
253
|
+
hasTypes: boolean;
|
|
254
|
+
}) => React.ReactNode;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Component that provides dispatch type selection (pickup or delivery)
|
|
258
|
+
* Uses FulfillmentDetailsService to manage dispatch type state
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* ```tsx
|
|
262
|
+
* <CoreFulfillmentDetails.DispatchTypeSelector>
|
|
263
|
+
* {({ availableTypes, selectedType, selectDispatchType }) => (
|
|
264
|
+
* <div>
|
|
265
|
+
* {availableTypes.map(({ type, isSelected }) => (
|
|
266
|
+
* <button
|
|
267
|
+
* key={type}
|
|
268
|
+
* onClick={() => selectDispatchType(type)}
|
|
269
|
+
* disabled={isSelected}
|
|
270
|
+
* >
|
|
271
|
+
* {type} {isSelected && '(selected)'}
|
|
272
|
+
* </button>
|
|
273
|
+
* ))}
|
|
274
|
+
* </div>
|
|
275
|
+
* )}
|
|
276
|
+
* </CoreFulfillmentDetails.DispatchTypeSelector>
|
|
277
|
+
* ```
|
|
278
|
+
*/
|
|
279
|
+
export declare const DispatchTypeSelector: React.FC<DispatchTypeSelectorProps>;
|
|
@@ -22,12 +22,19 @@ export const Root = ({ children, fulfillmentDetailsServiceConfig, }) => {
|
|
|
22
22
|
if (!fulfillmentDetailsServiceConfig &&
|
|
23
23
|
oloSettingsService.operation?.get()) {
|
|
24
24
|
const operation = oloSettingsService.operation.get();
|
|
25
|
+
const availableDispatchTypes = fulfillmentsService.availableTypes?.get();
|
|
25
26
|
if (operation) {
|
|
26
|
-
const loadedConfig = loadFulfillmentDetailsServiceConfig(operation
|
|
27
|
+
const loadedConfig = loadFulfillmentDetailsServiceConfig(operation, {
|
|
28
|
+
availableDispatchTypes,
|
|
29
|
+
});
|
|
27
30
|
setConfig(loadedConfig);
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
|
-
}, [
|
|
33
|
+
}, [
|
|
34
|
+
fulfillmentDetailsServiceConfig,
|
|
35
|
+
oloSettingsService.operation,
|
|
36
|
+
fulfillmentsService.availableTypes,
|
|
37
|
+
]);
|
|
31
38
|
if (!config || isFulfillmentsLoading) {
|
|
32
39
|
return null;
|
|
33
40
|
}
|
|
@@ -41,7 +48,7 @@ export const Root = ({ children, fulfillmentDetailsServiceConfig, }) => {
|
|
|
41
48
|
export const AddressName = ({ children }) => {
|
|
42
49
|
const fulfillmentDetailsService = useService(FulfillmentDetailsServiceDefinition);
|
|
43
50
|
const selectedAddress = fulfillmentDetailsService?.address?.get();
|
|
44
|
-
|
|
51
|
+
const error = fulfillmentDetailsService.error?.get() ?? null;
|
|
45
52
|
const dispatchType = fulfillmentDetailsService.dispatchType?.get();
|
|
46
53
|
const addressName = selectedAddress?.formatted;
|
|
47
54
|
const fullAddress = selectedAddress
|
|
@@ -58,7 +65,9 @@ export const AddressName = ({ children }) => {
|
|
|
58
65
|
.filter(Boolean)
|
|
59
66
|
.join(', ')
|
|
60
67
|
: null;
|
|
61
|
-
|
|
68
|
+
console.log('error', error);
|
|
69
|
+
console.log('selectedAddress', selectedAddress);
|
|
70
|
+
const hasAddress = Boolean(selectedAddress && !error);
|
|
62
71
|
return children({
|
|
63
72
|
addressName,
|
|
64
73
|
fullAddress,
|
|
@@ -209,6 +218,7 @@ export const DeliveryAddress = ({ children, }) => {
|
|
|
209
218
|
: null;
|
|
210
219
|
const hasAddress = Boolean(currentAddress);
|
|
211
220
|
const onAddressChange = (newAddress) => {
|
|
221
|
+
console.log('onAddressChange', newAddress);
|
|
212
222
|
fulfillmentDetailsService.setAddress(newAddress);
|
|
213
223
|
};
|
|
214
224
|
const onAddressClear = () => {
|
|
@@ -379,7 +389,9 @@ export const AddressPicker = ({ children }) => {
|
|
|
379
389
|
subdivision: address.subdivision || '',
|
|
380
390
|
postalCode: address.postalCode || '',
|
|
381
391
|
country: address.country || '',
|
|
382
|
-
|
|
392
|
+
// @ts-expect-error
|
|
393
|
+
formattedAddress: address.formattedAddress || '',
|
|
394
|
+
streetAddress: address.streetAddress,
|
|
383
395
|
};
|
|
384
396
|
// TODO - check first available time slot & available dates
|
|
385
397
|
fulfillmentDetailsService.setAddress(addressFields);
|
|
@@ -402,6 +414,7 @@ export const AddressPicker = ({ children }) => {
|
|
|
402
414
|
setIsLoading(false);
|
|
403
415
|
}
|
|
404
416
|
};
|
|
417
|
+
const error = fulfillmentDetailsService.error?.get() ?? null;
|
|
405
418
|
return children({
|
|
406
419
|
inputValue,
|
|
407
420
|
onInputChange,
|
|
@@ -410,6 +423,7 @@ export const AddressPicker = ({ children }) => {
|
|
|
410
423
|
onPredictionSelect,
|
|
411
424
|
isDelivery,
|
|
412
425
|
dispatchType,
|
|
426
|
+
error,
|
|
413
427
|
});
|
|
414
428
|
};
|
|
415
429
|
/**
|
|
@@ -461,3 +475,48 @@ export const SaveButton = ({ children }) => {
|
|
|
461
475
|
dispatchType,
|
|
462
476
|
});
|
|
463
477
|
};
|
|
478
|
+
/**
|
|
479
|
+
* Component that provides dispatch type selection (pickup or delivery)
|
|
480
|
+
* Uses FulfillmentDetailsService to manage dispatch type state
|
|
481
|
+
*
|
|
482
|
+
* @example
|
|
483
|
+
* ```tsx
|
|
484
|
+
* <CoreFulfillmentDetails.DispatchTypeSelector>
|
|
485
|
+
* {({ availableTypes, selectedType, selectDispatchType }) => (
|
|
486
|
+
* <div>
|
|
487
|
+
* {availableTypes.map(({ type, isSelected }) => (
|
|
488
|
+
* <button
|
|
489
|
+
* key={type}
|
|
490
|
+
* onClick={() => selectDispatchType(type)}
|
|
491
|
+
* disabled={isSelected}
|
|
492
|
+
* >
|
|
493
|
+
* {type} {isSelected && '(selected)'}
|
|
494
|
+
* </button>
|
|
495
|
+
* ))}
|
|
496
|
+
* </div>
|
|
497
|
+
* )}
|
|
498
|
+
* </CoreFulfillmentDetails.DispatchTypeSelector>
|
|
499
|
+
* ```
|
|
500
|
+
*/
|
|
501
|
+
export const DispatchTypeSelector = ({ children, }) => {
|
|
502
|
+
const fulfillmentDetailsService = useService(FulfillmentDetailsServiceDefinition);
|
|
503
|
+
const selectedType = fulfillmentDetailsService.dispatchType?.get() ?? null;
|
|
504
|
+
const availableDispatchTypes = fulfillmentDetailsService.availableDispatchTypes?.get() ?? [];
|
|
505
|
+
// Map available dispatch types to the format needed
|
|
506
|
+
const availableTypes = availableDispatchTypes.map((type) => ({
|
|
507
|
+
type,
|
|
508
|
+
isSelected: type === selectedType,
|
|
509
|
+
}));
|
|
510
|
+
console.log('availableTypes', availableTypes);
|
|
511
|
+
const selectDispatchType = (type) => {
|
|
512
|
+
console.log('selectDispatchType', type);
|
|
513
|
+
fulfillmentDetailsService.setDispatchType(type);
|
|
514
|
+
};
|
|
515
|
+
const hasTypes = availableTypes.length > 0;
|
|
516
|
+
return children({
|
|
517
|
+
availableTypes,
|
|
518
|
+
selectedType,
|
|
519
|
+
selectDispatchType,
|
|
520
|
+
hasTypes,
|
|
521
|
+
});
|
|
522
|
+
};
|
|
@@ -36,6 +36,8 @@ export interface FulfillmentDetailsServiceAPI {
|
|
|
36
36
|
availableTimeSlotsForDate: Signal<TimeSlot[]>;
|
|
37
37
|
/** Available time slots filtered by current dispatch type */
|
|
38
38
|
availableTimeSlots: ReadOnlySignal<TimeSlot[]>;
|
|
39
|
+
/** Available dispatch types (PICKUP and/or DELIVERY) based on available time slots */
|
|
40
|
+
availableDispatchTypes: ReadOnlySignal<DispatchType[]>;
|
|
39
41
|
/** Selected scheduling type (ASAP or PREORDER) */
|
|
40
42
|
schedulingType: Signal<FulfillmentTypeEnum | undefined>;
|
|
41
43
|
/**
|
|
@@ -101,6 +103,8 @@ export interface FulfillmentDetailsServiceConfig {
|
|
|
101
103
|
initialAddress?: Address;
|
|
102
104
|
/** Number of days ahead to calculate available dates (default: 30) */
|
|
103
105
|
daysAhead?: number;
|
|
106
|
+
/** Available dispatch types from fulfillments service */
|
|
107
|
+
availableDispatchTypes?: DispatchType[];
|
|
104
108
|
}
|
|
105
109
|
export declare const FulfillmentDetailsServiceDefinition: string & {
|
|
106
110
|
__api: FulfillmentDetailsServiceAPI;
|
|
@@ -124,4 +128,5 @@ export declare function loadFulfillmentDetailsServiceConfig(operation: Operation
|
|
|
124
128
|
initialDispatchType?: DispatchType;
|
|
125
129
|
initialAddress?: Address;
|
|
126
130
|
daysAhead?: number;
|
|
131
|
+
availableDispatchTypes?: DispatchType[];
|
|
127
132
|
}): FulfillmentDetailsServiceConfig;
|
|
@@ -44,6 +44,24 @@ export const FulfillmentDetailsService = implementService.withConfig()(Fulfillme
|
|
|
44
44
|
const availableTimeSlotsForDate = signalsService.signal([]);
|
|
45
45
|
const schedulingType = signalsService.signal(fulfillmentsService.schedulingType?.get());
|
|
46
46
|
// ========================================
|
|
47
|
+
// Sync selectedFulfillment from FulfillmentsService to dispatchType
|
|
48
|
+
// ========================================
|
|
49
|
+
// Sync dispatchType when selectedFulfillment changes in FulfillmentsService
|
|
50
|
+
// Use peek() to read dispatchType without creating a dependency, so the effect
|
|
51
|
+
// only runs when selectedFulfillment changes, not when dispatchType changes
|
|
52
|
+
signalsService.effect(() => {
|
|
53
|
+
const selectedFulfillment = fulfillmentsService.selectedFulfillment?.get();
|
|
54
|
+
const newDispatchType = selectedFulfillment?.type
|
|
55
|
+
? selectedFulfillment.type
|
|
56
|
+
: null;
|
|
57
|
+
// Use peek() to read current value without subscribing to changes
|
|
58
|
+
const currentDispatchType = dispatchType.peek();
|
|
59
|
+
// Only update if different to avoid unnecessary updates and circular updates
|
|
60
|
+
if (currentDispatchType !== newDispatchType) {
|
|
61
|
+
dispatchType.set(newDispatchType);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
// ========================================
|
|
47
65
|
// Helper Functions
|
|
48
66
|
// ========================================
|
|
49
67
|
/**
|
|
@@ -115,8 +133,78 @@ export const FulfillmentDetailsService = implementService.withConfig()(Fulfillme
|
|
|
115
133
|
asapTimeSlot.set(slot);
|
|
116
134
|
}
|
|
117
135
|
};
|
|
136
|
+
const convertAddressInputToAddress = (address) => {
|
|
137
|
+
if (!address)
|
|
138
|
+
return null;
|
|
139
|
+
const {
|
|
140
|
+
// @ts-expect-error
|
|
141
|
+
addressLine,
|
|
142
|
+
// @ts-expect-error
|
|
143
|
+
streetAddress,
|
|
144
|
+
// @ts-expect-error
|
|
145
|
+
formattedAddress,
|
|
146
|
+
// @ts-expect-error
|
|
147
|
+
location, ...rest } = address;
|
|
148
|
+
return {
|
|
149
|
+
streetAddress: streetAddress,
|
|
150
|
+
addressLine1: formattedAddress,
|
|
151
|
+
...rest,
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
const initAddress = async (addr) => {
|
|
155
|
+
isLoading.set(true);
|
|
156
|
+
error.set(null);
|
|
157
|
+
try {
|
|
158
|
+
const commonAddress = convertAddressInputToAddress(addr);
|
|
159
|
+
// 1. Get first available time slot per operation with the selected address
|
|
160
|
+
const firstTimeSlotsResponse = await operationsSDK.calculateFirstAvailableTimeSlotPerFulfillmentType(operation.id, {
|
|
161
|
+
deliveryAddress: commonAddress,
|
|
162
|
+
});
|
|
163
|
+
// Find the time slots for this operation
|
|
164
|
+
const deliveryTimeSlot = firstTimeSlotsResponse.timeslotsPerFulfillmentType?.find((ts) => ts.fulfilmentType === DispatchType.DELIVERY);
|
|
165
|
+
if (!deliveryTimeSlot?.timeSlot) {
|
|
166
|
+
isLoading.set(false);
|
|
167
|
+
// TODO - localize this message
|
|
168
|
+
error.set('No available time slots found for the selected address');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const firstTimeSlot = processTimeSlots([deliveryTimeSlot], DispatchType.DELIVERY)[0];
|
|
172
|
+
// 2. Fetch time slots for the selected address and date (using firstTimeSlot.startTime)
|
|
173
|
+
const targetDate = firstTimeSlot.startTime;
|
|
174
|
+
if (!targetDate) {
|
|
175
|
+
isLoading.set(false);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
await calculateAvailableTimeSlotsForDateFn(targetDate);
|
|
179
|
+
// Set the date and time slot
|
|
180
|
+
date.set(targetDate);
|
|
181
|
+
setTimeslot(firstTimeSlot);
|
|
182
|
+
// 3. Fetch available dates for the selected address
|
|
183
|
+
const startDate = new Date();
|
|
184
|
+
startDate.setHours(0, 0, 0, 0);
|
|
185
|
+
const endDate = new Date();
|
|
186
|
+
endDate.setDate(endDate.getDate() + daysAhead);
|
|
187
|
+
endDate.setHours(23, 59, 59, 999);
|
|
188
|
+
await calculateAvailableDatesInRangeFn(startDate, endDate);
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to initialize address';
|
|
192
|
+
error.set(errorMessage);
|
|
193
|
+
console.error('Error initializing address:', err);
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
isLoading.set(false);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
118
199
|
const setAddress = (addr) => {
|
|
119
200
|
address.set(addr);
|
|
201
|
+
// Initialize address: fetch first time slot, time slots for date, and available dates
|
|
202
|
+
if (addr) {
|
|
203
|
+
initAddress(addr).catch((err) => {
|
|
204
|
+
console.error('Error initializing address:', err);
|
|
205
|
+
error.set('Failed to initialize address');
|
|
206
|
+
});
|
|
207
|
+
}
|
|
120
208
|
};
|
|
121
209
|
const setDate = async (selectedDate) => {
|
|
122
210
|
date.set(selectedDate);
|
|
@@ -129,7 +217,6 @@ export const FulfillmentDetailsService = implementService.withConfig()(Fulfillme
|
|
|
129
217
|
}
|
|
130
218
|
};
|
|
131
219
|
const setDispatchType = (type) => {
|
|
132
|
-
console.log('setDispatchType', type);
|
|
133
220
|
dispatchType.set(type);
|
|
134
221
|
// Re-filter available time slots based on new dispatch type
|
|
135
222
|
const currentDate = date.get();
|
|
@@ -244,6 +331,7 @@ export const FulfillmentDetailsService = implementService.withConfig()(Fulfillme
|
|
|
244
331
|
});
|
|
245
332
|
const { timeslotsPerFulfillmentType } = response;
|
|
246
333
|
const currentDispatchType = dispatchType.get();
|
|
334
|
+
// Process slots filtered by current dispatch type
|
|
247
335
|
const slots = processTimeSlots(timeslotsPerFulfillmentType, currentDispatchType).reverse();
|
|
248
336
|
availableTimeSlotsForDate.set(slots);
|
|
249
337
|
// Auto-select first available slot if none selected
|
|
@@ -308,6 +396,12 @@ export const FulfillmentDetailsService = implementService.withConfig()(Fulfillme
|
|
|
308
396
|
}
|
|
309
397
|
return allTimeSlots.filter((slot) => slot.dispatchType === currentDispatchType);
|
|
310
398
|
});
|
|
399
|
+
/**
|
|
400
|
+
* Available dispatch types from config
|
|
401
|
+
*/
|
|
402
|
+
const availableDispatchTypes = signalsService.computed(() => {
|
|
403
|
+
return config.availableDispatchTypes ?? [];
|
|
404
|
+
});
|
|
311
405
|
// ========================================
|
|
312
406
|
// Return Service API
|
|
313
407
|
// ========================================
|
|
@@ -323,6 +417,7 @@ export const FulfillmentDetailsService = implementService.withConfig()(Fulfillme
|
|
|
323
417
|
availableDates,
|
|
324
418
|
availableTimeSlotsForDate,
|
|
325
419
|
availableTimeSlots,
|
|
420
|
+
availableDispatchTypes,
|
|
326
421
|
schedulingType,
|
|
327
422
|
// Actions
|
|
328
423
|
getTimeslot,
|
|
@@ -355,5 +450,6 @@ export function loadFulfillmentDetailsServiceConfig(operation, options) {
|
|
|
355
450
|
initialDispatchType: options?.initialDispatchType,
|
|
356
451
|
initialAddress: options?.initialAddress,
|
|
357
452
|
daysAhead: options?.daysAhead ?? 30,
|
|
453
|
+
availableDispatchTypes: options?.availableDispatchTypes,
|
|
358
454
|
};
|
|
359
455
|
}
|
|
@@ -381,6 +381,8 @@ interface AddressPickerProps extends Omit<React.ComponentPropsWithoutRef<'div'>,
|
|
|
381
381
|
predictionSecondaryTextClassName?: string;
|
|
382
382
|
/** Optional class name for the prediction description */
|
|
383
383
|
predictionDescriptionClassName?: string;
|
|
384
|
+
/** Optional class name for the error message */
|
|
385
|
+
errorClassName?: string;
|
|
384
386
|
/** Minimum input length before showing predictions (default: 2) */
|
|
385
387
|
minInputLength?: number;
|
|
386
388
|
/** Children render prop that receives the address picker props */
|
|
@@ -392,6 +394,7 @@ interface AddressPickerProps extends Omit<React.ComponentPropsWithoutRef<'div'>,
|
|
|
392
394
|
onPredictionSelect: (prediction: AddressPrediction) => Promise<void>;
|
|
393
395
|
isDelivery: boolean;
|
|
394
396
|
dispatchType: DispatchType | null;
|
|
397
|
+
error: string | null;
|
|
395
398
|
}> | React.ReactNode;
|
|
396
399
|
}
|
|
397
400
|
export declare const AddressPicker: React.ForwardRefExoticComponent<AddressPickerProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -441,4 +444,74 @@ interface SaveButtonProps extends Omit<React.ComponentPropsWithoutRef<'button'>,
|
|
|
441
444
|
* ```
|
|
442
445
|
*/
|
|
443
446
|
export declare const SaveButton: React.ForwardRefExoticComponent<SaveButtonProps & React.RefAttributes<HTMLButtonElement>>;
|
|
447
|
+
interface DispatchTypeSelectorProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'children'> {
|
|
448
|
+
/** Child components that will have access to dispatch type context */
|
|
449
|
+
children: React.ReactNode;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Container component for dispatch type selection (pickup or delivery)
|
|
453
|
+
* Provides context for child components to access dispatch type data
|
|
454
|
+
* Does not render if no dispatch types are available
|
|
455
|
+
*
|
|
456
|
+
* @example
|
|
457
|
+
* ```tsx
|
|
458
|
+
* <FulfillmentDetails.DispatchTypeSelector>
|
|
459
|
+
* <FulfillmentDetails.DispatchTypeOptions emptyState={<div>No options</div>}>
|
|
460
|
+
* <FulfillmentDetails.DispatchTypeOptionRepeater>
|
|
461
|
+
* {({ type, isSelected, selectDispatchType }) => (
|
|
462
|
+
* <button onClick={() => selectDispatchType(type)}>
|
|
463
|
+
* {type} {isSelected && '✓'}
|
|
464
|
+
* </button>
|
|
465
|
+
* )}
|
|
466
|
+
* </FulfillmentDetails.DispatchTypeOptionRepeater>
|
|
467
|
+
* </FulfillmentDetails.DispatchTypeOptions>
|
|
468
|
+
* </FulfillmentDetails.DispatchTypeSelector>
|
|
469
|
+
* ```
|
|
470
|
+
*/
|
|
471
|
+
export declare const DispatchTypeSelector: React.ForwardRefExoticComponent<DispatchTypeSelectorProps & React.RefAttributes<HTMLDivElement>>;
|
|
472
|
+
interface DispatchTypeOptionsProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'children'> {
|
|
473
|
+
/** Child components to render when dispatch types are available */
|
|
474
|
+
children: React.ReactNode;
|
|
475
|
+
/** Optional content to display when no dispatch types are available */
|
|
476
|
+
emptyState?: React.ReactNode;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Container for dispatch type options list with empty state support
|
|
480
|
+
*
|
|
481
|
+
* @example
|
|
482
|
+
* ```tsx
|
|
483
|
+
* <FulfillmentDetails.DispatchTypeOptions emptyState={<div>No options available</div>}>
|
|
484
|
+
* <FulfillmentDetails.DispatchTypeOptionRepeater>
|
|
485
|
+
* {({ type }) => <div>{type}</div>}
|
|
486
|
+
* </FulfillmentDetails.DispatchTypeOptionRepeater>
|
|
487
|
+
* </FulfillmentDetails.DispatchTypeOptions>
|
|
488
|
+
* ```
|
|
489
|
+
*/
|
|
490
|
+
export declare const DispatchTypeOptions: React.ForwardRefExoticComponent<DispatchTypeOptionsProps & React.RefAttributes<HTMLDivElement>>;
|
|
491
|
+
interface DispatchTypeOptionRepeaterProps {
|
|
492
|
+
/** Render prop that receives each dispatch type option data */
|
|
493
|
+
children: (props: {
|
|
494
|
+
type: DispatchType;
|
|
495
|
+
isSelected: boolean;
|
|
496
|
+
selectDispatchType: (type: DispatchType) => void;
|
|
497
|
+
}) => React.ReactNode;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Repeater component that renders children for each available dispatch type
|
|
501
|
+
*
|
|
502
|
+
* @example
|
|
503
|
+
* ```tsx
|
|
504
|
+
* <FulfillmentDetails.DispatchTypeOptionRepeater>
|
|
505
|
+
* {({ type, isSelected, selectDispatchType }) => (
|
|
506
|
+
* <button
|
|
507
|
+
* onClick={() => selectDispatchType(type)}
|
|
508
|
+
* data-selected={isSelected}
|
|
509
|
+
* >
|
|
510
|
+
* {type}
|
|
511
|
+
* </button>
|
|
512
|
+
* )}
|
|
513
|
+
* </FulfillmentDetails.DispatchTypeOptionRepeater>
|
|
514
|
+
* ```
|
|
515
|
+
*/
|
|
516
|
+
export declare const DispatchTypeOptionRepeater: React.FC<DispatchTypeOptionRepeaterProps>;
|
|
444
517
|
export {};
|
|
@@ -13,6 +13,14 @@ function useTimeSlotContext() {
|
|
|
13
13
|
}
|
|
14
14
|
return context;
|
|
15
15
|
}
|
|
16
|
+
const DispatchTypeContext = React.createContext(null);
|
|
17
|
+
function useDispatchTypeContext() {
|
|
18
|
+
const context = React.useContext(DispatchTypeContext);
|
|
19
|
+
if (!context) {
|
|
20
|
+
throw new Error('useDispatchTypeContext must be used within a FulfillmentDetails.DispatchTypeSelector component');
|
|
21
|
+
}
|
|
22
|
+
return context;
|
|
23
|
+
}
|
|
16
24
|
// ========================================
|
|
17
25
|
// FULFILLMENT DETAILS HEADLESS COMPONENTS
|
|
18
26
|
// ========================================
|
|
@@ -32,6 +40,9 @@ var TestIds;
|
|
|
32
40
|
TestIds["addressPickerPredictions"] = "fulfillment-address-picker-predictions";
|
|
33
41
|
TestIds["addressPickerPrediction"] = "fulfillment-address-picker-prediction";
|
|
34
42
|
TestIds["saveButton"] = "fulfillment-save-button";
|
|
43
|
+
TestIds["dispatchTypeSelector"] = "fulfillment-dispatch-type-selector";
|
|
44
|
+
TestIds["dispatchTypeOptions"] = "fulfillment-dispatch-type-options";
|
|
45
|
+
TestIds["dispatchTypeOption"] = "fulfillment-dispatch-type-option";
|
|
35
46
|
})(TestIds || (TestIds = {}));
|
|
36
47
|
/**
|
|
37
48
|
* Root headless component for Fulfillment Details
|
|
@@ -482,17 +493,14 @@ const AddressPickerInput = React.memo(({ inputValue, onInputChange, predictions,
|
|
|
482
493
|
prevProps.onPredictionSelect === nextProps.onPredictionSelect &&
|
|
483
494
|
!predictionsChanged);
|
|
484
495
|
});
|
|
485
|
-
export const AddressPicker = React.forwardRef(({ children, asChild, className, placeholder = 'Enter delivery address', inputClassName, predictionsClassName, predictionClassName, predictionMainTextClassName, predictionSecondaryTextClassName, predictionDescriptionClassName, minInputLength = 2, ...rest }, ref) => {
|
|
486
|
-
return (_jsx(CoreFulfillmentDetails.AddressPicker, { children: ({ inputValue, onInputChange, predictions, isLoading, onPredictionSelect, isDelivery, dispatchType, }) => {
|
|
487
|
-
// Don't render anything if not delivery
|
|
488
|
-
if (!isDelivery) {
|
|
489
|
-
return null;
|
|
490
|
-
}
|
|
496
|
+
export const AddressPicker = React.forwardRef(({ children, asChild, className, placeholder = 'Enter delivery address', inputClassName, predictionsClassName, predictionClassName, predictionMainTextClassName, predictionSecondaryTextClassName, predictionDescriptionClassName, errorClassName, minInputLength = 2, ...rest }, ref) => {
|
|
497
|
+
return (_jsx(CoreFulfillmentDetails.AddressPicker, { children: ({ inputValue, onInputChange, predictions, isLoading, onPredictionSelect, isDelivery, dispatchType, error, }) => {
|
|
491
498
|
// Memoize defaultContent to prevent recreation when only predictions change
|
|
492
499
|
// This ensures the input element maintains focus
|
|
493
500
|
// Note: predictions is passed to AddressPickerInput which handles it internally,
|
|
494
501
|
// so we don't need to recreate defaultContent when only predictions change
|
|
495
|
-
|
|
502
|
+
// IMPORTANT: useMemo must be called before any conditional returns to follow Rules of Hooks
|
|
503
|
+
const defaultContent = useMemo(() => (_jsxs("div", { ref: ref, className: className, "data-testid": TestIds.addressPicker, ...rest, children: [_jsx(AddressPickerInput, { inputValue: inputValue, onInputChange: onInputChange, predictions: predictions, isLoading: isLoading, placeholder: placeholder, inputClassName: inputClassName, predictionsClassName: predictionsClassName, predictionClassName: predictionClassName, predictionMainTextClassName: predictionMainTextClassName, predictionSecondaryTextClassName: predictionSecondaryTextClassName, predictionDescriptionClassName: predictionDescriptionClassName, onPredictionSelect: onPredictionSelect }), error && (_jsx("div", { className: errorClassName, "data-testid": `${TestIds.addressPicker}-error`, children: error }))] })),
|
|
496
504
|
// Include predictions in deps so AddressPickerInput receives updates
|
|
497
505
|
// AddressPickerInput is memoized and will only re-render when necessary
|
|
498
506
|
[
|
|
@@ -508,9 +516,15 @@ export const AddressPicker = React.forwardRef(({ children, asChild, className, p
|
|
|
508
516
|
predictionMainTextClassName,
|
|
509
517
|
predictionSecondaryTextClassName,
|
|
510
518
|
predictionDescriptionClassName,
|
|
519
|
+
errorClassName,
|
|
520
|
+
error,
|
|
511
521
|
className,
|
|
512
522
|
// Note: rest and ref are intentionally excluded as they should be stable
|
|
513
523
|
]);
|
|
524
|
+
// Don't render anything if not delivery
|
|
525
|
+
if (!isDelivery) {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
514
528
|
return (_jsx(AsChildSlot, { asChild: asChild, customElement: children, customElementProps: {
|
|
515
529
|
inputValue,
|
|
516
530
|
onInputChange,
|
|
@@ -519,6 +533,7 @@ export const AddressPicker = React.forwardRef(({ children, asChild, className, p
|
|
|
519
533
|
onPredictionSelect,
|
|
520
534
|
isDelivery,
|
|
521
535
|
dispatchType,
|
|
536
|
+
error,
|
|
522
537
|
}, content: defaultContent, children: defaultContent }));
|
|
523
538
|
} }));
|
|
524
539
|
});
|
|
@@ -562,3 +577,89 @@ export const SaveButton = React.forwardRef(({ children, asChild, className, savi
|
|
|
562
577
|
} }));
|
|
563
578
|
});
|
|
564
579
|
SaveButton.displayName = 'FulfillmentDetails.SaveButton';
|
|
580
|
+
/**
|
|
581
|
+
* Container component for dispatch type selection (pickup or delivery)
|
|
582
|
+
* Provides context for child components to access dispatch type data
|
|
583
|
+
* Does not render if no dispatch types are available
|
|
584
|
+
*
|
|
585
|
+
* @example
|
|
586
|
+
* ```tsx
|
|
587
|
+
* <FulfillmentDetails.DispatchTypeSelector>
|
|
588
|
+
* <FulfillmentDetails.DispatchTypeOptions emptyState={<div>No options</div>}>
|
|
589
|
+
* <FulfillmentDetails.DispatchTypeOptionRepeater>
|
|
590
|
+
* {({ type, isSelected, selectDispatchType }) => (
|
|
591
|
+
* <button onClick={() => selectDispatchType(type)}>
|
|
592
|
+
* {type} {isSelected && '✓'}
|
|
593
|
+
* </button>
|
|
594
|
+
* )}
|
|
595
|
+
* </FulfillmentDetails.DispatchTypeOptionRepeater>
|
|
596
|
+
* </FulfillmentDetails.DispatchTypeOptions>
|
|
597
|
+
* </FulfillmentDetails.DispatchTypeSelector>
|
|
598
|
+
* ```
|
|
599
|
+
*/
|
|
600
|
+
export const DispatchTypeSelector = React.forwardRef(({ children, className, ...rest }, ref) => {
|
|
601
|
+
return (_jsx(CoreFulfillmentDetails.DispatchTypeSelector, { children: ({ availableTypes, selectedType, selectDispatchType, hasTypes }) => {
|
|
602
|
+
if (!hasTypes)
|
|
603
|
+
return null;
|
|
604
|
+
const contextValue = {
|
|
605
|
+
availableTypes: availableTypes.map(({ type, isSelected }) => ({
|
|
606
|
+
type,
|
|
607
|
+
isSelected,
|
|
608
|
+
})),
|
|
609
|
+
selectedType,
|
|
610
|
+
selectDispatchType,
|
|
611
|
+
hasTypes,
|
|
612
|
+
};
|
|
613
|
+
return (_jsx(DispatchTypeContext.Provider, { value: contextValue, children: _jsx("div", { ref: ref, className: className, "data-testid": TestIds.dispatchTypeSelector, ...rest, children: children }) }));
|
|
614
|
+
} }));
|
|
615
|
+
});
|
|
616
|
+
DispatchTypeSelector.displayName = 'FulfillmentDetails.DispatchTypeSelector';
|
|
617
|
+
/**
|
|
618
|
+
* Container for dispatch type options list with empty state support
|
|
619
|
+
*
|
|
620
|
+
* @example
|
|
621
|
+
* ```tsx
|
|
622
|
+
* <FulfillmentDetails.DispatchTypeOptions emptyState={<div>No options available</div>}>
|
|
623
|
+
* <FulfillmentDetails.DispatchTypeOptionRepeater>
|
|
624
|
+
* {({ type }) => <div>{type}</div>}
|
|
625
|
+
* </FulfillmentDetails.DispatchTypeOptionRepeater>
|
|
626
|
+
* </FulfillmentDetails.DispatchTypeOptions>
|
|
627
|
+
* ```
|
|
628
|
+
*/
|
|
629
|
+
export const DispatchTypeOptions = React.forwardRef(({ children, emptyState, className, ...rest }, ref) => {
|
|
630
|
+
const { hasTypes } = useDispatchTypeContext();
|
|
631
|
+
if (!hasTypes) {
|
|
632
|
+
return emptyState || null;
|
|
633
|
+
}
|
|
634
|
+
return (_jsx("div", { ref: ref, className: className, "data-testid": TestIds.dispatchTypeOptions, ...rest, children: children }));
|
|
635
|
+
});
|
|
636
|
+
DispatchTypeOptions.displayName = 'FulfillmentDetails.DispatchTypeOptions';
|
|
637
|
+
/**
|
|
638
|
+
* Repeater component that renders children for each available dispatch type
|
|
639
|
+
*
|
|
640
|
+
* @example
|
|
641
|
+
* ```tsx
|
|
642
|
+
* <FulfillmentDetails.DispatchTypeOptionRepeater>
|
|
643
|
+
* {({ type, isSelected, selectDispatchType }) => (
|
|
644
|
+
* <button
|
|
645
|
+
* onClick={() => selectDispatchType(type)}
|
|
646
|
+
* data-selected={isSelected}
|
|
647
|
+
* >
|
|
648
|
+
* {type}
|
|
649
|
+
* </button>
|
|
650
|
+
* )}
|
|
651
|
+
* </FulfillmentDetails.DispatchTypeOptionRepeater>
|
|
652
|
+
* ```
|
|
653
|
+
*/
|
|
654
|
+
export const DispatchTypeOptionRepeater = ({ children }) => {
|
|
655
|
+
const { availableTypes, selectDispatchType, hasTypes } = useDispatchTypeContext();
|
|
656
|
+
if (!hasTypes)
|
|
657
|
+
return null;
|
|
658
|
+
return (_jsx(_Fragment, { children: availableTypes.map(({ type, isSelected }) => (_jsx(React.Fragment, { children: children({
|
|
659
|
+
type,
|
|
660
|
+
isSelected,
|
|
661
|
+
selectDispatchType,
|
|
662
|
+
}) }, type))) }));
|
|
663
|
+
};
|
|
664
|
+
DispatchTypeOptionRepeater.displayName =
|
|
665
|
+
'FulfillmentDetails.DispatchTypeOptionRepeater';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { type FulfillmentDetailsServiceConfig } from '../../services/fulfillment-details-service.js';
|
|
3
3
|
import { DispatchType, FulfillmentTypeEnum, TimeSlot } from '../../types/fulfillments-types.js';
|
|
4
|
+
import { StreetAddress } from '../../types/operation.js';
|
|
4
5
|
export interface RootProps {
|
|
5
6
|
children: React.ReactNode;
|
|
6
7
|
fulfillmentDetailsServiceConfig?: FulfillmentDetailsServiceConfig;
|
|
@@ -97,6 +98,8 @@ export interface DeliveryAddressFields {
|
|
|
97
98
|
country?: string;
|
|
98
99
|
/** Formatted full address string */
|
|
99
100
|
formattedAddress?: string;
|
|
101
|
+
/** Street address */
|
|
102
|
+
streetAddress?: StreetAddress;
|
|
100
103
|
}
|
|
101
104
|
export interface DeliveryAddressProps {
|
|
102
105
|
children: (props: {
|
|
@@ -168,6 +171,8 @@ export interface AddressPickerProps {
|
|
|
168
171
|
isDelivery: boolean;
|
|
169
172
|
/** Current dispatch type */
|
|
170
173
|
dispatchType: DispatchType | null;
|
|
174
|
+
/** Error message from the service */
|
|
175
|
+
error: string | null;
|
|
171
176
|
}) => React.ReactNode;
|
|
172
177
|
}
|
|
173
178
|
/**
|
|
@@ -237,3 +242,38 @@ export interface SaveButtonProps {
|
|
|
237
242
|
* ```
|
|
238
243
|
*/
|
|
239
244
|
export declare const SaveButton: React.FC<SaveButtonProps>;
|
|
245
|
+
export interface DispatchTypeSelectorProps {
|
|
246
|
+
children: (props: {
|
|
247
|
+
availableTypes: Array<{
|
|
248
|
+
type: DispatchType;
|
|
249
|
+
isSelected: boolean;
|
|
250
|
+
}>;
|
|
251
|
+
selectedType: DispatchType | null;
|
|
252
|
+
selectDispatchType: (type: DispatchType) => void;
|
|
253
|
+
hasTypes: boolean;
|
|
254
|
+
}) => React.ReactNode;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Component that provides dispatch type selection (pickup or delivery)
|
|
258
|
+
* Uses FulfillmentDetailsService to manage dispatch type state
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* ```tsx
|
|
262
|
+
* <CoreFulfillmentDetails.DispatchTypeSelector>
|
|
263
|
+
* {({ availableTypes, selectedType, selectDispatchType }) => (
|
|
264
|
+
* <div>
|
|
265
|
+
* {availableTypes.map(({ type, isSelected }) => (
|
|
266
|
+
* <button
|
|
267
|
+
* key={type}
|
|
268
|
+
* onClick={() => selectDispatchType(type)}
|
|
269
|
+
* disabled={isSelected}
|
|
270
|
+
* >
|
|
271
|
+
* {type} {isSelected && '(selected)'}
|
|
272
|
+
* </button>
|
|
273
|
+
* ))}
|
|
274
|
+
* </div>
|
|
275
|
+
* )}
|
|
276
|
+
* </CoreFulfillmentDetails.DispatchTypeSelector>
|
|
277
|
+
* ```
|
|
278
|
+
*/
|
|
279
|
+
export declare const DispatchTypeSelector: React.FC<DispatchTypeSelectorProps>;
|
|
@@ -22,12 +22,19 @@ export const Root = ({ children, fulfillmentDetailsServiceConfig, }) => {
|
|
|
22
22
|
if (!fulfillmentDetailsServiceConfig &&
|
|
23
23
|
oloSettingsService.operation?.get()) {
|
|
24
24
|
const operation = oloSettingsService.operation.get();
|
|
25
|
+
const availableDispatchTypes = fulfillmentsService.availableTypes?.get();
|
|
25
26
|
if (operation) {
|
|
26
|
-
const loadedConfig = loadFulfillmentDetailsServiceConfig(operation
|
|
27
|
+
const loadedConfig = loadFulfillmentDetailsServiceConfig(operation, {
|
|
28
|
+
availableDispatchTypes,
|
|
29
|
+
});
|
|
27
30
|
setConfig(loadedConfig);
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
|
-
}, [
|
|
33
|
+
}, [
|
|
34
|
+
fulfillmentDetailsServiceConfig,
|
|
35
|
+
oloSettingsService.operation,
|
|
36
|
+
fulfillmentsService.availableTypes,
|
|
37
|
+
]);
|
|
31
38
|
if (!config || isFulfillmentsLoading) {
|
|
32
39
|
return null;
|
|
33
40
|
}
|
|
@@ -41,7 +48,7 @@ export const Root = ({ children, fulfillmentDetailsServiceConfig, }) => {
|
|
|
41
48
|
export const AddressName = ({ children }) => {
|
|
42
49
|
const fulfillmentDetailsService = useService(FulfillmentDetailsServiceDefinition);
|
|
43
50
|
const selectedAddress = fulfillmentDetailsService?.address?.get();
|
|
44
|
-
|
|
51
|
+
const error = fulfillmentDetailsService.error?.get() ?? null;
|
|
45
52
|
const dispatchType = fulfillmentDetailsService.dispatchType?.get();
|
|
46
53
|
const addressName = selectedAddress?.formatted;
|
|
47
54
|
const fullAddress = selectedAddress
|
|
@@ -58,7 +65,9 @@ export const AddressName = ({ children }) => {
|
|
|
58
65
|
.filter(Boolean)
|
|
59
66
|
.join(', ')
|
|
60
67
|
: null;
|
|
61
|
-
|
|
68
|
+
console.log('error', error);
|
|
69
|
+
console.log('selectedAddress', selectedAddress);
|
|
70
|
+
const hasAddress = Boolean(selectedAddress && !error);
|
|
62
71
|
return children({
|
|
63
72
|
addressName,
|
|
64
73
|
fullAddress,
|
|
@@ -209,6 +218,7 @@ export const DeliveryAddress = ({ children, }) => {
|
|
|
209
218
|
: null;
|
|
210
219
|
const hasAddress = Boolean(currentAddress);
|
|
211
220
|
const onAddressChange = (newAddress) => {
|
|
221
|
+
console.log('onAddressChange', newAddress);
|
|
212
222
|
fulfillmentDetailsService.setAddress(newAddress);
|
|
213
223
|
};
|
|
214
224
|
const onAddressClear = () => {
|
|
@@ -379,7 +389,9 @@ export const AddressPicker = ({ children }) => {
|
|
|
379
389
|
subdivision: address.subdivision || '',
|
|
380
390
|
postalCode: address.postalCode || '',
|
|
381
391
|
country: address.country || '',
|
|
382
|
-
|
|
392
|
+
// @ts-expect-error
|
|
393
|
+
formattedAddress: address.formattedAddress || '',
|
|
394
|
+
streetAddress: address.streetAddress,
|
|
383
395
|
};
|
|
384
396
|
// TODO - check first available time slot & available dates
|
|
385
397
|
fulfillmentDetailsService.setAddress(addressFields);
|
|
@@ -402,6 +414,7 @@ export const AddressPicker = ({ children }) => {
|
|
|
402
414
|
setIsLoading(false);
|
|
403
415
|
}
|
|
404
416
|
};
|
|
417
|
+
const error = fulfillmentDetailsService.error?.get() ?? null;
|
|
405
418
|
return children({
|
|
406
419
|
inputValue,
|
|
407
420
|
onInputChange,
|
|
@@ -410,6 +423,7 @@ export const AddressPicker = ({ children }) => {
|
|
|
410
423
|
onPredictionSelect,
|
|
411
424
|
isDelivery,
|
|
412
425
|
dispatchType,
|
|
426
|
+
error,
|
|
413
427
|
});
|
|
414
428
|
};
|
|
415
429
|
/**
|
|
@@ -461,3 +475,48 @@ export const SaveButton = ({ children }) => {
|
|
|
461
475
|
dispatchType,
|
|
462
476
|
});
|
|
463
477
|
};
|
|
478
|
+
/**
|
|
479
|
+
* Component that provides dispatch type selection (pickup or delivery)
|
|
480
|
+
* Uses FulfillmentDetailsService to manage dispatch type state
|
|
481
|
+
*
|
|
482
|
+
* @example
|
|
483
|
+
* ```tsx
|
|
484
|
+
* <CoreFulfillmentDetails.DispatchTypeSelector>
|
|
485
|
+
* {({ availableTypes, selectedType, selectDispatchType }) => (
|
|
486
|
+
* <div>
|
|
487
|
+
* {availableTypes.map(({ type, isSelected }) => (
|
|
488
|
+
* <button
|
|
489
|
+
* key={type}
|
|
490
|
+
* onClick={() => selectDispatchType(type)}
|
|
491
|
+
* disabled={isSelected}
|
|
492
|
+
* >
|
|
493
|
+
* {type} {isSelected && '(selected)'}
|
|
494
|
+
* </button>
|
|
495
|
+
* ))}
|
|
496
|
+
* </div>
|
|
497
|
+
* )}
|
|
498
|
+
* </CoreFulfillmentDetails.DispatchTypeSelector>
|
|
499
|
+
* ```
|
|
500
|
+
*/
|
|
501
|
+
export const DispatchTypeSelector = ({ children, }) => {
|
|
502
|
+
const fulfillmentDetailsService = useService(FulfillmentDetailsServiceDefinition);
|
|
503
|
+
const selectedType = fulfillmentDetailsService.dispatchType?.get() ?? null;
|
|
504
|
+
const availableDispatchTypes = fulfillmentDetailsService.availableDispatchTypes?.get() ?? [];
|
|
505
|
+
// Map available dispatch types to the format needed
|
|
506
|
+
const availableTypes = availableDispatchTypes.map((type) => ({
|
|
507
|
+
type,
|
|
508
|
+
isSelected: type === selectedType,
|
|
509
|
+
}));
|
|
510
|
+
console.log('availableTypes', availableTypes);
|
|
511
|
+
const selectDispatchType = (type) => {
|
|
512
|
+
console.log('selectDispatchType', type);
|
|
513
|
+
fulfillmentDetailsService.setDispatchType(type);
|
|
514
|
+
};
|
|
515
|
+
const hasTypes = availableTypes.length > 0;
|
|
516
|
+
return children({
|
|
517
|
+
availableTypes,
|
|
518
|
+
selectedType,
|
|
519
|
+
selectDispatchType,
|
|
520
|
+
hasTypes,
|
|
521
|
+
});
|
|
522
|
+
};
|
|
@@ -36,6 +36,8 @@ export interface FulfillmentDetailsServiceAPI {
|
|
|
36
36
|
availableTimeSlotsForDate: Signal<TimeSlot[]>;
|
|
37
37
|
/** Available time slots filtered by current dispatch type */
|
|
38
38
|
availableTimeSlots: ReadOnlySignal<TimeSlot[]>;
|
|
39
|
+
/** Available dispatch types (PICKUP and/or DELIVERY) based on available time slots */
|
|
40
|
+
availableDispatchTypes: ReadOnlySignal<DispatchType[]>;
|
|
39
41
|
/** Selected scheduling type (ASAP or PREORDER) */
|
|
40
42
|
schedulingType: Signal<FulfillmentTypeEnum | undefined>;
|
|
41
43
|
/**
|
|
@@ -101,6 +103,8 @@ export interface FulfillmentDetailsServiceConfig {
|
|
|
101
103
|
initialAddress?: Address;
|
|
102
104
|
/** Number of days ahead to calculate available dates (default: 30) */
|
|
103
105
|
daysAhead?: number;
|
|
106
|
+
/** Available dispatch types from fulfillments service */
|
|
107
|
+
availableDispatchTypes?: DispatchType[];
|
|
104
108
|
}
|
|
105
109
|
export declare const FulfillmentDetailsServiceDefinition: string & {
|
|
106
110
|
__api: FulfillmentDetailsServiceAPI;
|
|
@@ -124,4 +128,5 @@ export declare function loadFulfillmentDetailsServiceConfig(operation: Operation
|
|
|
124
128
|
initialDispatchType?: DispatchType;
|
|
125
129
|
initialAddress?: Address;
|
|
126
130
|
daysAhead?: number;
|
|
131
|
+
availableDispatchTypes?: DispatchType[];
|
|
127
132
|
}): FulfillmentDetailsServiceConfig;
|
|
@@ -44,6 +44,24 @@ export const FulfillmentDetailsService = implementService.withConfig()(Fulfillme
|
|
|
44
44
|
const availableTimeSlotsForDate = signalsService.signal([]);
|
|
45
45
|
const schedulingType = signalsService.signal(fulfillmentsService.schedulingType?.get());
|
|
46
46
|
// ========================================
|
|
47
|
+
// Sync selectedFulfillment from FulfillmentsService to dispatchType
|
|
48
|
+
// ========================================
|
|
49
|
+
// Sync dispatchType when selectedFulfillment changes in FulfillmentsService
|
|
50
|
+
// Use peek() to read dispatchType without creating a dependency, so the effect
|
|
51
|
+
// only runs when selectedFulfillment changes, not when dispatchType changes
|
|
52
|
+
signalsService.effect(() => {
|
|
53
|
+
const selectedFulfillment = fulfillmentsService.selectedFulfillment?.get();
|
|
54
|
+
const newDispatchType = selectedFulfillment?.type
|
|
55
|
+
? selectedFulfillment.type
|
|
56
|
+
: null;
|
|
57
|
+
// Use peek() to read current value without subscribing to changes
|
|
58
|
+
const currentDispatchType = dispatchType.peek();
|
|
59
|
+
// Only update if different to avoid unnecessary updates and circular updates
|
|
60
|
+
if (currentDispatchType !== newDispatchType) {
|
|
61
|
+
dispatchType.set(newDispatchType);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
// ========================================
|
|
47
65
|
// Helper Functions
|
|
48
66
|
// ========================================
|
|
49
67
|
/**
|
|
@@ -115,8 +133,78 @@ export const FulfillmentDetailsService = implementService.withConfig()(Fulfillme
|
|
|
115
133
|
asapTimeSlot.set(slot);
|
|
116
134
|
}
|
|
117
135
|
};
|
|
136
|
+
const convertAddressInputToAddress = (address) => {
|
|
137
|
+
if (!address)
|
|
138
|
+
return null;
|
|
139
|
+
const {
|
|
140
|
+
// @ts-expect-error
|
|
141
|
+
addressLine,
|
|
142
|
+
// @ts-expect-error
|
|
143
|
+
streetAddress,
|
|
144
|
+
// @ts-expect-error
|
|
145
|
+
formattedAddress,
|
|
146
|
+
// @ts-expect-error
|
|
147
|
+
location, ...rest } = address;
|
|
148
|
+
return {
|
|
149
|
+
streetAddress: streetAddress,
|
|
150
|
+
addressLine1: formattedAddress,
|
|
151
|
+
...rest,
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
const initAddress = async (addr) => {
|
|
155
|
+
isLoading.set(true);
|
|
156
|
+
error.set(null);
|
|
157
|
+
try {
|
|
158
|
+
const commonAddress = convertAddressInputToAddress(addr);
|
|
159
|
+
// 1. Get first available time slot per operation with the selected address
|
|
160
|
+
const firstTimeSlotsResponse = await operationsSDK.calculateFirstAvailableTimeSlotPerFulfillmentType(operation.id, {
|
|
161
|
+
deliveryAddress: commonAddress,
|
|
162
|
+
});
|
|
163
|
+
// Find the time slots for this operation
|
|
164
|
+
const deliveryTimeSlot = firstTimeSlotsResponse.timeslotsPerFulfillmentType?.find((ts) => ts.fulfilmentType === DispatchType.DELIVERY);
|
|
165
|
+
if (!deliveryTimeSlot?.timeSlot) {
|
|
166
|
+
isLoading.set(false);
|
|
167
|
+
// TODO - localize this message
|
|
168
|
+
error.set('No available time slots found for the selected address');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const firstTimeSlot = processTimeSlots([deliveryTimeSlot], DispatchType.DELIVERY)[0];
|
|
172
|
+
// 2. Fetch time slots for the selected address and date (using firstTimeSlot.startTime)
|
|
173
|
+
const targetDate = firstTimeSlot.startTime;
|
|
174
|
+
if (!targetDate) {
|
|
175
|
+
isLoading.set(false);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
await calculateAvailableTimeSlotsForDateFn(targetDate);
|
|
179
|
+
// Set the date and time slot
|
|
180
|
+
date.set(targetDate);
|
|
181
|
+
setTimeslot(firstTimeSlot);
|
|
182
|
+
// 3. Fetch available dates for the selected address
|
|
183
|
+
const startDate = new Date();
|
|
184
|
+
startDate.setHours(0, 0, 0, 0);
|
|
185
|
+
const endDate = new Date();
|
|
186
|
+
endDate.setDate(endDate.getDate() + daysAhead);
|
|
187
|
+
endDate.setHours(23, 59, 59, 999);
|
|
188
|
+
await calculateAvailableDatesInRangeFn(startDate, endDate);
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to initialize address';
|
|
192
|
+
error.set(errorMessage);
|
|
193
|
+
console.error('Error initializing address:', err);
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
isLoading.set(false);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
118
199
|
const setAddress = (addr) => {
|
|
119
200
|
address.set(addr);
|
|
201
|
+
// Initialize address: fetch first time slot, time slots for date, and available dates
|
|
202
|
+
if (addr) {
|
|
203
|
+
initAddress(addr).catch((err) => {
|
|
204
|
+
console.error('Error initializing address:', err);
|
|
205
|
+
error.set('Failed to initialize address');
|
|
206
|
+
});
|
|
207
|
+
}
|
|
120
208
|
};
|
|
121
209
|
const setDate = async (selectedDate) => {
|
|
122
210
|
date.set(selectedDate);
|
|
@@ -129,7 +217,6 @@ export const FulfillmentDetailsService = implementService.withConfig()(Fulfillme
|
|
|
129
217
|
}
|
|
130
218
|
};
|
|
131
219
|
const setDispatchType = (type) => {
|
|
132
|
-
console.log('setDispatchType', type);
|
|
133
220
|
dispatchType.set(type);
|
|
134
221
|
// Re-filter available time slots based on new dispatch type
|
|
135
222
|
const currentDate = date.get();
|
|
@@ -244,6 +331,7 @@ export const FulfillmentDetailsService = implementService.withConfig()(Fulfillme
|
|
|
244
331
|
});
|
|
245
332
|
const { timeslotsPerFulfillmentType } = response;
|
|
246
333
|
const currentDispatchType = dispatchType.get();
|
|
334
|
+
// Process slots filtered by current dispatch type
|
|
247
335
|
const slots = processTimeSlots(timeslotsPerFulfillmentType, currentDispatchType).reverse();
|
|
248
336
|
availableTimeSlotsForDate.set(slots);
|
|
249
337
|
// Auto-select first available slot if none selected
|
|
@@ -308,6 +396,12 @@ export const FulfillmentDetailsService = implementService.withConfig()(Fulfillme
|
|
|
308
396
|
}
|
|
309
397
|
return allTimeSlots.filter((slot) => slot.dispatchType === currentDispatchType);
|
|
310
398
|
});
|
|
399
|
+
/**
|
|
400
|
+
* Available dispatch types from config
|
|
401
|
+
*/
|
|
402
|
+
const availableDispatchTypes = signalsService.computed(() => {
|
|
403
|
+
return config.availableDispatchTypes ?? [];
|
|
404
|
+
});
|
|
311
405
|
// ========================================
|
|
312
406
|
// Return Service API
|
|
313
407
|
// ========================================
|
|
@@ -323,6 +417,7 @@ export const FulfillmentDetailsService = implementService.withConfig()(Fulfillme
|
|
|
323
417
|
availableDates,
|
|
324
418
|
availableTimeSlotsForDate,
|
|
325
419
|
availableTimeSlots,
|
|
420
|
+
availableDispatchTypes,
|
|
326
421
|
schedulingType,
|
|
327
422
|
// Actions
|
|
328
423
|
getTimeslot,
|
|
@@ -355,5 +450,6 @@ export function loadFulfillmentDetailsServiceConfig(operation, options) {
|
|
|
355
450
|
initialDispatchType: options?.initialDispatchType,
|
|
356
451
|
initialAddress: options?.initialAddress,
|
|
357
452
|
daysAhead: options?.daysAhead ?? 30,
|
|
453
|
+
availableDispatchTypes: options?.availableDispatchTypes,
|
|
358
454
|
};
|
|
359
455
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/headless-restaurants-olo",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.47",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -59,14 +59,14 @@
|
|
|
59
59
|
"@wix/ecom": "^1.0.1560",
|
|
60
60
|
"@wix/essentials": "^1.0.0",
|
|
61
61
|
"@wix/headless-components": "0.0.35",
|
|
62
|
-
"@wix/headless-media": "0.0.
|
|
63
|
-
"@wix/headless-restaurants-menus": "0.0.
|
|
62
|
+
"@wix/headless-media": "0.0.19",
|
|
63
|
+
"@wix/headless-restaurants-menus": "0.0.23",
|
|
64
64
|
"@wix/headless-utils": "0.0.8",
|
|
65
65
|
"@wix/redirects": "^1.0.0",
|
|
66
66
|
"@wix/restaurants": "^1.0.396",
|
|
67
67
|
"@wix/sdk": "^1.15.24",
|
|
68
|
-
"@wix/services-definitions": "^0.1
|
|
69
|
-
"@wix/services-manager-react": "^0.
|
|
68
|
+
"@wix/services-definitions": "^1.0.1",
|
|
69
|
+
"@wix/services-manager-react": "^1.0.2"
|
|
70
70
|
},
|
|
71
71
|
"publishConfig": {
|
|
72
72
|
"registry": "https://registry.npmjs.org/",
|
|
@@ -78,5 +78,5 @@
|
|
|
78
78
|
"groupId": "com.wixpress.headless-components"
|
|
79
79
|
}
|
|
80
80
|
},
|
|
81
|
-
"falconPackageHash": "
|
|
81
|
+
"falconPackageHash": "1353a228f4ad617d9b95f1e571f68b1e8c9791b4420c948df62df81f"
|
|
82
82
|
}
|