@evenicanpm/storefront-core 1.0.0 → 1.0.1
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/package.json +3 -2
- package/src/api-manager/datasources/d365/d365-address.datasource.ts +1 -1
- package/src/api-manager/datasources/d365/d365-user.datasource.ts +0 -19
- package/src/api-manager/datasources/e4/e4-cart.datasource.ts +2 -2
- package/src/api-manager/datasources/e4/e4-product.datasource.ts +19 -4
- package/src/api-manager/datasources/e4/e4.datasource.ts +1 -2
- package/src/api-manager/datasources/e4/e4.remaps.ts +24 -0
- package/src/api-manager/datasources/e4/e4.translator.ts +17 -0
- package/src/api-manager/datasources/e4/{e4-user.datasource.ts → user/e4-user.datasource.ts} +5 -7
- package/src/api-manager/datasources/e4/user/e4-user.remaps.ts +4 -0
- package/src/api-manager/datasources/e4/user/e4-user.translator.ts +10 -0
- package/src/api-manager/index.ts +21 -6
- package/src/api-manager/schemas/product.schema.ts +1 -1
- package/src/api-manager/services/address/queries/get-states.ts +1 -1
- package/src/api-manager/services/create-query.ts +10 -3
- package/src/api-manager/types/Datasource.ts +9 -9
- package/src/components/Blocks/Components/ProductCarousel/index.tsx +6 -6
- package/src/components/_components/navbar/category-based-menu/components/categories.tsx +1 -1
- package/src/components/_components/navbar/mega-menu/mega-menu.tsx +1 -1
- package/src/components/_components/navbar/nav-list/nav-item-child.tsx +2 -2
- package/src/components/_components/product-dimensions/product-dimensions.tsx +114 -0
- package/src/components/_components/product-quantity-buttons/product-quantity-buttons.tsx +88 -0
- package/src/components/_components/section-header/section-header.tsx +1 -1
- package/src/components/_components/settings/setting.tsx +1 -1
- package/src/components/_components/wishlist-modal/TODO.md +10 -0
- package/src/components/_components/wishlist-modal/add-to-wishlist-button.tsx +68 -0
- package/src/components/_components/wishlist-modal/add-to-wishlist-dialog.tsx +161 -0
- package/src/components/_components/wishlist-modal/remove-from-wishlist-dialog.tsx +174 -0
- package/src/components/_components/wishlist-modal/wishlist-modal-item.tsx +65 -0
- package/src/components/countries-input.tsx +1 -0
- package/src/hooks/use-cart.tsx +82 -0
- package/src/hooks/use-variants.tsx +85 -0
- package/src/pages/account/addresses/address-form.tsx +289 -0
- package/src/pages/account/addresses/address-item.tsx +104 -0
- package/src/pages/account/dashboard-header.tsx +123 -0
- package/src/pages/account/navigation.tsx +84 -0
- package/src/pages/account/no-records.tsx +20 -0
- package/src/pages/account/orders/icons/delivery.tsx +14 -0
- package/src/pages/account/orders/icons/package-box.tsx +13 -0
- package/src/pages/account/orders/icons/truck-filled.tsx +14 -0
- package/src/pages/account/orders/order-progress.tsx +111 -0
- package/src/pages/account/orders/order-row.tsx +61 -0
- package/src/pages/account/orders/order-summary.tsx +83 -0
- package/src/pages/account/orders/ordered-products.tsx +114 -0
- package/src/pages/account/profile/profile-button.test.tsx +59 -0
- package/src/pages/account/profile/profile-button.tsx +50 -0
- package/src/pages/account/profile/user-info.tsx +59 -0
- package/src/pages/account/styles.ts +32 -0
- package/src/pages/account/table-row.tsx +19 -0
- package/src/pages/account/wishlist/create-new-list.tsx +167 -0
- package/src/pages/account/wishlist/create-wishlist-button.tsx +40 -0
- package/src/pages/account/wishlist/wishlist-item.tsx +82 -0
- package/src/pages/blog/pagination.tsx +38 -0
- package/src/pages/cart/cart-item.tsx +312 -0
- package/src/pages/cart/checkout-form.tsx +122 -0
- package/src/pages/cart/coupon-entry.tsx +90 -0
- package/src/pages/cart/estimate-shipping.tsx +183 -0
- package/src/pages/cart/wrapper.tsx +30 -0
- package/src/pages/checkout/checkout-alt-form/address-card.tsx +73 -0
- package/src/pages/checkout/checkout-alt-form/checkout-form.tsx +133 -0
- package/src/pages/checkout/checkout-alt-form/checkout-step.tsx +120 -0
- package/src/pages/checkout/checkout-alt-form/customer-information.tsx +94 -0
- package/src/pages/checkout/checkout-alt-form/delivery-address.tsx +170 -0
- package/src/pages/checkout/checkout-alt-form/delivery-date.tsx +114 -0
- package/src/pages/checkout/checkout-alt-form/delivery-options.tsx +186 -0
- package/src/pages/checkout/checkout-alt-form/edit-address-form.tsx +130 -0
- package/src/pages/checkout/checkout-alt-form/heading.tsx +50 -0
- package/src/pages/checkout/checkout-alt-form/index.ts +1 -0
- package/src/pages/checkout/checkout-alt-form/new-address-form.tsx +294 -0
- package/src/pages/checkout/checkout-alt-form/node.tsx +9 -0
- package/src/pages/checkout/checkout-alt-form/payment-details.tsx +344 -0
- package/src/pages/checkout/checkout-alt-form/safe-card-preview-data.tsx +36 -0
- package/src/pages/checkout/checkout-alt-form/types.ts +20 -0
- package/src/pages/checkout/checkout-alt-summary/cart-item.tsx +39 -0
- package/src/pages/checkout/checkout-alt-summary/checkout-alt-summary.tsx +40 -0
- package/src/pages/checkout/checkout-alt-summary/index.ts +1 -0
- package/src/pages/checkout/checkout-alt-summary/list-item.tsx +31 -0
- package/src/pages/confirmation/address.tsx +22 -0
- package/src/pages/confirmation/confirmation-summary.tsx +52 -0
- package/src/pages/confirmation/ordered-products.tsx +108 -0
- package/src/pages/product-details/available-shops.tsx +48 -0
- package/src/pages/product-details/bopis/find-in-store-button.tsx +63 -0
- package/src/pages/product-details/bopis/find-in-store-modal.tsx +257 -0
- package/src/pages/product-details/bopis/pickup-option-select.tsx +127 -0
- package/src/pages/product-details/bopis/search-header.tsx +69 -0
- package/src/pages/product-details/checkbox-label.tsx +20 -0
- package/src/pages/product-details/currency.ts +64 -0
- package/src/pages/product-details/frequently-bought.tsx +90 -0
- package/src/pages/product-details/frequently-product-card.tsx +60 -0
- package/src/pages/product-details/product-comment.tsx +44 -0
- package/src/pages/product-details/product-description.tsx +22 -0
- package/src/pages/product-details/product-filter-card.tsx +257 -0
- package/src/pages/product-details/product-intro/product-intro-images.tsx +87 -0
- package/src/pages/product-details/product-intro/product-intro.tsx +250 -0
- package/src/pages/product-details/product-review.tsx +131 -0
- package/src/pages/product-details/product-tabs.tsx +51 -0
- package/src/pages/product-details/related-products.tsx +42 -0
- package/src/pages/product-details/types.ts +11 -0
- package/src/pages/product-list/breadcrumbs.tsx +39 -0
- package/src/pages/product-list/checkbox-label.tsx +20 -0
- package/src/pages/product-list/facet-group.tsx +125 -0
- package/src/pages/product-list/facet.tsx +36 -0
- package/src/pages/product-list/list-filter.tsx +40 -0
- package/src/pages/product-list/pagination.tsx +80 -0
- package/src/pages/product-list/product-list-context.tsx +302 -0
- package/src/pages/product-list/product-list-state.ts +187 -0
- package/src/pages/product-list/product-rating.tsx +16 -0
- package/src/pages/product-list/quick-view-dialog-content.tsx +205 -0
- package/src/pages/product-list/quick-view-dialog.tsx +54 -0
- package/src/pages/product-list/range-filter.tsx +125 -0
- package/src/pages/product-list/scrollbar.tsx +38 -0
- package/src/pages/product-list/search-bar.tsx +162 -0
- package/src/pages/product-list/selected-facets.tsx +80 -0
- package/src/pages/product-list/side-nav.tsx +49 -0
- package/src/pages/product-list/swatch.tsx +80 -0
- package/src/pages/product-list/types.ts +18 -0
- package/src/pages/product-list/use-product-list.ts +12 -0
- package/src/pages/product-list/useCategoryFilter.tsx +26 -0
- package/src/pages/product-list/utils/facet-helpers.ts +5 -0
- package/src/pages/product-list/utils/generate-breadcrumbs.ts +30 -0
- package/src/pages/product-list/utils/getCategoryFilterFromSlug.ts +153 -0
- package/src/pages/product-list/utils/product-list-helper.ts +111 -0
- package/src/pages/product-list/utils/product-list-types.ts +41 -0
- package/src/pages/product-list/utils/search-for-category.ts +76 -0
- package/src/pages/product-list/utils/sort-options.ts +44 -0
- package/src/pages/product-list/utils/use-previous-refiners.ts +14 -0
- package/src/pages/quickorder/order-upload.tsx +150 -0
- package/src/pages/quickorder/quick-order-form.tsx +343 -0
- package/src/pages/quickorder/quick-order-row.tsx +144 -0
- package/tsconfig.json +1 -0
- package/src/api-manager/datasources/e4/graphqlRequestSdk.ts +0 -10464
- /package/src/{components/_components/hooks/useOverflowDetect.ts → hooks/use-overflow-detect.ts} +0 -0
- /package/src/{components/_components/hooks/useSettings.ts → hooks/use-settings.ts} +0 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import useCurrencyFormatter from "@/lib/currency-formatter";
|
|
2
|
+
import { ProductDimensionType } from "@msdyn365-commerce/retail-proxy";
|
|
3
|
+
import { Card, Avatar, Box } from "@mui/material";
|
|
4
|
+
import Grid from "@mui/material/Grid2";
|
|
5
|
+
import { SalesLine } from "@/api-manager/schemas/order.schema";
|
|
6
|
+
import { FlexBetween, FlexBox } from "@/components/_components/flex-box/index";
|
|
7
|
+
import { H6, Paragraph, Span } from "@/components/_components/Typography";
|
|
8
|
+
import { useFormatter, useTranslations } from "next-intl";
|
|
9
|
+
import React from "react";
|
|
10
|
+
import { Fragment } from "react";
|
|
11
|
+
|
|
12
|
+
type Props = {
|
|
13
|
+
salesLines: SalesLine[];
|
|
14
|
+
orderId: string;
|
|
15
|
+
transactionDate: Date;
|
|
16
|
+
receiptEmail: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default function OrderedProducts({
|
|
20
|
+
salesLines,
|
|
21
|
+
orderId,
|
|
22
|
+
transactionDate,
|
|
23
|
+
receiptEmail,
|
|
24
|
+
}: Props) {
|
|
25
|
+
const t = useTranslations("Confirmation");
|
|
26
|
+
const { dateTime } = useFormatter();
|
|
27
|
+
const { formatCurrency } = useCurrencyFormatter();
|
|
28
|
+
|
|
29
|
+
const checkValidDate = (date: Date | string) => {
|
|
30
|
+
return new Date(date) instanceof Date;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const renderItem = (title: string, value: string) => (
|
|
34
|
+
<FlexBox gap={1} alignItems="center">
|
|
35
|
+
<Paragraph color="grey.600">{title}</Paragraph>
|
|
36
|
+
<Paragraph>{value}</Paragraph>
|
|
37
|
+
</FlexBox>
|
|
38
|
+
);
|
|
39
|
+
return (
|
|
40
|
+
<Card sx={{ p: 0, mb: "30px" }}>
|
|
41
|
+
<FlexBetween
|
|
42
|
+
px={3}
|
|
43
|
+
py={2}
|
|
44
|
+
flexWrap="wrap"
|
|
45
|
+
borderBottom="1px solid"
|
|
46
|
+
borderColor="divider"
|
|
47
|
+
>
|
|
48
|
+
{renderItem(t("orderNumber"), orderId)}
|
|
49
|
+
{checkValidDate(transactionDate)
|
|
50
|
+
? renderItem(
|
|
51
|
+
t("orderDate"),
|
|
52
|
+
dateTime(new Date(transactionDate), { dateStyle: "medium" }),
|
|
53
|
+
)
|
|
54
|
+
: null}
|
|
55
|
+
{renderItem(t("orderEmail"), receiptEmail)}
|
|
56
|
+
</FlexBetween>
|
|
57
|
+
|
|
58
|
+
<Grid container spacing={2} alignItems={"center"} padding={"15px 5px"}>
|
|
59
|
+
{salesLines.map((item, ind) => (
|
|
60
|
+
<Fragment key={ind}>
|
|
61
|
+
<Grid size={{ xs: 12, sm: 6 }}>
|
|
62
|
+
<FlexBox gap={2.5} alignItems="center">
|
|
63
|
+
<Avatar
|
|
64
|
+
alt={item.Product?.Name}
|
|
65
|
+
src={item.Product?.PrimaryImageUrl}
|
|
66
|
+
sx={{ mx: 1, width: 75, height: 75 }}
|
|
67
|
+
/>
|
|
68
|
+
<div>
|
|
69
|
+
<H6>{item.Product?.Name}</H6>
|
|
70
|
+
<Paragraph color="grey.600">
|
|
71
|
+
{formatCurrency(item.Price ?? 0)} x {item.Quantity}
|
|
72
|
+
</Paragraph>
|
|
73
|
+
{item.Comment ? (
|
|
74
|
+
<Paragraph mt={1}>
|
|
75
|
+
<H6 sx={{ display: "inline" }}>{t("commentLabel")}: </H6>
|
|
76
|
+
{item.Comment}
|
|
77
|
+
</Paragraph>
|
|
78
|
+
) : null}
|
|
79
|
+
</div>
|
|
80
|
+
</FlexBox>
|
|
81
|
+
</Grid>
|
|
82
|
+
|
|
83
|
+
<Grid size={{ xs: 8, sm: 4 }}>
|
|
84
|
+
{item.Product?.Dimensions?.length ? (
|
|
85
|
+
item.Product.Dimensions.map((dim, dimInd) => (
|
|
86
|
+
<Paragraph key={dimInd} paddingLeft={"15px"}>
|
|
87
|
+
<Span>
|
|
88
|
+
{ProductDimensionType[dim.DimensionTypeValue || 0]}:
|
|
89
|
+
</Span>
|
|
90
|
+
<Span sx={{ ml: 1 }}>{dim.DimensionValue?.Value}</Span>
|
|
91
|
+
</Paragraph>
|
|
92
|
+
))
|
|
93
|
+
) : (
|
|
94
|
+
<Box />
|
|
95
|
+
)}
|
|
96
|
+
</Grid>
|
|
97
|
+
|
|
98
|
+
<Grid size={{ xs: 4, sm: 2 }}>
|
|
99
|
+
<Box justifySelf={"end"} paddingRight={"15px"}>
|
|
100
|
+
<Paragraph>{formatCurrency(item.TotalAmount ?? 0)}</Paragraph>
|
|
101
|
+
</Box>
|
|
102
|
+
</Grid>
|
|
103
|
+
</Fragment>
|
|
104
|
+
))}
|
|
105
|
+
</Grid>
|
|
106
|
+
</Card>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import Box from "@mui/material/Box";
|
|
5
|
+
import Card from "@mui/material/Card";
|
|
6
|
+
import Grid from "@mui/material/Grid2";
|
|
7
|
+
import Avatar from "@mui/material/Avatar";
|
|
8
|
+
// GLOBAL CUSTOM COMPONENTS
|
|
9
|
+
import { H3, H4 } from "@/components/_components/Typography";
|
|
10
|
+
import FlexRowCenter from "@/components/_components/flex-box/flex-row-center";
|
|
11
|
+
|
|
12
|
+
export default function AvailableShops() {
|
|
13
|
+
return (
|
|
14
|
+
<Box mb={7.5}>
|
|
15
|
+
<H3 mb={3}>Also Available at</H3>
|
|
16
|
+
|
|
17
|
+
<Grid container spacing={4}>
|
|
18
|
+
{shopList.map((item) => (
|
|
19
|
+
<Grid size={{ lg: 2, md: 3, sm: 4, xs: 12 }} key={item.name}>
|
|
20
|
+
<Link href="/shops/scarlett-beauty">
|
|
21
|
+
<FlexRowCenter
|
|
22
|
+
p={3.25}
|
|
23
|
+
width="100%"
|
|
24
|
+
component={Card}
|
|
25
|
+
flexDirection="column"
|
|
26
|
+
>
|
|
27
|
+
<Avatar
|
|
28
|
+
alt={item.name}
|
|
29
|
+
src={item.imgUrl}
|
|
30
|
+
sx={{ width: 48, height: 48 }}
|
|
31
|
+
/>
|
|
32
|
+
<H4 mt={1.5} color="grey.800">
|
|
33
|
+
{item.name}
|
|
34
|
+
</H4>
|
|
35
|
+
</FlexRowCenter>
|
|
36
|
+
</Link>
|
|
37
|
+
</Grid>
|
|
38
|
+
))}
|
|
39
|
+
</Grid>
|
|
40
|
+
</Box>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const shopList = [
|
|
45
|
+
{ name: "Tech Friend", imgUrl: "/assets/images/faces/propic.png" },
|
|
46
|
+
{ name: "Smart Shop", imgUrl: "/assets/images/faces/propic(1).png" },
|
|
47
|
+
{ name: "Gadget 360", imgUrl: "/assets/images/faces/propic(8).png" },
|
|
48
|
+
];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Button } from "@mui/material";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import FindInStoreModal from "./find-in-store-modal";
|
|
4
|
+
import { AddToCartBopisInput } from "../../../hooks/use-cart";
|
|
5
|
+
import { useTranslations } from "next-intl";
|
|
6
|
+
|
|
7
|
+
type Props = {
|
|
8
|
+
productId: number;
|
|
9
|
+
canOpenModal(): boolean;
|
|
10
|
+
handleAddToCart: (bopisInput: AddToCartBopisInput) => void;
|
|
11
|
+
allDimensionsSelected: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A button component that triggers a "Find in Store" modal for BOPIS (Buy Online, Pickup In Store) functionality.
|
|
16
|
+
*
|
|
17
|
+
* @param props - The component props
|
|
18
|
+
* @param props.productId - The ID of the product to find in stores
|
|
19
|
+
* @param props.canOpenModal - Optional function that determines if the modal can be opened; if undefined, modal can always open
|
|
20
|
+
* @param props.handleAddToCart - Function to call when adding the item to cart with BOPIS information
|
|
21
|
+
* @param props.allDimensionsSelected - Flag indicating if all product dimensions/variations have been selected
|
|
22
|
+
*
|
|
23
|
+
* @returns A React component with a button that opens a modal for finding the product in nearby stores
|
|
24
|
+
*/
|
|
25
|
+
const FindInStoreButton = ({
|
|
26
|
+
productId,
|
|
27
|
+
canOpenModal,
|
|
28
|
+
handleAddToCart,
|
|
29
|
+
allDimensionsSelected,
|
|
30
|
+
}: Props) => {
|
|
31
|
+
const [open, setOpen] = React.useState(false);
|
|
32
|
+
const t = useTranslations("productDetail.Bopis");
|
|
33
|
+
const modalShouldOpen = () => canOpenModal === undefined || canOpenModal();
|
|
34
|
+
const handleOpen = () => {
|
|
35
|
+
if (modalShouldOpen()) {
|
|
36
|
+
setOpen(true);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const handleClose = () => {
|
|
40
|
+
setOpen(false);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const onAddToCart = (bopisInput: AddToCartBopisInput) => {
|
|
44
|
+
handleAddToCart(bopisInput);
|
|
45
|
+
handleClose();
|
|
46
|
+
};
|
|
47
|
+
return (
|
|
48
|
+
<React.Fragment>
|
|
49
|
+
<Button onClick={handleOpen} variant={"contained"} color={"secondary"}>
|
|
50
|
+
{t("findStoresBtn")}
|
|
51
|
+
</Button>
|
|
52
|
+
<FindInStoreModal
|
|
53
|
+
open={open}
|
|
54
|
+
handleClose={handleClose}
|
|
55
|
+
handleAddToCart={onAddToCart}
|
|
56
|
+
productId={productId}
|
|
57
|
+
allDimensionsSelected={allDimensionsSelected}
|
|
58
|
+
/>
|
|
59
|
+
</React.Fragment>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export default FindInStoreButton;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CircularProgress,
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogContent,
|
|
5
|
+
FormControlLabel,
|
|
6
|
+
FormGroup,
|
|
7
|
+
Switch,
|
|
8
|
+
Typography,
|
|
9
|
+
} from "@mui/material";
|
|
10
|
+
import { Box } from "@mui/system";
|
|
11
|
+
import React from "react";
|
|
12
|
+
import SearchHeader from "./search-header";
|
|
13
|
+
import ShopCard from "@/components/_components/shop-card";
|
|
14
|
+
import {
|
|
15
|
+
DeliveryOption,
|
|
16
|
+
ProductWarehouseInventoryAvailability,
|
|
17
|
+
QueryResultSettings,
|
|
18
|
+
SearchArea,
|
|
19
|
+
} from "@msdyn365-commerce/retail-proxy";
|
|
20
|
+
import PickupOptionSelect from "./pickup-option-select";
|
|
21
|
+
import { geolocate } from "@/lib/map-helper";
|
|
22
|
+
import getStores from "@/api-manager/services/organization/queries/get-stores";
|
|
23
|
+
import { AddToCartBopisInput } from "../../../hooks/use-cart";
|
|
24
|
+
import getProductAvailability from "@/api-manager/services/product/queries/get-product-availability";
|
|
25
|
+
import { InventorySearchCriteriaInput } from "@/api-manager/schemas/product.schema";
|
|
26
|
+
import { OrgUnitLocationReturnType } from "@/api-manager/schemas/organization.schema";
|
|
27
|
+
import { useTranslations } from "next-intl";
|
|
28
|
+
import getChannelConfiguration from "@/api-manager/services/organization/queries/get-channel-configuration";
|
|
29
|
+
|
|
30
|
+
type Props = {
|
|
31
|
+
open: boolean;
|
|
32
|
+
handleClose: () => void;
|
|
33
|
+
handleAddToCart: (bopisInput: AddToCartBopisInput) => void;
|
|
34
|
+
productId: number;
|
|
35
|
+
allDimensionsSelected: boolean;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const qrs: QueryResultSettings = {
|
|
39
|
+
Paging: {
|
|
40
|
+
Skip: 0,
|
|
41
|
+
Top: 1000,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
const initialSearchArea: SearchArea = {
|
|
45
|
+
DistanceUnitValue: 0,
|
|
46
|
+
Latitude: 0,
|
|
47
|
+
Longitude: 0,
|
|
48
|
+
Radius: 0,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Modal component that allows users to find a product in physical stores.
|
|
53
|
+
*
|
|
54
|
+
* @component
|
|
55
|
+
* @param {Object} props - Component props
|
|
56
|
+
* @param {Function} props.handleClose - Function to close the modal
|
|
57
|
+
* @param {boolean} props.open - Whether the modal is open or not
|
|
58
|
+
* @param {Function} props.handleAddToCart - Function to handle adding the product to cart
|
|
59
|
+
* @param {number} props.productId - ID of the product being searched
|
|
60
|
+
* @param {boolean} props.allDimensionsSelected - Whether all product dimensions (size, color, etc.) are selected
|
|
61
|
+
*
|
|
62
|
+
* @returns {JSX.Element} - Rendered modal component that displays available stores with inventory information
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* <FindInStoreModal
|
|
66
|
+
* handleClose={() => setModalOpen(false)}
|
|
67
|
+
* open={modalOpen}
|
|
68
|
+
* handleAddToCart={handleAddItemToCart}
|
|
69
|
+
* productId={123456}
|
|
70
|
+
* allDimensionsSelected={true}
|
|
71
|
+
* />
|
|
72
|
+
*/
|
|
73
|
+
const FindInStoreModal = ({
|
|
74
|
+
handleClose,
|
|
75
|
+
open,
|
|
76
|
+
handleAddToCart,
|
|
77
|
+
productId,
|
|
78
|
+
allDimensionsSelected,
|
|
79
|
+
}: Props): JSX.Element => {
|
|
80
|
+
const t = useTranslations("productDetail");
|
|
81
|
+
const [hideOutOfStock, setHideOutOfStock] = React.useState(false);
|
|
82
|
+
const { data: channelConfig } = getChannelConfiguration.useSuspenseData();
|
|
83
|
+
const [searchArea, setSearchArea] =
|
|
84
|
+
React.useState<SearchArea>(initialSearchArea);
|
|
85
|
+
const { data: stores, isLoading: storesLoading } = getStores.useData({
|
|
86
|
+
queryResultSettings: qrs,
|
|
87
|
+
searchArea,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const inventoryInput: InventorySearchCriteriaInput = React.useMemo(
|
|
91
|
+
() => ({
|
|
92
|
+
allDimensionsSelected,
|
|
93
|
+
searchCriteria: {
|
|
94
|
+
defaultWarehouseOnly: false,
|
|
95
|
+
productIds: [productId],
|
|
96
|
+
searchArea,
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
[allDimensionsSelected, productId, searchArea],
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const { data: inventoryData } =
|
|
103
|
+
getProductAvailability.useData(inventoryInput);
|
|
104
|
+
|
|
105
|
+
const renderTitle = () => (
|
|
106
|
+
<Box
|
|
107
|
+
display={"flex"}
|
|
108
|
+
justifyContent="flex-start"
|
|
109
|
+
alignItems="center"
|
|
110
|
+
sx={{ padding: 2 }}
|
|
111
|
+
>
|
|
112
|
+
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
|
113
|
+
{t("Bopis.findInStoreLabel")}
|
|
114
|
+
</Typography>
|
|
115
|
+
</Box>
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const toggleOutOfStock = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
119
|
+
setHideOutOfStock(event.target.checked);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const renderOutOfStockToggle = () => {
|
|
123
|
+
return (
|
|
124
|
+
<FormGroup>
|
|
125
|
+
<FormControlLabel
|
|
126
|
+
control={
|
|
127
|
+
<Switch checked={hideOutOfStock} onChange={toggleOutOfStock} />
|
|
128
|
+
}
|
|
129
|
+
label={t("Bopis.hideOutOfStockLabel")}
|
|
130
|
+
/>
|
|
131
|
+
</FormGroup>
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
const getInventoryRecordByStoreId = (
|
|
135
|
+
storeId: string,
|
|
136
|
+
): ProductWarehouseInventoryAvailability | undefined => {
|
|
137
|
+
const inventoryRecord =
|
|
138
|
+
inventoryData?.ProductWarehouseInventoryAvailabilities?.find(
|
|
139
|
+
(record) => record.InventLocationId === storeId,
|
|
140
|
+
);
|
|
141
|
+
return inventoryRecord;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const isOutOfStock = (storeId: string): boolean => {
|
|
145
|
+
const OUT_OF_STOCK_LABEL = "OOS"; // Always means out of stock
|
|
146
|
+
const inventoryRecord = getInventoryRecordByStoreId(storeId);
|
|
147
|
+
if (inventoryRecord) {
|
|
148
|
+
return (
|
|
149
|
+
inventoryRecord.PhysicalAvailableInventoryLevelCode ===
|
|
150
|
+
OUT_OF_STOCK_LABEL
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return true;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const getStoreList = () => {
|
|
157
|
+
if (!stores) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
const _stores = [...Object.values(stores)];
|
|
161
|
+
if (hideOutOfStock) {
|
|
162
|
+
return _stores.filter(
|
|
163
|
+
(store) => !isOutOfStock(store.location.OrgUnitNumber || ""),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return _stores;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const handleSearch = async (searchTerm: string) => {
|
|
170
|
+
if (!channelConfig) {
|
|
171
|
+
console.error("Channel configuration is not available.");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const [lat, lng] = await geolocate(
|
|
175
|
+
searchTerm,
|
|
176
|
+
channelConfig?.[0]?.BingMapsApiKey || "",
|
|
177
|
+
);
|
|
178
|
+
setSearchArea({
|
|
179
|
+
...searchArea,
|
|
180
|
+
Latitude: lat,
|
|
181
|
+
Longitude: lng,
|
|
182
|
+
DistanceUnitValue: 0,
|
|
183
|
+
Radius: 0,
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const renderActionArea = (
|
|
188
|
+
storeId: string,
|
|
189
|
+
pickupOptions: DeliveryOption[],
|
|
190
|
+
inventoryLabel: string,
|
|
191
|
+
) => (
|
|
192
|
+
<PickupOptionSelect
|
|
193
|
+
pickupOptions={pickupOptions || []}
|
|
194
|
+
storeId={storeId}
|
|
195
|
+
onPickupClick={handleAddToCart}
|
|
196
|
+
isOutOfStock={isOutOfStock(storeId)}
|
|
197
|
+
inventoryLabel={inventoryLabel}
|
|
198
|
+
/>
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const renderShopCard = (store: OrgUnitLocationReturnType) => {
|
|
202
|
+
const { location, storeHours, pickupOptions } = store;
|
|
203
|
+
if (!location.OrgUnitNumber) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
const inventoryRecord = getInventoryRecordByStoreId(location.OrgUnitNumber);
|
|
207
|
+
return (
|
|
208
|
+
<ShopCard
|
|
209
|
+
key={location.OrgUnitNumber}
|
|
210
|
+
name={location.OrgUnitName || ""}
|
|
211
|
+
address={location.Address || ""}
|
|
212
|
+
actionArea={renderActionArea(
|
|
213
|
+
location.OrgUnitNumber || "",
|
|
214
|
+
// @ts-ignore TODO: ??
|
|
215
|
+
pickupOptions || [],
|
|
216
|
+
inventoryRecord?.TotalAvailableInventoryLevelLabel || "",
|
|
217
|
+
)}
|
|
218
|
+
openFrom={location.OpenFrom || 0}
|
|
219
|
+
openTo={location.OpenTo || 0}
|
|
220
|
+
storeHours={storeHours}
|
|
221
|
+
inventoryRecord={inventoryRecord}
|
|
222
|
+
/>
|
|
223
|
+
);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<Dialog open={open} onClose={handleClose} fullWidth={true} maxWidth={"md"}>
|
|
228
|
+
<DialogContent>
|
|
229
|
+
{storesLoading && (
|
|
230
|
+
<Box
|
|
231
|
+
sx={{
|
|
232
|
+
display: "flex",
|
|
233
|
+
justifyContent: "center",
|
|
234
|
+
alignItems: "center",
|
|
235
|
+
height: "100px",
|
|
236
|
+
direction: "row",
|
|
237
|
+
}}
|
|
238
|
+
>
|
|
239
|
+
<CircularProgress />
|
|
240
|
+
</Box>
|
|
241
|
+
)}
|
|
242
|
+
{!storesLoading && (
|
|
243
|
+
<React.Fragment>
|
|
244
|
+
{renderTitle()}
|
|
245
|
+
<SearchHeader handleSearch={handleSearch} />
|
|
246
|
+
{renderOutOfStockToggle()}
|
|
247
|
+
<Box sx={{ padding: 2, marginBottom: 2 }}>
|
|
248
|
+
{getStoreList().map((store) => renderShopCard(store))}
|
|
249
|
+
</Box>
|
|
250
|
+
</React.Fragment>
|
|
251
|
+
)}
|
|
252
|
+
</DialogContent>
|
|
253
|
+
</Dialog>
|
|
254
|
+
);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
export default FindInStoreModal;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { FlexBox } from "@/components/_components/flex-box";
|
|
2
|
+
import { DeliveryOption } from "@msdyn365-commerce/retail-proxy";
|
|
3
|
+
import {
|
|
4
|
+
Box,
|
|
5
|
+
Button,
|
|
6
|
+
MenuItem,
|
|
7
|
+
Select,
|
|
8
|
+
SelectChangeEvent,
|
|
9
|
+
Typography,
|
|
10
|
+
} from "@mui/material";
|
|
11
|
+
import React from "react";
|
|
12
|
+
import { AddToCartBopisInput } from "../../../hooks/use-cart";
|
|
13
|
+
import { useTranslations } from "next-intl";
|
|
14
|
+
|
|
15
|
+
type Props = {
|
|
16
|
+
pickupOptions: DeliveryOption[];
|
|
17
|
+
storeId: string;
|
|
18
|
+
onPickupClick: (bopisInput: AddToCartBopisInput) => void;
|
|
19
|
+
isOutOfStock: boolean;
|
|
20
|
+
inventoryLabel: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A component that renders pickup options for a BOPIS (Buy Online, Pickup In Store) flow.
|
|
25
|
+
*
|
|
26
|
+
* @component
|
|
27
|
+
* @param {Object} props - The component props
|
|
28
|
+
* @param {DeliveryOption[]} props.pickupOptions - Array of available pickup options for the store
|
|
29
|
+
* @param {string} props.storeId - ID of the store for pickup
|
|
30
|
+
* @param {Function} props.onPickupClick - Callback function called when user clicks the pickup button
|
|
31
|
+
* @param {boolean} props.isOutOfStock - Flag indicating if the product is out of stock at this location
|
|
32
|
+
* @param {string} props.inventoryLabel - Label to display when product is out of stock
|
|
33
|
+
*
|
|
34
|
+
* @returns {JSX.Element} The rendered pickup option selection component
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* <PickupOptionSelect
|
|
38
|
+
* pickupOptions={storePickupOptions}
|
|
39
|
+
* storeId="store123"
|
|
40
|
+
* onPickupClick={handleAddToCart}
|
|
41
|
+
* isOutOfStock={false}
|
|
42
|
+
* inventoryLabel="Out of stock"
|
|
43
|
+
* />
|
|
44
|
+
*/
|
|
45
|
+
const PickupOptionSelect = ({
|
|
46
|
+
pickupOptions,
|
|
47
|
+
storeId,
|
|
48
|
+
onPickupClick,
|
|
49
|
+
isOutOfStock,
|
|
50
|
+
inventoryLabel,
|
|
51
|
+
}: Props): JSX.Element => {
|
|
52
|
+
const [selectedOption, setSelectedOption] =
|
|
53
|
+
React.useState<DeliveryOption | null>(pickupOptions[0] || null);
|
|
54
|
+
const handlePickupOptionChange = (event: SelectChangeEvent) => {
|
|
55
|
+
const _selectedOption = pickupOptions.find(
|
|
56
|
+
(option) => option.Code === event.target.value,
|
|
57
|
+
);
|
|
58
|
+
setSelectedOption(_selectedOption || null);
|
|
59
|
+
};
|
|
60
|
+
const t = useTranslations("productDetail");
|
|
61
|
+
|
|
62
|
+
const renderOptions = () => (
|
|
63
|
+
<Select
|
|
64
|
+
labelId={`pickup-option-select-${storeId}`}
|
|
65
|
+
id={`pickup-option-select-${storeId}`}
|
|
66
|
+
onChange={handlePickupOptionChange}
|
|
67
|
+
value={selectedOption?.Code || ""}
|
|
68
|
+
>
|
|
69
|
+
{pickupOptions.map((option) => (
|
|
70
|
+
<MenuItem key={option.Code} value={option.Code}>
|
|
71
|
+
{option.Description}
|
|
72
|
+
</MenuItem>
|
|
73
|
+
))}
|
|
74
|
+
</Select>
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const handlePickupClick = () => {
|
|
78
|
+
if (selectedOption && selectedOption.Code) {
|
|
79
|
+
const bopisInput: AddToCartBopisInput = {
|
|
80
|
+
warehouseId: storeId,
|
|
81
|
+
deliveryMode: selectedOption.Code,
|
|
82
|
+
};
|
|
83
|
+
onPickupClick(bopisInput);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (pickupOptions.length === 0) {
|
|
88
|
+
return (
|
|
89
|
+
<Box sx={{ minHeight: "40px", padding: 2 }}>
|
|
90
|
+
<Typography variant={"body2"} sx={{ fontWeight: 600 }}>
|
|
91
|
+
{t("Bopis.noPickupAvailable")}
|
|
92
|
+
</Typography>
|
|
93
|
+
</Box>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (isOutOfStock) {
|
|
98
|
+
return (
|
|
99
|
+
<Box sx={{ minHeight: "40px", padding: 2 }}>
|
|
100
|
+
<Typography variant={"body2"} sx={{ fontWeight: 600 }}>
|
|
101
|
+
{inventoryLabel}
|
|
102
|
+
</Typography>
|
|
103
|
+
</Box>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<FlexBox alignItems={"center"} gap={1} sx={{ padding: 2 }}>
|
|
109
|
+
{pickupOptions.length > 1 ? (
|
|
110
|
+
renderOptions()
|
|
111
|
+
) : (
|
|
112
|
+
<Box>{selectedOption?.Description}</Box>
|
|
113
|
+
)}
|
|
114
|
+
<Box>
|
|
115
|
+
<Button
|
|
116
|
+
variant={"contained"}
|
|
117
|
+
color={"primary"}
|
|
118
|
+
onClick={handlePickupClick}
|
|
119
|
+
>
|
|
120
|
+
{t("btnAddToCart")}
|
|
121
|
+
</Button>
|
|
122
|
+
</Box>
|
|
123
|
+
</FlexBox>
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export default PickupOptionSelect;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FormControl,
|
|
3
|
+
IconButton,
|
|
4
|
+
InputAdornment,
|
|
5
|
+
OutlinedInput,
|
|
6
|
+
} from "@mui/material";
|
|
7
|
+
import React from "react";
|
|
8
|
+
import SearchIcon from "@mui/icons-material/Search";
|
|
9
|
+
import { useTranslations } from "next-intl";
|
|
10
|
+
type Props = {
|
|
11
|
+
handleSearch: (searchTerm: string) => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Component for rendering a search header with a search input and submit functionality.
|
|
16
|
+
*
|
|
17
|
+
* @component
|
|
18
|
+
* @param {Object} props - Component props
|
|
19
|
+
* @param {Function} props.handleSearch - Callback function that handles the search action with the search term
|
|
20
|
+
*
|
|
21
|
+
* @returns {JSX.Element} A form with a search input field that triggers the search on submit or Enter key
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* <SearchHeader handleSearch={(term) => console.log(`Searching for ${term}`)} />
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
const SearchHeader = ({ handleSearch }: Props): JSX.Element => {
|
|
29
|
+
const [searchTerm, setSearchTerm] = React.useState<string>("");
|
|
30
|
+
const t = useTranslations("productDetail.Bopis");
|
|
31
|
+
const renderSearchAdornment = () => (
|
|
32
|
+
<InputAdornment position="end">
|
|
33
|
+
<IconButton type="submit" aria-label="search" edge="end">
|
|
34
|
+
<SearchIcon />
|
|
35
|
+
</IconButton>
|
|
36
|
+
</InputAdornment>
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const handleSubmit = (e: React.KeyboardEvent) => {
|
|
40
|
+
if (e.key === "Enter" && searchTerm) {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
e.stopPropagation();
|
|
43
|
+
handleSearch(searchTerm);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const onSearch = (e: React.FormEvent) => {
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
if (searchTerm) {
|
|
50
|
+
handleSearch(searchTerm);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<form onSubmit={onSearch}>
|
|
56
|
+
<FormControl sx={{ m: 1, width: "100%" }} variant="outlined">
|
|
57
|
+
<OutlinedInput
|
|
58
|
+
endAdornment={renderSearchAdornment()}
|
|
59
|
+
value={searchTerm}
|
|
60
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
61
|
+
placeholder={t("searchPlaceholder")}
|
|
62
|
+
onKeyUp={handleSubmit}
|
|
63
|
+
/>
|
|
64
|
+
</FormControl>
|
|
65
|
+
</form>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export default SearchHeader;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
// MUI
|
|
3
|
+
import FormControlLabel from "@mui/material/FormControlLabel";
|
|
4
|
+
import Checkbox, { CheckboxProps } from "@mui/material/Checkbox";
|
|
5
|
+
|
|
6
|
+
// ==============================================================
|
|
7
|
+
interface Props extends CheckboxProps {
|
|
8
|
+
label: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
// ==============================================================
|
|
11
|
+
|
|
12
|
+
export default function CheckboxLabel({ label, ...props }: Props) {
|
|
13
|
+
return (
|
|
14
|
+
<FormControlLabel
|
|
15
|
+
label={label}
|
|
16
|
+
control={<Checkbox size="small" color="primary" {...props} />}
|
|
17
|
+
slotProps={{ typography: { fontSize: 14, lineHeight: 1 } }}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
}
|