@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.
Files changed (133) hide show
  1. package/package.json +3 -2
  2. package/src/api-manager/datasources/d365/d365-address.datasource.ts +1 -1
  3. package/src/api-manager/datasources/d365/d365-user.datasource.ts +0 -19
  4. package/src/api-manager/datasources/e4/e4-cart.datasource.ts +2 -2
  5. package/src/api-manager/datasources/e4/e4-product.datasource.ts +19 -4
  6. package/src/api-manager/datasources/e4/e4.datasource.ts +1 -2
  7. package/src/api-manager/datasources/e4/e4.remaps.ts +24 -0
  8. package/src/api-manager/datasources/e4/e4.translator.ts +17 -0
  9. package/src/api-manager/datasources/e4/{e4-user.datasource.ts → user/e4-user.datasource.ts} +5 -7
  10. package/src/api-manager/datasources/e4/user/e4-user.remaps.ts +4 -0
  11. package/src/api-manager/datasources/e4/user/e4-user.translator.ts +10 -0
  12. package/src/api-manager/index.ts +21 -6
  13. package/src/api-manager/schemas/product.schema.ts +1 -1
  14. package/src/api-manager/services/address/queries/get-states.ts +1 -1
  15. package/src/api-manager/services/create-query.ts +10 -3
  16. package/src/api-manager/types/Datasource.ts +9 -9
  17. package/src/components/Blocks/Components/ProductCarousel/index.tsx +6 -6
  18. package/src/components/_components/navbar/category-based-menu/components/categories.tsx +1 -1
  19. package/src/components/_components/navbar/mega-menu/mega-menu.tsx +1 -1
  20. package/src/components/_components/navbar/nav-list/nav-item-child.tsx +2 -2
  21. package/src/components/_components/product-dimensions/product-dimensions.tsx +114 -0
  22. package/src/components/_components/product-quantity-buttons/product-quantity-buttons.tsx +88 -0
  23. package/src/components/_components/section-header/section-header.tsx +1 -1
  24. package/src/components/_components/settings/setting.tsx +1 -1
  25. package/src/components/_components/wishlist-modal/TODO.md +10 -0
  26. package/src/components/_components/wishlist-modal/add-to-wishlist-button.tsx +68 -0
  27. package/src/components/_components/wishlist-modal/add-to-wishlist-dialog.tsx +161 -0
  28. package/src/components/_components/wishlist-modal/remove-from-wishlist-dialog.tsx +174 -0
  29. package/src/components/_components/wishlist-modal/wishlist-modal-item.tsx +65 -0
  30. package/src/components/countries-input.tsx +1 -0
  31. package/src/hooks/use-cart.tsx +82 -0
  32. package/src/hooks/use-variants.tsx +85 -0
  33. package/src/pages/account/addresses/address-form.tsx +289 -0
  34. package/src/pages/account/addresses/address-item.tsx +104 -0
  35. package/src/pages/account/dashboard-header.tsx +123 -0
  36. package/src/pages/account/navigation.tsx +84 -0
  37. package/src/pages/account/no-records.tsx +20 -0
  38. package/src/pages/account/orders/icons/delivery.tsx +14 -0
  39. package/src/pages/account/orders/icons/package-box.tsx +13 -0
  40. package/src/pages/account/orders/icons/truck-filled.tsx +14 -0
  41. package/src/pages/account/orders/order-progress.tsx +111 -0
  42. package/src/pages/account/orders/order-row.tsx +61 -0
  43. package/src/pages/account/orders/order-summary.tsx +83 -0
  44. package/src/pages/account/orders/ordered-products.tsx +114 -0
  45. package/src/pages/account/profile/profile-button.test.tsx +59 -0
  46. package/src/pages/account/profile/profile-button.tsx +50 -0
  47. package/src/pages/account/profile/user-info.tsx +59 -0
  48. package/src/pages/account/styles.ts +32 -0
  49. package/src/pages/account/table-row.tsx +19 -0
  50. package/src/pages/account/wishlist/create-new-list.tsx +167 -0
  51. package/src/pages/account/wishlist/create-wishlist-button.tsx +40 -0
  52. package/src/pages/account/wishlist/wishlist-item.tsx +82 -0
  53. package/src/pages/blog/pagination.tsx +38 -0
  54. package/src/pages/cart/cart-item.tsx +312 -0
  55. package/src/pages/cart/checkout-form.tsx +122 -0
  56. package/src/pages/cart/coupon-entry.tsx +90 -0
  57. package/src/pages/cart/estimate-shipping.tsx +183 -0
  58. package/src/pages/cart/wrapper.tsx +30 -0
  59. package/src/pages/checkout/checkout-alt-form/address-card.tsx +73 -0
  60. package/src/pages/checkout/checkout-alt-form/checkout-form.tsx +133 -0
  61. package/src/pages/checkout/checkout-alt-form/checkout-step.tsx +120 -0
  62. package/src/pages/checkout/checkout-alt-form/customer-information.tsx +94 -0
  63. package/src/pages/checkout/checkout-alt-form/delivery-address.tsx +170 -0
  64. package/src/pages/checkout/checkout-alt-form/delivery-date.tsx +114 -0
  65. package/src/pages/checkout/checkout-alt-form/delivery-options.tsx +186 -0
  66. package/src/pages/checkout/checkout-alt-form/edit-address-form.tsx +130 -0
  67. package/src/pages/checkout/checkout-alt-form/heading.tsx +50 -0
  68. package/src/pages/checkout/checkout-alt-form/index.ts +1 -0
  69. package/src/pages/checkout/checkout-alt-form/new-address-form.tsx +294 -0
  70. package/src/pages/checkout/checkout-alt-form/node.tsx +9 -0
  71. package/src/pages/checkout/checkout-alt-form/payment-details.tsx +344 -0
  72. package/src/pages/checkout/checkout-alt-form/safe-card-preview-data.tsx +36 -0
  73. package/src/pages/checkout/checkout-alt-form/types.ts +20 -0
  74. package/src/pages/checkout/checkout-alt-summary/cart-item.tsx +39 -0
  75. package/src/pages/checkout/checkout-alt-summary/checkout-alt-summary.tsx +40 -0
  76. package/src/pages/checkout/checkout-alt-summary/index.ts +1 -0
  77. package/src/pages/checkout/checkout-alt-summary/list-item.tsx +31 -0
  78. package/src/pages/confirmation/address.tsx +22 -0
  79. package/src/pages/confirmation/confirmation-summary.tsx +52 -0
  80. package/src/pages/confirmation/ordered-products.tsx +108 -0
  81. package/src/pages/product-details/available-shops.tsx +48 -0
  82. package/src/pages/product-details/bopis/find-in-store-button.tsx +63 -0
  83. package/src/pages/product-details/bopis/find-in-store-modal.tsx +257 -0
  84. package/src/pages/product-details/bopis/pickup-option-select.tsx +127 -0
  85. package/src/pages/product-details/bopis/search-header.tsx +69 -0
  86. package/src/pages/product-details/checkbox-label.tsx +20 -0
  87. package/src/pages/product-details/currency.ts +64 -0
  88. package/src/pages/product-details/frequently-bought.tsx +90 -0
  89. package/src/pages/product-details/frequently-product-card.tsx +60 -0
  90. package/src/pages/product-details/product-comment.tsx +44 -0
  91. package/src/pages/product-details/product-description.tsx +22 -0
  92. package/src/pages/product-details/product-filter-card.tsx +257 -0
  93. package/src/pages/product-details/product-intro/product-intro-images.tsx +87 -0
  94. package/src/pages/product-details/product-intro/product-intro.tsx +250 -0
  95. package/src/pages/product-details/product-review.tsx +131 -0
  96. package/src/pages/product-details/product-tabs.tsx +51 -0
  97. package/src/pages/product-details/related-products.tsx +42 -0
  98. package/src/pages/product-details/types.ts +11 -0
  99. package/src/pages/product-list/breadcrumbs.tsx +39 -0
  100. package/src/pages/product-list/checkbox-label.tsx +20 -0
  101. package/src/pages/product-list/facet-group.tsx +125 -0
  102. package/src/pages/product-list/facet.tsx +36 -0
  103. package/src/pages/product-list/list-filter.tsx +40 -0
  104. package/src/pages/product-list/pagination.tsx +80 -0
  105. package/src/pages/product-list/product-list-context.tsx +302 -0
  106. package/src/pages/product-list/product-list-state.ts +187 -0
  107. package/src/pages/product-list/product-rating.tsx +16 -0
  108. package/src/pages/product-list/quick-view-dialog-content.tsx +205 -0
  109. package/src/pages/product-list/quick-view-dialog.tsx +54 -0
  110. package/src/pages/product-list/range-filter.tsx +125 -0
  111. package/src/pages/product-list/scrollbar.tsx +38 -0
  112. package/src/pages/product-list/search-bar.tsx +162 -0
  113. package/src/pages/product-list/selected-facets.tsx +80 -0
  114. package/src/pages/product-list/side-nav.tsx +49 -0
  115. package/src/pages/product-list/swatch.tsx +80 -0
  116. package/src/pages/product-list/types.ts +18 -0
  117. package/src/pages/product-list/use-product-list.ts +12 -0
  118. package/src/pages/product-list/useCategoryFilter.tsx +26 -0
  119. package/src/pages/product-list/utils/facet-helpers.ts +5 -0
  120. package/src/pages/product-list/utils/generate-breadcrumbs.ts +30 -0
  121. package/src/pages/product-list/utils/getCategoryFilterFromSlug.ts +153 -0
  122. package/src/pages/product-list/utils/product-list-helper.ts +111 -0
  123. package/src/pages/product-list/utils/product-list-types.ts +41 -0
  124. package/src/pages/product-list/utils/search-for-category.ts +76 -0
  125. package/src/pages/product-list/utils/sort-options.ts +44 -0
  126. package/src/pages/product-list/utils/use-previous-refiners.ts +14 -0
  127. package/src/pages/quickorder/order-upload.tsx +150 -0
  128. package/src/pages/quickorder/quick-order-form.tsx +343 -0
  129. package/src/pages/quickorder/quick-order-row.tsx +144 -0
  130. package/tsconfig.json +1 -0
  131. package/src/api-manager/datasources/e4/graphqlRequestSdk.ts +0 -10464
  132. /package/src/{components/_components/hooks/useOverflowDetect.ts → hooks/use-overflow-detect.ts} +0 -0
  133. /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
+ }