@evenicanpm/storefront-core 2.2.0 → 2.3.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 +7 -3
- package/src/api-manager/README.md +52 -27
- package/src/api-manager/datasources/d365/d365-cart.datasource.ts +166 -17
- package/src/api-manager/datasources/d365/d365-invoice.datasource.ts +100 -0
- package/src/api-manager/datasources/d365/d365-order.datasource.ts +75 -18
- package/src/api-manager/datasources/d365/d365-product.datasource.ts +19 -1
- package/src/api-manager/datasources/d365/d365-user.datasource.ts +125 -36
- package/src/api-manager/datasources/d365/d365.datasource.ts +3 -0
- package/src/api-manager/datasources/d365/utils/get-context-cookie.ts +10 -3
- package/src/api-manager/datasources/e4/e4.datasource.ts +9 -0
- package/src/api-manager/datasources/e4/graphqlRequestSdk.ts +7 -0
- package/src/api-manager/datasources/e4/order/e4-order.datasource.ts +7 -4
- package/src/api-manager/datasources/e4/user/e4-user.datasource.ts +4 -0
- package/src/api-manager/index.ts +54 -3
- package/src/api-manager/lib/get-graphql-client.ts +11 -5
- package/src/api-manager/schemas/cart.schema.ts +10 -1
- package/src/api-manager/schemas/invoice.schema.ts +55 -0
- package/src/api-manager/schemas/order.schema.ts +13 -1
- package/src/api-manager/schemas/user.schema.ts +8 -0
- package/src/api-manager/services/create-query.ts +4 -10
- package/src/api-manager/services/invoice/queries/get-invoice-details.ts +18 -0
- package/src/api-manager/services/invoice/queries/get-invoices.ts +18 -0
- package/src/api-manager/services/order/queries/get-order-details.ts +1 -1
- package/src/api-manager/services/order/queries/get-orders.ts +6 -3
- package/src/api-manager/services/user/queries/get-customer-balance.ts +20 -0
- package/src/api-manager/types/Datasource.ts +21 -2
- package/src/auth/better-auth.ts +1 -1
- package/src/cms/blocks/block-manager.tsx +18 -3
- package/src/cms/blocks/components/cta-banner.tsx +65 -0
- package/src/cms/blocks/components/footer/footer-content.tsx +61 -0
- package/src/cms/blocks/components/footer/index.tsx +9 -71
- package/src/cms/blocks/components/footer/interfaces.ts +4 -0
- package/src/cms/blocks/components/footer/sections/footer-contact.tsx +18 -9
- package/src/cms/blocks/components/footer/sections/footer-logo.tsx +17 -2
- package/src/cms/blocks/components/hero-image.tsx +83 -0
- package/src/cms/blocks/index.tsx +2 -0
- package/src/cms/blocks/interfaces.ts +20 -0
- package/src/cms/draft-mode-badge.tsx +26 -0
- package/src/cms/queries.ts +81 -0
- package/src/components/_tests_/site-logo.test.tsx +1 -1
- package/src/components/flex-box/flex-box.tsx +2 -0
- package/src/components/header/components/user.tsx +35 -11
- package/src/components/header/sticky-header.tsx +1 -0
- package/src/components/mini-cart/mini-cart.tsx +3 -3
- package/src/components/product-cards/product-card-plp/product-card.tsx +4 -12
- package/src/components/product-cards/product-card-plp/styles/index.ts +15 -0
- package/src/components/site-logo.tsx +1 -1
- package/src/components/wishlist-dialogs/add-to-wishlist/compound/wishlist-dialog-header.tsx +1 -0
- package/src/components/wishlist-dialogs/add-to-wishlist/compound/wishlist-dialog-item.tsx +3 -1
- package/src/pages/account/account-navigation.tsx +88 -60
- package/src/pages/account/account-routes.ts +86 -0
- package/src/pages/account/orders/order-history-filter-types.tsx +26 -0
- package/src/pages/account/orders/order-history-filters-panel.tsx +375 -0
- package/src/pages/account/orders/order-history-header.tsx +54 -0
- package/src/pages/account/orders/order-history-pagination.tsx +55 -0
- package/src/pages/account/orders/order-history-root.tsx +125 -0
- package/src/pages/account/orders/order-history-row.tsx +110 -0
- package/src/pages/account/orders/order-history-search-and-filter.tsx +449 -0
- package/src/pages/account/orders/order-history-search-bar.tsx +84 -0
- package/src/pages/account/orders/order-history-search-dropdown.tsx +53 -0
- package/src/pages/account/orders/order-history-sort.tsx +90 -0
- package/src/pages/account/orders/order-history-table.tsx +54 -0
- package/src/pages/account/orders/order-row.tsx +2 -2
- package/src/pages/account/orders/orders-drop-down-handler.tsx +19 -0
- package/src/pages/account/table-header.tsx +20 -0
- package/src/pages/account/table-row-skeleton.tsx +31 -0
- package/src/pages/account/wishlist/wishlist-item.tsx +4 -1
- package/src/pages/blog/blog-card.tsx +9 -3
- package/src/pages/blog/blog-detail-view.tsx +152 -0
- package/src/pages/blog/blog-list-view.tsx +59 -0
- package/src/pages/cart/estimate-shipping.tsx +6 -5
- package/src/pages/checkout/checkout-alt-form/checkout-form.tsx +43 -21
- package/src/pages/checkout/checkout-alt-form/steps/payment/payment-details.tsx +144 -19
- package/src/pages/cms-page-view.tsx +53 -0
- package/src/pages/product-details/product-description.tsx +25 -13
- package/src/pages/product-details/product-intro/compound/product-intro-images.tsx +26 -8
- package/src/pages/product-details/product-intro/compound/thumbnail-with-skeleton.tsx +17 -21
- package/src/pages/product-details/product-tabs.tsx +20 -4
- package/src/pages/quickorder/order-upload.tsx +12 -6
- package/src/pages/quickorder/quick-order.tsx +25 -20
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { RetrieveCardPaymentAcceptResultInput } from "@evenicanpm/storefront-core/src/api-manager/schemas/cart.schema";
|
|
2
2
|
import usePaymentAcceptResult from "@evenicanpm/storefront-core/src/api-manager/services/cart/mutations/get-payment-accept-result";
|
|
3
3
|
import getPaymentAcceptPoint from "@evenicanpm/storefront-core/src/api-manager/services/cart/queries/get-payment-accept-point";
|
|
4
|
+
import getCustomerBalance from "@evenicanpm/storefront-core/src/api-manager/services/user/queries/get-customer-balance";
|
|
5
|
+
import getUser from "@evenicanpm/storefront-core/src/api-manager/services/user/queries/get-user";
|
|
4
6
|
import { FlexBox } from "@evenicanpm/storefront-core/src/components/flex-box";
|
|
5
7
|
import {
|
|
6
8
|
H6,
|
|
@@ -12,10 +14,20 @@ import type {
|
|
|
12
14
|
CardPaymentAcceptPoint,
|
|
13
15
|
CardPaymentAcceptResult,
|
|
14
16
|
} from "@msdyn365-commerce/retail-proxy";
|
|
15
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
Alert,
|
|
19
|
+
Box,
|
|
20
|
+
Card,
|
|
21
|
+
FormControlLabel,
|
|
22
|
+
Radio,
|
|
23
|
+
RadioGroup,
|
|
24
|
+
} from "@mui/material";
|
|
25
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
16
26
|
import { useTranslations } from "next-intl";
|
|
17
27
|
import React from "react";
|
|
18
28
|
|
|
29
|
+
export type PaymentMethod = "card" | "on-account";
|
|
30
|
+
|
|
19
31
|
enum PaymentConnectorPostMessageType {
|
|
20
32
|
CardPrefix = "msax-cc-cardprefix",
|
|
21
33
|
Error = "msax-cc-error",
|
|
@@ -41,7 +53,10 @@ type PaymentError = {
|
|
|
41
53
|
};
|
|
42
54
|
|
|
43
55
|
type PaymentDetailsCompound = React.FC<CheckoutStepComponentProps> & {
|
|
44
|
-
Preview: React.FC<{
|
|
56
|
+
Preview: React.FC<{
|
|
57
|
+
paymentAcceptResult: CardPaymentAcceptResult | null;
|
|
58
|
+
selectedPaymentMethod: PaymentMethod;
|
|
59
|
+
}>;
|
|
45
60
|
Form: React.FC<{
|
|
46
61
|
iframeReference: React.RefObject<HTMLIFrameElement>;
|
|
47
62
|
iframeHeight: number;
|
|
@@ -62,14 +77,37 @@ const PaymentDetails: PaymentDetailsCompound = (props) => {
|
|
|
62
77
|
|
|
63
78
|
const [paymentAcceptResult, setPaymentAcceptResult] =
|
|
64
79
|
React.useState<CardPaymentAcceptResult | null>(null);
|
|
80
|
+
const [selectedPaymentMethod, setSelectedPaymentMethod] =
|
|
81
|
+
React.useState<PaymentMethod>("card");
|
|
65
82
|
|
|
66
83
|
const t = useTranslations("Checkout.Payment");
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
84
|
+
const queryClient = useQueryClient();
|
|
85
|
+
const { data: user } = getUser.useData();
|
|
86
|
+
const { data: customerBalance } = getCustomerBalance.useData(
|
|
87
|
+
user?.AccountNumber
|
|
88
|
+
? {
|
|
89
|
+
accountNumber: user.AccountNumber,
|
|
90
|
+
invoiceAccountNumber: user.InvoiceAccount,
|
|
91
|
+
}
|
|
92
|
+
: undefined,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const availableCredit = customerBalance
|
|
96
|
+
? customerBalance.CreditLimit -
|
|
97
|
+
customerBalance.Balance -
|
|
98
|
+
customerBalance.PendingBalance
|
|
99
|
+
: 0;
|
|
100
|
+
const canPayOnAccount = availableCredit >= (cart.TotalAmount ?? 0);
|
|
101
|
+
|
|
102
|
+
const { data: paymentAcceptPointData } = getPaymentAcceptPoint.useData(
|
|
103
|
+
{
|
|
104
|
+
cartId: cart.Id,
|
|
105
|
+
totalAmount: cart.TotalAmount ?? 0,
|
|
106
|
+
origin: globalThis.window?.location?.origin,
|
|
107
|
+
isClient: !!globalThis.window,
|
|
108
|
+
},
|
|
109
|
+
{ enabled: selectedPaymentMethod === "card" },
|
|
110
|
+
);
|
|
73
111
|
|
|
74
112
|
const { mutateAsync: retrievePaymentAcceptResult } = usePaymentAcceptResult();
|
|
75
113
|
const iframeReference = React.useRef<HTMLIFrameElement>(null);
|
|
@@ -219,10 +257,29 @@ const PaymentDetails: PaymentDetailsCompound = (props) => {
|
|
|
219
257
|
|
|
220
258
|
React.useEffect(() => {
|
|
221
259
|
if (stepSubmissionState === "submitting") {
|
|
222
|
-
|
|
223
|
-
|
|
260
|
+
if (selectedPaymentMethod === "on-account") {
|
|
261
|
+
// Store on-account selection in query cache for checkout-form to read
|
|
262
|
+
queryClient.setQueryData(
|
|
263
|
+
["selectedPaymentMethod", cart.Id],
|
|
264
|
+
"on-account",
|
|
265
|
+
);
|
|
266
|
+
setStepSubmissionState("idle");
|
|
267
|
+
handleStepCompletion();
|
|
268
|
+
} else {
|
|
269
|
+
// INFO: This listens for `submitting` state, then posts submit message to iframe
|
|
270
|
+
queryClient.setQueryData(["selectedPaymentMethod", cart.Id], "card");
|
|
271
|
+
postSubmitMessage();
|
|
272
|
+
}
|
|
224
273
|
}
|
|
225
|
-
}, [
|
|
274
|
+
}, [
|
|
275
|
+
postSubmitMessage,
|
|
276
|
+
stepSubmissionState,
|
|
277
|
+
selectedPaymentMethod,
|
|
278
|
+
queryClient,
|
|
279
|
+
cart.Id,
|
|
280
|
+
setStepSubmissionState,
|
|
281
|
+
handleStepCompletion,
|
|
282
|
+
]);
|
|
226
283
|
|
|
227
284
|
React.useEffect(() => {
|
|
228
285
|
if (!paymentAcceptPointData || stepViewState !== "active") return;
|
|
@@ -246,21 +303,89 @@ const PaymentDetails: PaymentDetailsCompound = (props) => {
|
|
|
246
303
|
<>
|
|
247
304
|
{children ||
|
|
248
305
|
(stepViewState === "preview" ? (
|
|
249
|
-
<PaymentDetails.Preview
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
iframeReference={iframeReference}
|
|
253
|
-
iframeHeight={iframeHeight}
|
|
254
|
-
errorState={errorState}
|
|
255
|
-
paymentAcceptPointData={paymentAcceptPointData}
|
|
306
|
+
<PaymentDetails.Preview
|
|
307
|
+
paymentAcceptResult={paymentAcceptResult}
|
|
308
|
+
selectedPaymentMethod={selectedPaymentMethod}
|
|
256
309
|
/>
|
|
310
|
+
) : (
|
|
311
|
+
<Box sx={{ m: 2 }}>
|
|
312
|
+
{canPayOnAccount && (
|
|
313
|
+
<>
|
|
314
|
+
<H6 sx={{ mb: 2 }}>{t("selectPaymentMethod")}</H6>
|
|
315
|
+
<RadioGroup
|
|
316
|
+
value={selectedPaymentMethod}
|
|
317
|
+
onChange={(e) =>
|
|
318
|
+
setSelectedPaymentMethod(e.target.value as PaymentMethod)
|
|
319
|
+
}
|
|
320
|
+
sx={{ mb: 3 }}
|
|
321
|
+
>
|
|
322
|
+
<FormControlLabel
|
|
323
|
+
value="card"
|
|
324
|
+
control={<Radio />}
|
|
325
|
+
label={t("payWithCard")}
|
|
326
|
+
/>
|
|
327
|
+
<FormControlLabel
|
|
328
|
+
value="on-account"
|
|
329
|
+
control={<Radio />}
|
|
330
|
+
label={`${t("payOnAccount")} (${t("availableCredit")}: ${availableCredit.toFixed(2)})`}
|
|
331
|
+
/>
|
|
332
|
+
</RadioGroup>
|
|
333
|
+
</>
|
|
334
|
+
)}
|
|
335
|
+
{selectedPaymentMethod === "card" ? (
|
|
336
|
+
<PaymentDetails.Form
|
|
337
|
+
iframeReference={iframeReference}
|
|
338
|
+
iframeHeight={iframeHeight}
|
|
339
|
+
errorState={errorState}
|
|
340
|
+
paymentAcceptPointData={paymentAcceptPointData}
|
|
341
|
+
/>
|
|
342
|
+
) : (
|
|
343
|
+
<Card
|
|
344
|
+
sx={{
|
|
345
|
+
p: 3,
|
|
346
|
+
border: "1px solid",
|
|
347
|
+
borderColor: "success.main",
|
|
348
|
+
backgroundColor: "success.light",
|
|
349
|
+
}}
|
|
350
|
+
>
|
|
351
|
+
<Paragraph fontWeight="bold">{t("onAccountSummary")}</Paragraph>
|
|
352
|
+
<Paragraph>
|
|
353
|
+
{t("chargeAmount")}: {cart.TotalAmount?.toFixed(2)}
|
|
354
|
+
</Paragraph>
|
|
355
|
+
<Paragraph>
|
|
356
|
+
{t("availableCredit")}: {availableCredit.toFixed(2)}
|
|
357
|
+
</Paragraph>
|
|
358
|
+
</Card>
|
|
359
|
+
)}
|
|
360
|
+
</Box>
|
|
257
361
|
))}
|
|
258
362
|
</>
|
|
259
363
|
);
|
|
260
364
|
};
|
|
261
365
|
|
|
262
|
-
PaymentDetails.Preview = ({ paymentAcceptResult }) => {
|
|
366
|
+
PaymentDetails.Preview = ({ paymentAcceptResult, selectedPaymentMethod }) => {
|
|
263
367
|
const t = useTranslations("Checkout.Payment");
|
|
368
|
+
|
|
369
|
+
if (selectedPaymentMethod === "on-account") {
|
|
370
|
+
return (
|
|
371
|
+
<FlexBox flexDirection={"column"} sx={{ m: 2 }}>
|
|
372
|
+
<H6 sx={{ mb: 2 }}>{t("cardPaymentDetails")}</H6>
|
|
373
|
+
<Card
|
|
374
|
+
sx={{
|
|
375
|
+
padding: 2,
|
|
376
|
+
boxShadow: "none",
|
|
377
|
+
border: "1px solid",
|
|
378
|
+
position: "relative",
|
|
379
|
+
backgroundColor: "grey.100",
|
|
380
|
+
borderColor: "secondary.main",
|
|
381
|
+
}}
|
|
382
|
+
>
|
|
383
|
+
<Paragraph fontWeight="bold">{t("payOnAccount")}</Paragraph>
|
|
384
|
+
</Card>
|
|
385
|
+
</FlexBox>
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
264
389
|
if (!paymentAcceptResult) return null;
|
|
265
390
|
|
|
266
391
|
return (
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Box, Container, Paper } from "@mui/material";
|
|
3
|
+
import { useSuspenseQuery } from "@tanstack/react-query";
|
|
4
|
+
import BlockManager from "../cms/blocks/block-manager";
|
|
5
|
+
import type { BlockManagerType } from "../cms/blocks/interfaces";
|
|
6
|
+
import { DraftModeBadge } from "../cms/draft-mode-badge";
|
|
7
|
+
import { cmsQueryOptions } from "../cms/queries";
|
|
8
|
+
import { H1, H4 } from "../components/Typography";
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
slug: string;
|
|
12
|
+
cmsLocale?: string;
|
|
13
|
+
isDraftMode: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const CmsPageView = ({ slug, cmsLocale, isDraftMode }: Props) => {
|
|
17
|
+
const { data } = useSuspenseQuery(
|
|
18
|
+
cmsQueryOptions.page(slug, isDraftMode ? "draft" : "published", cmsLocale),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const pageData = data?.data[0];
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
{isDraftMode && <DraftModeBadge />}
|
|
26
|
+
{pageData ? (
|
|
27
|
+
<Box
|
|
28
|
+
sx={{
|
|
29
|
+
width: "100%",
|
|
30
|
+
maxWidth: "100vw",
|
|
31
|
+
overflowX: "hidden",
|
|
32
|
+
wordWrap: "break-word",
|
|
33
|
+
marginTop: "25px",
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
<BlockManager
|
|
37
|
+
blocks={pageData?.block as unknown as BlockManagerType}
|
|
38
|
+
/>
|
|
39
|
+
</Box>
|
|
40
|
+
) : (
|
|
41
|
+
<Container>
|
|
42
|
+
<Paper sx={{ marginTop: "25px", padding: "50px" }} elevation={1}>
|
|
43
|
+
<H1>e4 Storefront</H1>
|
|
44
|
+
<H4>
|
|
45
|
+
Add a page to the "Page" content-type in CMS to add
|
|
46
|
+
content to this page.
|
|
47
|
+
</H4>
|
|
48
|
+
</Paper>
|
|
49
|
+
</Container>
|
|
50
|
+
)}
|
|
51
|
+
</>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
@@ -3,22 +3,34 @@
|
|
|
3
3
|
import { H3 } from "@evenicanpm/storefront-core/src/components/Typography";
|
|
4
4
|
import type { AttributeValue } from "@msdyn365-commerce/retail-proxy";
|
|
5
5
|
|
|
6
|
-
type Props = { attributes: AttributeValue[] };
|
|
6
|
+
type Props = { attributes: AttributeValue[]; description?: string };
|
|
7
7
|
|
|
8
|
-
export default function ProductDescription({
|
|
8
|
+
export default function ProductDescription({
|
|
9
|
+
attributes,
|
|
10
|
+
description,
|
|
11
|
+
}: Readonly<Props>) {
|
|
9
12
|
return (
|
|
10
13
|
<div>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
{description && (
|
|
15
|
+
<div style={{ marginBottom: 16 }}>
|
|
16
|
+
<p>{description}</p>
|
|
17
|
+
</div>
|
|
18
|
+
)}
|
|
19
|
+
{attributes?.length > 0 && (
|
|
20
|
+
<>
|
|
21
|
+
<H3 mb={2}>Specifications:</H3>
|
|
22
|
+
{attributes.map((attr, index) => {
|
|
23
|
+
if (attr?.TextValue && attr?.Name) {
|
|
24
|
+
return (
|
|
25
|
+
<div key={`attr-${index + 1}`}>
|
|
26
|
+
{attr.Name}: {attr.TextValue}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
})}
|
|
32
|
+
</>
|
|
33
|
+
)}
|
|
22
34
|
</div>
|
|
23
35
|
);
|
|
24
36
|
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "@evenicanpm/storefront-core/src/pages/product-details/product-intro/compound/context";
|
|
12
12
|
import ThumbnailWithSkeleton from "@evenicanpm/storefront-core/src/pages/product-details/product-intro/compound/thumbnail-with-skeleton";
|
|
13
13
|
import Grid from "@mui/material/Grid2";
|
|
14
|
+
import Skeleton from "@mui/material/Skeleton";
|
|
14
15
|
import type React from "react";
|
|
15
16
|
import { useContext, useState } from "react";
|
|
16
17
|
|
|
@@ -124,21 +125,38 @@ ProductImages.Main = () => {
|
|
|
124
125
|
"ProductImages.Main must be used inside a product images context",
|
|
125
126
|
);
|
|
126
127
|
const { name, images, primaryImageUrl, selectedImageIndex } = ctx;
|
|
128
|
+
const [loaded, setLoaded] = useState(false);
|
|
129
|
+
|
|
130
|
+
const src =
|
|
131
|
+
Array.isArray(images) &&
|
|
132
|
+
selectedImageIndex !== undefined &&
|
|
133
|
+
images[selectedImageIndex]
|
|
134
|
+
? images[selectedImageIndex]
|
|
135
|
+
: primaryImageUrl;
|
|
127
136
|
|
|
128
137
|
return (
|
|
129
|
-
<FlexBox
|
|
138
|
+
<FlexBox
|
|
139
|
+
borderRadius={3}
|
|
140
|
+
overflow="hidden"
|
|
141
|
+
justifyContent="center"
|
|
142
|
+
mb={6}
|
|
143
|
+
position="relative"
|
|
144
|
+
>
|
|
145
|
+
{!loaded && (
|
|
146
|
+
<Skeleton
|
|
147
|
+
variant="rounded"
|
|
148
|
+
width={300}
|
|
149
|
+
height={300}
|
|
150
|
+
sx={{ position: "absolute", inset: 0, m: "auto" }}
|
|
151
|
+
/>
|
|
152
|
+
)}
|
|
130
153
|
<ImageWithFallback
|
|
131
154
|
alt={name ?? "product"}
|
|
132
155
|
width={300}
|
|
133
156
|
height={300}
|
|
134
157
|
loading="eager"
|
|
135
|
-
src={
|
|
136
|
-
|
|
137
|
-
selectedImageIndex !== undefined &&
|
|
138
|
-
images[selectedImageIndex]
|
|
139
|
-
? images[selectedImageIndex]
|
|
140
|
-
: primaryImageUrl
|
|
141
|
-
}
|
|
158
|
+
src={src}
|
|
159
|
+
onLoad={() => setLoaded(true)}
|
|
142
160
|
style={{ objectFit: "contain" }}
|
|
143
161
|
/>
|
|
144
162
|
</FlexBox>
|
|
@@ -1,31 +1,27 @@
|
|
|
1
|
-
import { Skeleton } from "@mui/material";
|
|
2
1
|
import Image from "next/image";
|
|
3
|
-
|
|
2
|
+
|
|
3
|
+
const shimmer = `<svg width="50" height="50" xmlns="http://www.w3.org/2000/svg"><rect width="50" height="50" fill="#f0f0f0"/></svg>`;
|
|
4
|
+
const blurDataURL = `data:image/svg+xml;base64,${typeof window === "undefined" ? Buffer.from(shimmer).toString("base64") : btoa(shimmer)}`;
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
+
* Product thumbnail image
|
|
7
8
|
* @category Sub-Component
|
|
8
9
|
*/
|
|
9
10
|
function ThumbnailWithSkeleton({ src }: Readonly<{ src: string }>) {
|
|
10
|
-
const [loaded, setLoaded] = useState(false);
|
|
11
|
-
|
|
12
11
|
return (
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}}
|
|
27
|
-
/>
|
|
28
|
-
</>
|
|
12
|
+
<Image
|
|
13
|
+
src={src}
|
|
14
|
+
alt="Product thumbnail"
|
|
15
|
+
width={50}
|
|
16
|
+
height={50}
|
|
17
|
+
placeholder="blur"
|
|
18
|
+
blurDataURL={blurDataURL}
|
|
19
|
+
style={{
|
|
20
|
+
width: "80%",
|
|
21
|
+
height: "80%",
|
|
22
|
+
objectFit: "cover",
|
|
23
|
+
}}
|
|
24
|
+
/>
|
|
29
25
|
);
|
|
30
26
|
}
|
|
31
27
|
|
|
@@ -23,11 +23,16 @@ const StyledTabs = styled(Tabs)(({ theme }) => ({
|
|
|
23
23
|
},
|
|
24
24
|
}));
|
|
25
25
|
|
|
26
|
-
type Props = {
|
|
26
|
+
type Props = {
|
|
27
|
+
productAttributes: AttributeValue[];
|
|
28
|
+
productDescription?: string;
|
|
29
|
+
children: React.ReactNode;
|
|
30
|
+
};
|
|
27
31
|
|
|
28
32
|
export interface TabsContext {
|
|
29
33
|
selectedOption: number;
|
|
30
34
|
productAttributes: AttributeValue[];
|
|
35
|
+
productDescription?: string;
|
|
31
36
|
handleOptionClick: (_: React.SyntheticEvent, value: number) => void;
|
|
32
37
|
}
|
|
33
38
|
|
|
@@ -54,6 +59,7 @@ const TabsContext = createContext<TabsContext | null>(null);
|
|
|
54
59
|
*/
|
|
55
60
|
export default function ProductTabs({
|
|
56
61
|
productAttributes,
|
|
62
|
+
productDescription,
|
|
57
63
|
children,
|
|
58
64
|
}: Readonly<Props>) {
|
|
59
65
|
const [selectedOption, setSelectedOption] = useState(0);
|
|
@@ -62,7 +68,12 @@ export default function ProductTabs({
|
|
|
62
68
|
|
|
63
69
|
return (
|
|
64
70
|
<TabsContext.Provider
|
|
65
|
-
value={{
|
|
71
|
+
value={{
|
|
72
|
+
selectedOption,
|
|
73
|
+
handleOptionClick,
|
|
74
|
+
productAttributes,
|
|
75
|
+
productDescription,
|
|
76
|
+
}}
|
|
66
77
|
>
|
|
67
78
|
<Box mb={6}>{children}</Box>
|
|
68
79
|
</TabsContext.Provider>
|
|
@@ -130,6 +141,11 @@ ProductTabs.Description = () => {
|
|
|
130
141
|
throw new Error(
|
|
131
142
|
"ProductTabs.Tabs must be used inside a ProductTabsContext",
|
|
132
143
|
);
|
|
133
|
-
const { productAttributes } = ctx;
|
|
134
|
-
return
|
|
144
|
+
const { productAttributes, productDescription } = ctx;
|
|
145
|
+
return (
|
|
146
|
+
<ProductDescription
|
|
147
|
+
attributes={productAttributes}
|
|
148
|
+
description={productDescription}
|
|
149
|
+
/>
|
|
150
|
+
);
|
|
135
151
|
};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import DropZone from "@evenicanpm/storefront-core/src/components/DropZone";
|
|
4
4
|
import { Alert } from "@mui/material";
|
|
5
5
|
import { useTranslations } from "next-intl";
|
|
6
|
-
import { useEffect, useState } from "react";
|
|
6
|
+
import { useEffect, useRef, useState } from "react";
|
|
7
7
|
import type { Accept, FileRejection } from "react-dropzone";
|
|
8
8
|
|
|
9
9
|
export interface OrderUploadItem {
|
|
@@ -42,10 +42,16 @@ export default function OrderUpload({
|
|
|
42
42
|
const t = useTranslations("QuickOrder");
|
|
43
43
|
|
|
44
44
|
const [items, setItems] = useState<OrderUploadItem[]>([]);
|
|
45
|
-
const [
|
|
45
|
+
const [_errCount, setErrCount] = useState<number>(0);
|
|
46
46
|
const [files, setFiles] = useState<File[]>();
|
|
47
47
|
const [fileRejections, setFileRejections] = useState<string[]>([]);
|
|
48
48
|
|
|
49
|
+
const itemsRef = useRef(items);
|
|
50
|
+
itemsRef.current = items;
|
|
51
|
+
|
|
52
|
+
const onUploadItemsRef = useRef(onUploadItems);
|
|
53
|
+
onUploadItemsRef.current = onUploadItems;
|
|
54
|
+
|
|
49
55
|
const accept: Accept = { "text/*": [".xlsx", ".csv", ".xls"] };
|
|
50
56
|
|
|
51
57
|
useEffect(loadXlsxScript, []);
|
|
@@ -60,16 +66,16 @@ export default function OrderUpload({
|
|
|
60
66
|
const sheet = readFirstSheet(buffer);
|
|
61
67
|
const parsed = extractItemsFromSheet(sheet);
|
|
62
68
|
|
|
63
|
-
const { validItems, errors } = validateItems(parsed,
|
|
69
|
+
const { validItems, errors } = validateItems(parsed, itemsRef.current);
|
|
64
70
|
|
|
65
71
|
setItems(validItems);
|
|
66
|
-
setErrCount(
|
|
67
|
-
|
|
72
|
+
setErrCount((prev) => prev + errors);
|
|
73
|
+
onUploadItemsRef.current(validItems);
|
|
68
74
|
}
|
|
69
75
|
};
|
|
70
76
|
|
|
71
77
|
processFiles();
|
|
72
|
-
}, [files
|
|
78
|
+
}, [files]);
|
|
73
79
|
|
|
74
80
|
const handleReject = (rejs: FileRejection[]) => {
|
|
75
81
|
const msgs = rejs.map((r) => `${r.file.name} ${t("fileRejectionMsg")}`);
|
|
@@ -61,6 +61,7 @@ export interface QuickOrderRowData {
|
|
|
61
61
|
|
|
62
62
|
type SearchableProduct = {
|
|
63
63
|
ItemId?: string;
|
|
64
|
+
RecordId?: number;
|
|
64
65
|
};
|
|
65
66
|
|
|
66
67
|
type Ctx = {
|
|
@@ -93,17 +94,22 @@ const QuickOrder = ({ children }: { children?: ReactNode }) => {
|
|
|
93
94
|
|
|
94
95
|
const tableInit = useCallback(() => {
|
|
95
96
|
const next: QuickOrderRowData[] = [];
|
|
96
|
-
for (let i = 0; i <
|
|
97
|
+
for (let i = 0; i < 2; i++) next.push(generateEmptyRow(i + 1));
|
|
97
98
|
setRows(next);
|
|
98
99
|
}, []);
|
|
99
100
|
|
|
100
101
|
const deleteRow = (id: number) => {
|
|
102
|
+
if (rows.length <= 1) return;
|
|
101
103
|
const current = [...rows];
|
|
102
104
|
const idx = current.findIndex((r) => r.id === id);
|
|
103
105
|
if (idx > -1) {
|
|
104
106
|
current.splice(idx, 1);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
+
// Ensure there's always a trailing empty row for input
|
|
108
|
+
const lastRow = current[current.length - 1];
|
|
109
|
+
if (lastRow.value) {
|
|
110
|
+
const newRowId = current.reduce((m, r) => Math.max(m, r.id), 0) + 1;
|
|
111
|
+
current.push(generateEmptyRow(newRowId));
|
|
112
|
+
}
|
|
107
113
|
setRows(current);
|
|
108
114
|
}
|
|
109
115
|
};
|
|
@@ -176,6 +182,14 @@ const QuickOrder = ({ children }: { children?: ReactNode }) => {
|
|
|
176
182
|
}
|
|
177
183
|
|
|
178
184
|
current[idx] = generateRowData(product, id, value, found, quantity);
|
|
185
|
+
|
|
186
|
+
// Auto-add an empty row when the last row has data entered
|
|
187
|
+
const lastRow = current[current.length - 1];
|
|
188
|
+
if (lastRow.value) {
|
|
189
|
+
const newRowId = current.reduce((m, r) => Math.max(m, r.id), 0) + 1;
|
|
190
|
+
current.push(generateEmptyRow(newRowId));
|
|
191
|
+
}
|
|
192
|
+
|
|
179
193
|
setRows(current);
|
|
180
194
|
};
|
|
181
195
|
|
|
@@ -234,9 +248,11 @@ const QuickOrder = ({ children }: { children?: ReactNode }) => {
|
|
|
234
248
|
let rowId: number = curr.reduce((m, r) => Math.max(m, r.id), 0) + 1;
|
|
235
249
|
|
|
236
250
|
items.forEach((it: OrderUploadItem): void => {
|
|
251
|
+
const itemIdStr = it.itemId.toString().toLowerCase();
|
|
237
252
|
const product: ProductSearchResult | undefined = response?.find(
|
|
238
253
|
(r: SearchableProduct) =>
|
|
239
|
-
|
|
254
|
+
itemIdStr === r.ItemId?.toLowerCase() ||
|
|
255
|
+
itemIdStr === String(r.RecordId),
|
|
240
256
|
);
|
|
241
257
|
const qty: number = it.qty || 1;
|
|
242
258
|
curr.push(
|
|
@@ -250,7 +266,8 @@ const QuickOrder = ({ children }: { children?: ReactNode }) => {
|
|
|
250
266
|
);
|
|
251
267
|
});
|
|
252
268
|
|
|
253
|
-
|
|
269
|
+
// Add one trailing empty row for the next entry
|
|
270
|
+
curr.push(generateEmptyRow(rowId++));
|
|
254
271
|
setRows(curr);
|
|
255
272
|
},
|
|
256
273
|
[rows, searchProductsMultiple, generateRowData],
|
|
@@ -316,20 +333,14 @@ QuickOrder.Body = () => {
|
|
|
316
333
|
const { rows } = ctx;
|
|
317
334
|
return (
|
|
318
335
|
<TableBody>
|
|
319
|
-
{rows.map((r
|
|
320
|
-
<QuickOrder.Row key={r.id} row={r}
|
|
336
|
+
{rows.map((r) => (
|
|
337
|
+
<QuickOrder.Row key={r.id} row={r} />
|
|
321
338
|
))}
|
|
322
339
|
</TableBody>
|
|
323
340
|
);
|
|
324
341
|
};
|
|
325
342
|
|
|
326
|
-
QuickOrder.Row = ({
|
|
327
|
-
row,
|
|
328
|
-
isLast,
|
|
329
|
-
}: {
|
|
330
|
-
row: QuickOrderRowData;
|
|
331
|
-
isLast: boolean;
|
|
332
|
-
}) => {
|
|
343
|
+
QuickOrder.Row = ({ row }: { row: QuickOrderRowData }) => {
|
|
333
344
|
const ctx = useQuickOrder();
|
|
334
345
|
const theme = useTheme();
|
|
335
346
|
const t = useTranslations("QuickOrder");
|
|
@@ -378,10 +389,6 @@ QuickOrder.Row = ({
|
|
|
378
389
|
ctx.updateRow(row.id, row.value, e.target.value);
|
|
379
390
|
};
|
|
380
391
|
|
|
381
|
-
const onFocus = () => {
|
|
382
|
-
if (isLast) ctx.addRows();
|
|
383
|
-
};
|
|
384
|
-
|
|
385
392
|
return (
|
|
386
393
|
<TableRow
|
|
387
394
|
sx={{
|
|
@@ -394,7 +401,6 @@ QuickOrder.Row = ({
|
|
|
394
401
|
<QuickOrder.SkuInput
|
|
395
402
|
value={inputValue}
|
|
396
403
|
onChange={onValueChange}
|
|
397
|
-
onFocus={onFocus}
|
|
398
404
|
placeholder={t("Table.itemNo")}
|
|
399
405
|
/>
|
|
400
406
|
</TableCell>
|
|
@@ -403,7 +409,6 @@ QuickOrder.Row = ({
|
|
|
403
409
|
<QuickOrder.QtyInput
|
|
404
410
|
value={row.quantity}
|
|
405
411
|
onChange={onQtyChange}
|
|
406
|
-
onFocus={onFocus}
|
|
407
412
|
placeholder={t("Table.quantity")}
|
|
408
413
|
/>
|
|
409
414
|
</TableCell>
|