@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.
Files changed (80) hide show
  1. package/package.json +7 -3
  2. package/src/api-manager/README.md +52 -27
  3. package/src/api-manager/datasources/d365/d365-cart.datasource.ts +166 -17
  4. package/src/api-manager/datasources/d365/d365-invoice.datasource.ts +100 -0
  5. package/src/api-manager/datasources/d365/d365-order.datasource.ts +75 -18
  6. package/src/api-manager/datasources/d365/d365-product.datasource.ts +19 -1
  7. package/src/api-manager/datasources/d365/d365-user.datasource.ts +125 -36
  8. package/src/api-manager/datasources/d365/d365.datasource.ts +3 -0
  9. package/src/api-manager/datasources/d365/utils/get-context-cookie.ts +10 -3
  10. package/src/api-manager/datasources/e4/e4.datasource.ts +9 -0
  11. package/src/api-manager/datasources/e4/graphqlRequestSdk.ts +7 -0
  12. package/src/api-manager/datasources/e4/order/e4-order.datasource.ts +7 -4
  13. package/src/api-manager/datasources/e4/user/e4-user.datasource.ts +4 -0
  14. package/src/api-manager/index.ts +54 -3
  15. package/src/api-manager/lib/get-graphql-client.ts +11 -5
  16. package/src/api-manager/schemas/cart.schema.ts +10 -1
  17. package/src/api-manager/schemas/invoice.schema.ts +55 -0
  18. package/src/api-manager/schemas/order.schema.ts +13 -1
  19. package/src/api-manager/schemas/user.schema.ts +8 -0
  20. package/src/api-manager/services/create-query.ts +4 -10
  21. package/src/api-manager/services/invoice/queries/get-invoice-details.ts +18 -0
  22. package/src/api-manager/services/invoice/queries/get-invoices.ts +18 -0
  23. package/src/api-manager/services/order/queries/get-order-details.ts +1 -1
  24. package/src/api-manager/services/order/queries/get-orders.ts +6 -3
  25. package/src/api-manager/services/user/queries/get-customer-balance.ts +20 -0
  26. package/src/api-manager/types/Datasource.ts +21 -2
  27. package/src/auth/better-auth.ts +1 -1
  28. package/src/cms/blocks/block-manager.tsx +18 -3
  29. package/src/cms/blocks/components/cta-banner.tsx +65 -0
  30. package/src/cms/blocks/components/footer/footer-content.tsx +61 -0
  31. package/src/cms/blocks/components/footer/index.tsx +9 -71
  32. package/src/cms/blocks/components/footer/interfaces.ts +4 -0
  33. package/src/cms/blocks/components/footer/sections/footer-contact.tsx +18 -9
  34. package/src/cms/blocks/components/footer/sections/footer-logo.tsx +17 -2
  35. package/src/cms/blocks/components/hero-image.tsx +83 -0
  36. package/src/cms/blocks/index.tsx +2 -0
  37. package/src/cms/blocks/interfaces.ts +20 -0
  38. package/src/cms/draft-mode-badge.tsx +26 -0
  39. package/src/cms/queries.ts +81 -0
  40. package/src/components/_tests_/site-logo.test.tsx +1 -1
  41. package/src/components/flex-box/flex-box.tsx +2 -0
  42. package/src/components/header/components/user.tsx +35 -11
  43. package/src/components/header/sticky-header.tsx +1 -0
  44. package/src/components/mini-cart/mini-cart.tsx +3 -3
  45. package/src/components/product-cards/product-card-plp/product-card.tsx +4 -12
  46. package/src/components/product-cards/product-card-plp/styles/index.ts +15 -0
  47. package/src/components/site-logo.tsx +1 -1
  48. package/src/components/wishlist-dialogs/add-to-wishlist/compound/wishlist-dialog-header.tsx +1 -0
  49. package/src/components/wishlist-dialogs/add-to-wishlist/compound/wishlist-dialog-item.tsx +3 -1
  50. package/src/pages/account/account-navigation.tsx +88 -60
  51. package/src/pages/account/account-routes.ts +86 -0
  52. package/src/pages/account/orders/order-history-filter-types.tsx +26 -0
  53. package/src/pages/account/orders/order-history-filters-panel.tsx +375 -0
  54. package/src/pages/account/orders/order-history-header.tsx +54 -0
  55. package/src/pages/account/orders/order-history-pagination.tsx +55 -0
  56. package/src/pages/account/orders/order-history-root.tsx +125 -0
  57. package/src/pages/account/orders/order-history-row.tsx +110 -0
  58. package/src/pages/account/orders/order-history-search-and-filter.tsx +449 -0
  59. package/src/pages/account/orders/order-history-search-bar.tsx +84 -0
  60. package/src/pages/account/orders/order-history-search-dropdown.tsx +53 -0
  61. package/src/pages/account/orders/order-history-sort.tsx +90 -0
  62. package/src/pages/account/orders/order-history-table.tsx +54 -0
  63. package/src/pages/account/orders/order-row.tsx +2 -2
  64. package/src/pages/account/orders/orders-drop-down-handler.tsx +19 -0
  65. package/src/pages/account/table-header.tsx +20 -0
  66. package/src/pages/account/table-row-skeleton.tsx +31 -0
  67. package/src/pages/account/wishlist/wishlist-item.tsx +4 -1
  68. package/src/pages/blog/blog-card.tsx +9 -3
  69. package/src/pages/blog/blog-detail-view.tsx +152 -0
  70. package/src/pages/blog/blog-list-view.tsx +59 -0
  71. package/src/pages/cart/estimate-shipping.tsx +6 -5
  72. package/src/pages/checkout/checkout-alt-form/checkout-form.tsx +43 -21
  73. package/src/pages/checkout/checkout-alt-form/steps/payment/payment-details.tsx +144 -19
  74. package/src/pages/cms-page-view.tsx +53 -0
  75. package/src/pages/product-details/product-description.tsx +25 -13
  76. package/src/pages/product-details/product-intro/compound/product-intro-images.tsx +26 -8
  77. package/src/pages/product-details/product-intro/compound/thumbnail-with-skeleton.tsx +17 -21
  78. package/src/pages/product-details/product-tabs.tsx +20 -4
  79. package/src/pages/quickorder/order-upload.tsx +12 -6
  80. 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 { Alert, Box, Card } from "@mui/material";
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<{ paymentAcceptResult: CardPaymentAcceptResult | null }>;
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 { data: paymentAcceptPointData } = getPaymentAcceptPoint.useData({
68
- cartId: cart.Id,
69
- totalAmount: cart.TotalAmount ?? 0,
70
- origin: globalThis.window?.location?.origin,
71
- isClient: !!globalThis.window,
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
- // INFO: This listens for `submitting` state, then posts submit message to iframe
223
- postSubmitMessage();
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
- }, [postSubmitMessage, stepSubmissionState]);
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 paymentAcceptResult={paymentAcceptResult} />
250
- ) : (
251
- <PaymentDetails.Form
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 &quot;Page&quot; 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({ attributes }: Readonly<Props>) {
8
+ export default function ProductDescription({
9
+ attributes,
10
+ description,
11
+ }: Readonly<Props>) {
9
12
  return (
10
13
  <div>
11
- <H3 mb={2}>Specifications:</H3>
12
- {attributes?.map((attr, index) => {
13
- if (attr?.TextValue && attr?.Name) {
14
- return (
15
- <div key={`attr-${index + 1}`}>
16
- {attr.Name}: {attr.TextValue}
17
- </div>
18
- );
19
- }
20
- return null;
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 borderRadius={3} overflow="hidden" justifyContent="center" mb={6}>
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
- Array.isArray(images) &&
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
- import { useState } from "react";
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
- * Shows a skeleton until the image loads
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
- {!loaded && <Skeleton variant="rounded" width={50} height={50} />}
15
- <Image
16
- src={src}
17
- alt="Product thumbnail"
18
- width={50}
19
- height={50}
20
- onLoad={() => setLoaded(true)}
21
- style={{
22
- display: loaded ? "block" : "none",
23
- width: "80%",
24
- height: "80%",
25
- objectFit: "cover",
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 = { productAttributes: AttributeValue[]; children: React.ReactNode };
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={{ selectedOption, handleOptionClick, productAttributes }}
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 <ProductDescription attributes={productAttributes} />;
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 [errCount, setErrCount] = useState<number>(0);
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, items);
69
+ const { validItems, errors } = validateItems(parsed, itemsRef.current);
64
70
 
65
71
  setItems(validItems);
66
- setErrCount(errCount + errors);
67
- onUploadItems(validItems);
72
+ setErrCount((prev) => prev + errors);
73
+ onUploadItemsRef.current(validItems);
68
74
  }
69
75
  };
70
76
 
71
77
  processFiles();
72
- }, [files, items, onUploadItems, errCount]);
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 < 3; i++) next.push(generateEmptyRow(i + 1));
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
- const newRowId = (current.at(-1)?.id ?? 0) + 1;
106
- if (current.length < 3) current.push(generateEmptyRow(newRowId));
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
- it.itemId.toString().toLowerCase() === r.ItemId?.toLowerCase(),
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
- while (curr.length < 3) curr.push({ ...generateEmptyRow(rowId++) });
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, i) => (
320
- <QuickOrder.Row key={r.id} row={r} isLast={i === rows.length - 1} />
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>