@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,150 @@
|
|
|
1
|
+
import DropZone from "@/components/_components/DropZone";
|
|
2
|
+
import { Alert } from "@mui/material";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { Accept, FileRejection } from "react-dropzone/.";
|
|
5
|
+
|
|
6
|
+
export interface OrderUploadItem {
|
|
7
|
+
itemId: string;
|
|
8
|
+
qty: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface OrderUploadProps {
|
|
12
|
+
onUploadItems: (data: OrderUploadItem[]) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// TODO: fix type
|
|
16
|
+
// eslint-disable-next-line
|
|
17
|
+
declare const XLSX: any;
|
|
18
|
+
|
|
19
|
+
export default function OrderUpload({ onUploadItems }: OrderUploadProps) {
|
|
20
|
+
const [items, setItems] = useState<OrderUploadItem[]>([]);
|
|
21
|
+
const [errCount, setErrCount] = useState<number>(0);
|
|
22
|
+
const [files, setFiles] = useState<File[]>();
|
|
23
|
+
const [fileRejections, setFileRejections] = useState<string[]>([]);
|
|
24
|
+
|
|
25
|
+
const accept: Accept = {
|
|
26
|
+
"text/*": [".xlsx", ".csv", ".xls"],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const script = document.createElement("script");
|
|
31
|
+
script.lang = "javascript";
|
|
32
|
+
script.src =
|
|
33
|
+
"https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js";
|
|
34
|
+
document.head.appendChild(script);
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
const handleReject = (fileRejections: FileRejection[]) => {
|
|
38
|
+
const rejections = fileRejections.map(
|
|
39
|
+
(r) => `${r.file.name} {t('QuickOrder.fileRejectionMsg')}`,
|
|
40
|
+
);
|
|
41
|
+
setFileRejections(rejections);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const resetStates = () => {
|
|
45
|
+
setItems([]);
|
|
46
|
+
setErrCount(0);
|
|
47
|
+
setFileRejections([]);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const getFiles = <T extends File>(files: T[]) => {
|
|
51
|
+
resetStates();
|
|
52
|
+
setFiles(files);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const handleCloseError = () => {
|
|
56
|
+
setFileRejections([]);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const handleUploadItems = (items: OrderUploadItem[]) => {
|
|
61
|
+
onUploadItems(items);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const parseFile = (file: File) => {
|
|
65
|
+
const currItems = items || [];
|
|
66
|
+
let currErrCount = errCount;
|
|
67
|
+
const reader = new FileReader();
|
|
68
|
+
reader.onload = (evt) => {
|
|
69
|
+
const bstr = evt.target?.result;
|
|
70
|
+
const wb = XLSX.read(bstr, { type: "binary" });
|
|
71
|
+
const wsname = wb.SheetNames[0];
|
|
72
|
+
const ws = wb.Sheets[wsname];
|
|
73
|
+
const ref = ws["!ref"];
|
|
74
|
+
let data: OrderUploadItem[] = [];
|
|
75
|
+
try {
|
|
76
|
+
if (ref) {
|
|
77
|
+
const spl = ref.split(":");
|
|
78
|
+
if (spl.length === 2) {
|
|
79
|
+
const rowCount = Number(spl[1].substring(1));
|
|
80
|
+
for (let i = 2; i < rowCount + 1; i++) {
|
|
81
|
+
const qty = ws[`B${i}`]?.v;
|
|
82
|
+
const itemId = ws[`A${i}`]?.v;
|
|
83
|
+
if (itemId) {
|
|
84
|
+
data.push({
|
|
85
|
+
itemId,
|
|
86
|
+
qty,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch (ex) {
|
|
93
|
+
// Do nothing
|
|
94
|
+
}
|
|
95
|
+
if (data.length === 0) {
|
|
96
|
+
data = XLSX.utils.sheet_to_json(ws, {
|
|
97
|
+
header: 0,
|
|
98
|
+
}) as OrderUploadItem[];
|
|
99
|
+
}
|
|
100
|
+
data.forEach((d) => {
|
|
101
|
+
const qty =
|
|
102
|
+
typeof d.qty === "string" || d.qty - Math.floor(d.qty) !== 0
|
|
103
|
+
? 1
|
|
104
|
+
: d.qty;
|
|
105
|
+
if (d.itemId && d.qty) {
|
|
106
|
+
currItems.push({ qty: qty, itemId: d.itemId });
|
|
107
|
+
} else {
|
|
108
|
+
currErrCount++;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
setItems(currItems);
|
|
112
|
+
setErrCount(currErrCount);
|
|
113
|
+
handleUploadItems(currItems);
|
|
114
|
+
};
|
|
115
|
+
reader.readAsBinaryString(file);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const isValidFile = (file: File): boolean => {
|
|
119
|
+
parseFile(file);
|
|
120
|
+
return file.type === "text/csv";
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (files) {
|
|
124
|
+
files.forEach((file) => {
|
|
125
|
+
if (isValidFile(file)) {
|
|
126
|
+
//console.log("Valid!");
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
// TODO: fix dependency
|
|
131
|
+
//eslint-disable-next-line
|
|
132
|
+
}, [items, files, errCount]);
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<>
|
|
136
|
+
{fileRejections.length > 0 && (
|
|
137
|
+
<Alert severity="error" onClose={handleCloseError}>
|
|
138
|
+
{fileRejections.map((msg, index) => (
|
|
139
|
+
<div key={index}>{msg}</div>
|
|
140
|
+
))}
|
|
141
|
+
</Alert>
|
|
142
|
+
)}
|
|
143
|
+
<DropZone
|
|
144
|
+
onChange={getFiles}
|
|
145
|
+
handleReject={handleReject}
|
|
146
|
+
accept={accept}
|
|
147
|
+
/>
|
|
148
|
+
</>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { FlexBox } from "@/components/_components/flex-box/index";
|
|
4
|
+
import {
|
|
5
|
+
Button,
|
|
6
|
+
Container,
|
|
7
|
+
Table,
|
|
8
|
+
TableBody,
|
|
9
|
+
TableCell,
|
|
10
|
+
TableContainer,
|
|
11
|
+
TableHead,
|
|
12
|
+
TableRow,
|
|
13
|
+
} from "@mui/material";
|
|
14
|
+
import QuickOrderRow, { QuickOrderRowData } from "./quick-order-row";
|
|
15
|
+
import { useCallback, useEffect, useState } from "react";
|
|
16
|
+
import { ProductSearchResult } from "@msdyn365-commerce/retail-proxy";
|
|
17
|
+
import useSearchProducts from "@/api-manager/services/product/mutations/search-products";
|
|
18
|
+
import { BorderShadowCard } from "@/components/BoxShadowCard";
|
|
19
|
+
import { H2, Span } from "@/components/_components/Typography";
|
|
20
|
+
import useAddToCart from "@/api-manager/services/cart/mutations/add-to-cart";
|
|
21
|
+
import OrderUpload, { OrderUploadItem } from "./order-upload";
|
|
22
|
+
import { useTranslations } from "next-intl";
|
|
23
|
+
import getCart from "@/api-manager/services/cart/queries/get-cart";
|
|
24
|
+
|
|
25
|
+
export enum RowError {
|
|
26
|
+
ProductNotFound,
|
|
27
|
+
InvalidQuantity,
|
|
28
|
+
OutOfStock,
|
|
29
|
+
None,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default function QuickOrderForm() {
|
|
33
|
+
const [rows, setRows] = useState<QuickOrderRowData[]>([]);
|
|
34
|
+
const { mutateAsync: searchProducts } = useSearchProducts();
|
|
35
|
+
const { mutateAsync: addToCart } = useAddToCart();
|
|
36
|
+
const { data: cart } = getCart.useData();
|
|
37
|
+
|
|
38
|
+
const t = useTranslations("QuickOrder");
|
|
39
|
+
|
|
40
|
+
const _generateEmptyRow = (id: number) => {
|
|
41
|
+
return {
|
|
42
|
+
id,
|
|
43
|
+
value: "",
|
|
44
|
+
quantity: "1",
|
|
45
|
+
product: null,
|
|
46
|
+
rowError: RowError.None,
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// initialize the table with 3 empty rows
|
|
51
|
+
const _tableInit = useCallback(() => {
|
|
52
|
+
const numEmptyRows = 3;
|
|
53
|
+
const rows: QuickOrderRowData[] = [];
|
|
54
|
+
for (let i = 0; i < numEmptyRows; i++) {
|
|
55
|
+
rows.push(_generateEmptyRow(i + 1));
|
|
56
|
+
}
|
|
57
|
+
setRows(rows);
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
const _handleRowDelete = (id: number) => {
|
|
61
|
+
const currentRows = [...rows];
|
|
62
|
+
const idx = currentRows.findIndex((r) => r.id === id);
|
|
63
|
+
if (idx > -1) {
|
|
64
|
+
currentRows.splice(idx, 1);
|
|
65
|
+
const newRowId = currentRows[currentRows.length - 1].id + 1;
|
|
66
|
+
if (currentRows.length < 3) {
|
|
67
|
+
currentRows.push(_generateEmptyRow(newRowId));
|
|
68
|
+
}
|
|
69
|
+
setRows(currentRows);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const _handleRowUpdate = async (
|
|
74
|
+
id: number,
|
|
75
|
+
value: string,
|
|
76
|
+
quantity: string,
|
|
77
|
+
) => {
|
|
78
|
+
const currentRows = [...rows];
|
|
79
|
+
const idx = currentRows.findIndex((row) => row.id === id);
|
|
80
|
+
|
|
81
|
+
// if value changes and is beyond the min characters, do a search for the product
|
|
82
|
+
let product = currentRows[idx].product;
|
|
83
|
+
let foundProduct = false;
|
|
84
|
+
if (currentRows[idx].value !== value) {
|
|
85
|
+
currentRows[idx].value = value;
|
|
86
|
+
const response = await searchProductByRow(id, value);
|
|
87
|
+
product = (response && response[0]) || null;
|
|
88
|
+
}
|
|
89
|
+
foundProduct = !!product;
|
|
90
|
+
currentRows[idx] = _generateRowData(
|
|
91
|
+
product,
|
|
92
|
+
id,
|
|
93
|
+
value,
|
|
94
|
+
foundProduct,
|
|
95
|
+
quantity,
|
|
96
|
+
);
|
|
97
|
+
setRows(currentRows);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const searchProductByRow = async (_id: number, searchText: string) => {
|
|
101
|
+
// call getProductSearchResults with a single entry in an array
|
|
102
|
+
const minQueryLength = 5;
|
|
103
|
+
let response;
|
|
104
|
+
if (searchText.length >= minQueryLength) {
|
|
105
|
+
response = await getProductSearchResults([searchText]);
|
|
106
|
+
}
|
|
107
|
+
return response;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const getProductSearchResults = useCallback(
|
|
111
|
+
async (itemIds: string[]) => {
|
|
112
|
+
// API call to retrieve matching products from database
|
|
113
|
+
const results = await searchProducts({ itemIds });
|
|
114
|
+
return results;
|
|
115
|
+
},
|
|
116
|
+
[searchProducts],
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const searchProductsMultiple = useCallback(
|
|
120
|
+
async (itemIds: string[]) => {
|
|
121
|
+
// call getProductSearchResults for all item ids
|
|
122
|
+
let response;
|
|
123
|
+
const minQueryLength = 5;
|
|
124
|
+
const validItemIds = itemIds.filter((it) => it.length >= minQueryLength);
|
|
125
|
+
if (validItemIds) {
|
|
126
|
+
response = await getProductSearchResults(validItemIds);
|
|
127
|
+
}
|
|
128
|
+
return response;
|
|
129
|
+
},
|
|
130
|
+
[getProductSearchResults],
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const _generateRowData = useCallback(
|
|
134
|
+
(
|
|
135
|
+
product: ProductSearchResult | null,
|
|
136
|
+
rowId: number,
|
|
137
|
+
searchText: string,
|
|
138
|
+
foundProduct: boolean,
|
|
139
|
+
quantity: string,
|
|
140
|
+
) => {
|
|
141
|
+
let rowError = RowError.None;
|
|
142
|
+
if (
|
|
143
|
+
Number.isNaN(Number(quantity)) ||
|
|
144
|
+
Number(quantity) < 1 ||
|
|
145
|
+
!Number.isInteger(Number(quantity))
|
|
146
|
+
) {
|
|
147
|
+
rowError = RowError.InvalidQuantity;
|
|
148
|
+
} else if (!foundProduct) {
|
|
149
|
+
rowError = RowError.ProductNotFound;
|
|
150
|
+
}
|
|
151
|
+
// TODO: Out of Stock
|
|
152
|
+
const row: QuickOrderRowData = {
|
|
153
|
+
..._generateEmptyRow(rowId),
|
|
154
|
+
product: foundProduct ? product : null,
|
|
155
|
+
value: searchText,
|
|
156
|
+
quantity: quantity.toString(),
|
|
157
|
+
rowError: rowError,
|
|
158
|
+
};
|
|
159
|
+
return row;
|
|
160
|
+
},
|
|
161
|
+
[],
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const _handleAddRows = () => {
|
|
165
|
+
const currentRows = [...rows];
|
|
166
|
+
|
|
167
|
+
const maxRowId = currentRows.reduce(
|
|
168
|
+
(max, row) => (row.id > max ? row.id : max),
|
|
169
|
+
0,
|
|
170
|
+
);
|
|
171
|
+
const newRowId = maxRowId + 1;
|
|
172
|
+
currentRows.push(_generateEmptyRow(newRowId));
|
|
173
|
+
currentRows.push(_generateEmptyRow(newRowId + 1));
|
|
174
|
+
|
|
175
|
+
setRows(currentRows);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const _generateFile = () => {
|
|
179
|
+
if (rows.length === 0) return;
|
|
180
|
+
|
|
181
|
+
const header = "SKU,Quantity\n";
|
|
182
|
+
|
|
183
|
+
// parse data into CSV format
|
|
184
|
+
const csvContent =
|
|
185
|
+
"data:text/csv;charset=utf-8," +
|
|
186
|
+
header +
|
|
187
|
+
rows.map(({ value, quantity }) => `${value},${quantity}`).join("\n");
|
|
188
|
+
const encodedUri = encodeURI(csvContent);
|
|
189
|
+
const link = document.createElement("a");
|
|
190
|
+
link.setAttribute("href", encodedUri);
|
|
191
|
+
link.setAttribute("download", "quick_order.csv");
|
|
192
|
+
document.body.appendChild(link);
|
|
193
|
+
link.click();
|
|
194
|
+
document.body.removeChild(link);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const _generateRow = (row: QuickOrderRowData, isLast: boolean) => {
|
|
198
|
+
return (
|
|
199
|
+
<QuickOrderRow
|
|
200
|
+
key={row.id}
|
|
201
|
+
id={row.id}
|
|
202
|
+
value={row.value}
|
|
203
|
+
quantity={row.quantity}
|
|
204
|
+
product={row.product}
|
|
205
|
+
onRowDelete={_handleRowDelete}
|
|
206
|
+
onRowChange={_handleRowUpdate}
|
|
207
|
+
onAddRows={_handleAddRows}
|
|
208
|
+
isLastRow={isLast}
|
|
209
|
+
rowError={row.rowError}
|
|
210
|
+
/>
|
|
211
|
+
);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const handleAddToCart = async () => {
|
|
215
|
+
// add all rows that have a RowError of type None with requested quantities
|
|
216
|
+
if (!cart) return;
|
|
217
|
+
|
|
218
|
+
const products = rows
|
|
219
|
+
.filter((row) => row.product && row.rowError === RowError.None)
|
|
220
|
+
.map((productRow) => ({
|
|
221
|
+
ProductId: Number(productRow.product?.RecordId),
|
|
222
|
+
Quantity: Number(productRow.quantity),
|
|
223
|
+
}));
|
|
224
|
+
if (products.length > 0) {
|
|
225
|
+
await addToCart({
|
|
226
|
+
cartId: cart.Id,
|
|
227
|
+
version: cart.Version,
|
|
228
|
+
cartLine: products,
|
|
229
|
+
});
|
|
230
|
+
_tableInit();
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const handleUploadedItems = useCallback(
|
|
235
|
+
async (items: OrderUploadItem[]) => {
|
|
236
|
+
const itemIds = items.map((it) => it.itemId.toString());
|
|
237
|
+
const response = await searchProductsMultiple(itemIds);
|
|
238
|
+
|
|
239
|
+
// add items to table
|
|
240
|
+
let currRows = [...rows];
|
|
241
|
+
const idx = currRows.findIndex((r) => !r.value);
|
|
242
|
+
if (idx > -1) {
|
|
243
|
+
currRows = currRows.slice(0, idx);
|
|
244
|
+
}
|
|
245
|
+
let rowId =
|
|
246
|
+
currRows.reduce((max, row) => (row.id > max ? row.id : max), 0) + 1;
|
|
247
|
+
|
|
248
|
+
items.forEach((it) => {
|
|
249
|
+
const product = response?.find((r) => {
|
|
250
|
+
const itemId = it.itemId.toString().toLowerCase();
|
|
251
|
+
const searchText = r.ItemId?.toLowerCase();
|
|
252
|
+
return itemId === searchText;
|
|
253
|
+
});
|
|
254
|
+
const qty = it.qty || 1;
|
|
255
|
+
const rowData = _generateRowData(
|
|
256
|
+
product || null,
|
|
257
|
+
rowId++,
|
|
258
|
+
it.itemId,
|
|
259
|
+
!!product,
|
|
260
|
+
qty.toString(),
|
|
261
|
+
);
|
|
262
|
+
currRows.push(rowData);
|
|
263
|
+
});
|
|
264
|
+
while (currRows.length < 3) {
|
|
265
|
+
currRows.push({ ..._generateEmptyRow(rowId++) });
|
|
266
|
+
}
|
|
267
|
+
setRows(currRows);
|
|
268
|
+
},
|
|
269
|
+
[_generateRowData, rows, searchProductsMultiple],
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
_tableInit();
|
|
274
|
+
}, [_tableInit]);
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
<Container>
|
|
278
|
+
<BorderShadowCard>
|
|
279
|
+
<FlexBox justifyContent="left" px={3}>
|
|
280
|
+
<H2>{t("title")}</H2>
|
|
281
|
+
</FlexBox>
|
|
282
|
+
<FlexBox justifyContent="left" px={3}>
|
|
283
|
+
<Span>{t("entryBySku")}</Span>
|
|
284
|
+
</FlexBox>
|
|
285
|
+
</BorderShadowCard>
|
|
286
|
+
<BorderShadowCard sx={{ padding: 0 }}>
|
|
287
|
+
<TableContainer sx={{ borderRadius: "4px" }}>
|
|
288
|
+
<Table>
|
|
289
|
+
<TableHead sx={{ backgroundColor: "#323337" }}>
|
|
290
|
+
<TableRow>
|
|
291
|
+
<TableCell sx={{ color: "white", width: { xs: "30%" } }}>
|
|
292
|
+
{t("Table.itemNo")}
|
|
293
|
+
</TableCell>
|
|
294
|
+
<TableCell sx={{ color: "white", width: { xs: "20%" } }}>
|
|
295
|
+
{t("Table.quantity")}
|
|
296
|
+
</TableCell>
|
|
297
|
+
<TableCell sx={{ color: "white" }}>
|
|
298
|
+
{t("Table.itemName")}
|
|
299
|
+
</TableCell>
|
|
300
|
+
<TableCell
|
|
301
|
+
sx={{
|
|
302
|
+
color: "white",
|
|
303
|
+
display: { xs: "none", sm: "table-cell" },
|
|
304
|
+
}}
|
|
305
|
+
>
|
|
306
|
+
{t("Table.price")}
|
|
307
|
+
</TableCell>
|
|
308
|
+
<TableCell sx={{ color: "white" }}>
|
|
309
|
+
{t("Table.delete")}
|
|
310
|
+
</TableCell>
|
|
311
|
+
</TableRow>
|
|
312
|
+
</TableHead>
|
|
313
|
+
<TableBody>
|
|
314
|
+
{rows.map((r, i) => _generateRow(r, i === rows.length - 1))}
|
|
315
|
+
</TableBody>
|
|
316
|
+
</Table>
|
|
317
|
+
</TableContainer>
|
|
318
|
+
</BorderShadowCard>
|
|
319
|
+
<BorderShadowCard>
|
|
320
|
+
<FlexBox sx={{ justifyContent: "center", gap: { xs: 2, sm: 20 } }}>
|
|
321
|
+
<Button
|
|
322
|
+
variant="contained"
|
|
323
|
+
color="secondary"
|
|
324
|
+
onClick={handleAddToCart}
|
|
325
|
+
>
|
|
326
|
+
{t("addToCartBtn")}
|
|
327
|
+
</Button>
|
|
328
|
+
<Button variant="contained" color="secondary" onClick={_tableInit}>
|
|
329
|
+
{t("resetFormBtn")}
|
|
330
|
+
</Button>
|
|
331
|
+
<Button variant="contained" color="secondary" onClick={_generateFile}>
|
|
332
|
+
{t("downloadFileBtn")}
|
|
333
|
+
</Button>
|
|
334
|
+
</FlexBox>
|
|
335
|
+
</BorderShadowCard>
|
|
336
|
+
<BorderShadowCard>
|
|
337
|
+
<FlexBox justifyContent="center" gap={2} flexDirection="column" px={2}>
|
|
338
|
+
<OrderUpload onUploadItems={handleUploadedItems} />
|
|
339
|
+
</FlexBox>
|
|
340
|
+
</BorderShadowCard>
|
|
341
|
+
</Container>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ProductSearchResult } from "@msdyn365-commerce/retail-proxy";
|
|
4
|
+
import {
|
|
5
|
+
Button,
|
|
6
|
+
TableCell,
|
|
7
|
+
TableRow,
|
|
8
|
+
TextField,
|
|
9
|
+
useTheme,
|
|
10
|
+
} from "@mui/material";
|
|
11
|
+
import React, { useEffect, useMemo, useState } from "react";
|
|
12
|
+
import DeleteIcon from "@mui/icons-material/Delete";
|
|
13
|
+
import { RowError } from "./quick-order-form";
|
|
14
|
+
import { Span } from "@/components/_components/Typography";
|
|
15
|
+
import { useTranslations } from "next-intl";
|
|
16
|
+
import useCurrencyFormatter from "@/lib/currency-formatter";
|
|
17
|
+
import _ from "lodash";
|
|
18
|
+
|
|
19
|
+
export interface QuickOrderRowData {
|
|
20
|
+
id: number;
|
|
21
|
+
value: string;
|
|
22
|
+
quantity: string;
|
|
23
|
+
product: ProductSearchResult | null;
|
|
24
|
+
rowError: RowError;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface QuickOrderRowProps {
|
|
28
|
+
id: number;
|
|
29
|
+
value: string;
|
|
30
|
+
quantity: string;
|
|
31
|
+
product: ProductSearchResult | null;
|
|
32
|
+
rowError: RowError;
|
|
33
|
+
onRowDelete: (id: number) => void;
|
|
34
|
+
onAddRows: () => void;
|
|
35
|
+
onRowChange: (id: number, value: string, quantity: string) => void;
|
|
36
|
+
isLastRow: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default function QuickOrderRow({
|
|
40
|
+
id,
|
|
41
|
+
value,
|
|
42
|
+
quantity,
|
|
43
|
+
product,
|
|
44
|
+
onRowDelete,
|
|
45
|
+
onAddRows,
|
|
46
|
+
onRowChange,
|
|
47
|
+
isLastRow,
|
|
48
|
+
rowError,
|
|
49
|
+
}: QuickOrderRowProps) {
|
|
50
|
+
const theme = useTheme();
|
|
51
|
+
const t = useTranslations("QuickOrder");
|
|
52
|
+
const { formatCurrency } = useCurrencyFormatter();
|
|
53
|
+
|
|
54
|
+
const [inputValue, setInputValue] = useState<string>(value || "");
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
setInputValue(value || "");
|
|
58
|
+
}, [value]);
|
|
59
|
+
|
|
60
|
+
const debouncedOnChange = useMemo(
|
|
61
|
+
() =>
|
|
62
|
+
_.debounce((val) => {
|
|
63
|
+
onRowChange(id, val, quantity);
|
|
64
|
+
}, 500),
|
|
65
|
+
[onRowChange, id, quantity],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const handleValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
69
|
+
setInputValue(e.target.value);
|
|
70
|
+
debouncedOnChange(e.target.value);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const handleQuantityChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
74
|
+
onRowChange(id, value, e.target.value);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const addRows = () => {
|
|
78
|
+
onAddRows();
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const handleRowDelete = () => onRowDelete(id);
|
|
82
|
+
|
|
83
|
+
const getRowStatusMessage = (product: ProductSearchResult | null) => {
|
|
84
|
+
switch (rowError) {
|
|
85
|
+
case RowError.InvalidQuantity:
|
|
86
|
+
return t("invalidQuantity");
|
|
87
|
+
case RowError.ProductNotFound:
|
|
88
|
+
return t("productNotFound");
|
|
89
|
+
case RowError.OutOfStock:
|
|
90
|
+
return t("OOS");
|
|
91
|
+
}
|
|
92
|
+
if (product) {
|
|
93
|
+
return product.Name;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<TableRow
|
|
100
|
+
sx={{
|
|
101
|
+
...(rowError !== RowError.None && {
|
|
102
|
+
bgcolor: theme.palette.error.light,
|
|
103
|
+
}),
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
<TableCell sx={{ width: { xs: "30%" } }}>
|
|
107
|
+
<TextField
|
|
108
|
+
value={inputValue}
|
|
109
|
+
InputProps={{ sx: { bgcolor: "white" } }}
|
|
110
|
+
onChange={handleValueChange}
|
|
111
|
+
onFocus={() => {
|
|
112
|
+
if (isLastRow && addRows) {
|
|
113
|
+
addRows();
|
|
114
|
+
}
|
|
115
|
+
}}
|
|
116
|
+
/>
|
|
117
|
+
</TableCell>
|
|
118
|
+
<TableCell sx={{ width: { xs: "20%" } }}>
|
|
119
|
+
<TextField
|
|
120
|
+
value={quantity}
|
|
121
|
+
InputProps={{ sx: { bgcolor: "white" } }}
|
|
122
|
+
onChange={handleQuantityChange}
|
|
123
|
+
onFocus={() => {
|
|
124
|
+
if (isLastRow && addRows) {
|
|
125
|
+
addRows();
|
|
126
|
+
}
|
|
127
|
+
}}
|
|
128
|
+
/>
|
|
129
|
+
</TableCell>
|
|
130
|
+
<TableCell>{getRowStatusMessage(product)}</TableCell>
|
|
131
|
+
<TableCell sx={{ display: { xs: "none", sm: "table-cell" } }}>
|
|
132
|
+
{product?.Price ? formatCurrency(product?.Price) : ""}
|
|
133
|
+
</TableCell>
|
|
134
|
+
<TableCell>
|
|
135
|
+
<Button variant="contained" color="secondary" onClick={handleRowDelete}>
|
|
136
|
+
<DeleteIcon />
|
|
137
|
+
<Span sx={{ display: { xs: "none", sm: "inline-flex" } }}>
|
|
138
|
+
{t("deleteBtn")}
|
|
139
|
+
</Span>
|
|
140
|
+
</Button>
|
|
141
|
+
</TableCell>
|
|
142
|
+
</TableRow>
|
|
143
|
+
);
|
|
144
|
+
}
|