From 455fc60c293e92b20f7c13620bbd39e6e44c5021 Mon Sep 17 00:00:00 2001 From: "aton.xia" Date: Fri, 10 Feb 2023 13:46:33 +0800 Subject: [PATCH] feat: update quote file upload --- .../src/components/B3QuantityTextField.tsx | 12 +- .../src/pages/quote/QuoteDetail.tsx | 39 ++- .../storefront/src/pages/quote/QuoteDraft.tsx | 20 +- .../src/pages/quote/components/FileUpload.tsx | 308 ++++++++++++++++++ .../quote/components/QuoteAttachment.tsx | 83 ++++- .../components/ReAddToCart.tsx | 17 + .../src/shared/service/b2b/graphql/quote.ts | 3 +- .../src/shared/service/request/b3Fetch.ts | 20 +- 8 files changed, 475 insertions(+), 27 deletions(-) create mode 100644 apps/storefront/src/pages/quote/components/FileUpload.tsx diff --git a/apps/storefront/src/components/B3QuantityTextField.tsx b/apps/storefront/src/components/B3QuantityTextField.tsx index 6a01e4d6..c3cd0e6e 100644 --- a/apps/storefront/src/components/B3QuantityTextField.tsx +++ b/apps/storefront/src/components/B3QuantityTextField.tsx @@ -57,6 +57,10 @@ export const B3QuantityTextField = (props: B3NumberTextFieldProps) => { validMessage = ('Out of stock') } else if (isStock === '1' && quantity > stock) { validMessage = (`${stock} in stock`) + } else if (minQuantity !== 0 && quantity < minQuantity) { + validMessage = (`Min is ${minQuantity}`) + } else if (maxQuantity !== 0 && quantity > maxQuantity) { + validMessage = (`Max is ${maxQuantity}`) } setValidMessage(validMessage) @@ -69,13 +73,7 @@ export const B3QuantityTextField = (props: B3NumberTextFieldProps) => { } const handleBlur = () => { - let quantity = parseInt(`${value}`, 10) || 0 - - if (minQuantity !== 0 && quantity < minQuantity) { - quantity = minQuantity - } else if (maxQuantity !== 0 && quantity > maxQuantity) { - quantity = maxQuantity - } + const quantity = parseInt(`${value}`, 10) || 0 onChange(quantity, !validateQuantity(quantity)) } diff --git a/apps/storefront/src/pages/quote/QuoteDetail.tsx b/apps/storefront/src/pages/quote/QuoteDetail.tsx index 8324d290..9be8d13a 100644 --- a/apps/storefront/src/pages/quote/QuoteDetail.tsx +++ b/apps/storefront/src/pages/quote/QuoteDetail.tsx @@ -71,6 +71,8 @@ const QuoteDetail = () => { const [quoteDetail, setQuoteDetail] = useState({}) const [productList, setProductList] = useState([]) const [currency, setCurrency] = useState({}) + const [fileList, setFileList] = useState([]) + const [quoteSummary, setQuoteSummary] = useState({ originalSubtotal: 0, discount: 0, @@ -113,6 +115,34 @@ const QuoteDetail = () => { setCurrency(quote.currency) setProductList(quote.productsList) + const { + backendAttachFiles = [], + storefrontAttachFiles = [], + } = quote + + const newFileList: CustomFieldItems[] = [] + storefrontAttachFiles.forEach((file: CustomFieldItems) => { + newFileList.push({ + fileName: file.fileName, + fileType: file.fileType, + fileUrl: file.fileUrl, + id: file.fileUrl, + title: 'Uploaded by customer: xxxx', // TODO + }) + }) + + backendAttachFiles.forEach((file: CustomFieldItems) => { + newFileList.push({ + fileName: file.fileName, + fileType: file.fileType, + fileUrl: file.fileUrl, + id: file.fileUrl, + title: 'Uploaded by sales rep: xxxx', // TODO + }) + }) + + setFileList(newFileList) + return quote } catch (err: any) { snackbar.error(err) @@ -339,7 +369,14 @@ const QuoteDetail = () => { displayPrint: 'none', }} > - + { + fileList.length > 0 && ( + + ) + } diff --git a/apps/storefront/src/pages/quote/QuoteDraft.tsx b/apps/storefront/src/pages/quote/QuoteDraft.tsx index 3ab51690..e4f45393 100644 --- a/apps/storefront/src/pages/quote/QuoteDraft.tsx +++ b/apps/storefront/src/pages/quote/QuoteDraft.tsx @@ -374,6 +374,19 @@ const QuoteDraft = ({ quoteTableRef.current?.refreshList() } + const getFileList = (files: CustomFieldItems[]) => { + if (role === 100) { + return [] + } + + return files.map((file) => ({ + fileUrl: file.fileUrl, + fileName: file.fileName, + fileType: file.fileType, + fileSize: `${file.fileSize}`, + })) + } + const handleSubmit = async () => { setLoading(true) try { @@ -467,6 +480,8 @@ const QuoteDraft = ({ const currency = getDefaultCurrencyInfo() + const fileList = getFileList(info.fileInfo || []) + const data = { notes: note, legalTerms: '', @@ -482,6 +497,7 @@ const QuoteDraft = ({ billingAddress, contactInfo, productList, + fileList, currency: { currencyExchangeRate: currency.currency_exchange_rate, token: currency.token, @@ -745,7 +761,9 @@ const QuoteDraft = ({ - + { + role !== 100 && + } diff --git a/apps/storefront/src/pages/quote/components/FileUpload.tsx b/apps/storefront/src/pages/quote/components/FileUpload.tsx new file mode 100644 index 00000000..0d77c006 --- /dev/null +++ b/apps/storefront/src/pages/quote/components/FileUpload.tsx @@ -0,0 +1,308 @@ +import { + Box, + Tooltip, + Typography, +} from '@mui/material' + +import AttachFileIcon from '@mui/icons-material/AttachFile' +import HelpIcon from '@mui/icons-material/Help' +import DeleteIcon from '@mui/icons-material/Delete' + +import styled from '@emotion/styled' + +import { + useState, +} from 'react' + +import { + DropzoneArea, +} from 'react-mui-dropzone' + +import { + noop, +} from 'lodash' + +import { + v1 as uuid, +} from 'uuid' + +import { + B3Sping, +} from '@/components/spin/B3Sping' + +import { + FILE_UPLOAD_ACCEPT_TYPE, +} from '../../../constants' + +import { + uploadB2BFile, +} from '@/shared/service/b2b' + +import { + snackbar, +} from '@/utils' + +const FileUploadContainer = styled(Box)(() => ({ + '& .file-upload-area': { + cursor: 'pointer', + '& .MuiDropzoneArea-textContainer': { + display: 'flex', + alignItems: 'center', + color: '#1976D2', + }, + '& .MuiDropzoneArea-text': { + order: 1, + textTransform: 'uppercase', + fontWeight: 500, + fontSize: '14px', + lineHeight: '24px', + }, + }, +})) + +const FileListItem = styled(Box)(() => ({ + display: 'flex', + background: 'rgba(25, 118, 210, 0.3)', + borderRadius: '18px', + padding: '6px 8px', + alignItems: 'center', + margin: '0 0 2px', + color: 'rgba(0, 0, 0, 0.54)', + '& .fileList-name-area': { + display: 'flex', + flex: 1, + alignItems: 'center', + }, + '& .fileList-name': { + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + overflow: 'hidden', + flexGrow: 1, + flexBasis: '100px', + maxWidth: '200px', + color: '#313440', + fontSize: '14px', + cursor: 'pointer', + }, +})) + +const FileUserTitle = styled(Typography)(() => ({ + marginBottom: '16px', + fontSize: '10px', + color: 'rgba(0, 0, 0, 0.38)', + padding: '0 12px', + textAlign: 'right', + wordBreak: 'break-word', +})) + +export interface FileObjects { + id: string, + fileName: string, + fileType: string, + fileUrl: string, + fileSize?: number, + title?: string, + hasDelete?: boolean, +} + +interface FileUploadProps { + title?: string, + tips?: string, + maxFileSize?: number, + fileNumber?: number, + acceptedFiles?: string[], + onchange?: (file: FileObjects) => void, + fileList: FileObjects[], + allowUpload?: boolean, + onDelete?: (id: string) => void, +} + +export const FileUpload = (props: FileUploadProps) => { + const { + title = 'Add Attachment', + tips = 'You can add up to 3 files,not bigger that 2MB each.', + maxFileSize = 2097152, // 2MB + fileNumber = 3, + acceptedFiles = FILE_UPLOAD_ACCEPT_TYPE, + onchange = noop, + fileList = [], + allowUpload = true, + onDelete = noop, + } = props + + const [loading, setLoading] = useState(false) + + const getMaxFileSizeLabel = (maxSize: number) => { + if (maxSize / 1048576 > 1) { + return `${(maxSize / 1048576).toFixed(1)}MB` + } + if (maxSize / 1024 > 1) { + return `${(maxSize / 1024).toFixed(1)}KB` + } + return `${maxSize}B` + } + + const getRejectMessage = ( + rejectedFile: File, + acceptedFiles: string[], + maxFileSize: number, + ) => { + const { + size, + type, + } = rejectedFile + + let isAcceptFileType = false + acceptedFiles.forEach((acceptedFileType: string) => { + isAcceptFileType = new RegExp(acceptedFileType).test(type) || isAcceptFileType + }) + + let message = '' + if (!isAcceptFileType) { + message = 'file type not support' + } + + if (size > maxFileSize) { + message = `file exceeds upload limit. Maximum file size is ${getMaxFileSizeLabel(maxFileSize)}` + } + + if (message) { + snackbar.error(message) + } + + return message + } + + const getFileLimitExceedMessage = () => { + snackbar.error(`file exceeds upload limit. Maximum file size is ${getMaxFileSizeLabel(maxFileSize)}`) + return '' + } + + const handleChange = async (files: File[]) => { + const file = files.length > 0 ? files[0] : null + + if (file && fileList.length >= fileNumber) { + snackbar.error(`You can add up to ${fileNumber} files`) + return + } + + if (file) { + try { + setLoading(true) + const { + code, + data: fileInfo, + message, + } = await uploadB2BFile({ + file, + type: 'quoteAttachedFile', + }) + if (code === 200) { + onchange({ + ...fileInfo, + id: uuid(), + }) + } else { + snackbar.error(message) + } + + setLoading(false) + } catch (error) { + setLoading(false) + } + } + } + + const handleDelete = (id: string) => { + onDelete(id) + } + + const downloadFile = (fileUrl: string) => { + if (fileUrl) { + window.open(fileUrl, '_blank') + } + } + + return ( + + + + { + fileList.map((file, index) => ( + + + + + { downloadFile(file.fileUrl) }} + > + {file.fileName} + + + { + file.hasDelete && ( + { handleDelete(file.id) }} + /> + ) + } + + + {file.title || ''} + + + )) + } + + { + allowUpload && ( + + + + + + + + + + ) + } + + + ) +} diff --git a/apps/storefront/src/pages/quote/components/QuoteAttachment.tsx b/apps/storefront/src/pages/quote/components/QuoteAttachment.tsx index 9bb27814..4b3c23c6 100644 --- a/apps/storefront/src/pages/quote/components/QuoteAttachment.tsx +++ b/apps/storefront/src/pages/quote/components/QuoteAttachment.tsx @@ -7,6 +7,7 @@ import { import { useState, useEffect, + useContext, } from 'react' import { @@ -17,37 +18,89 @@ import { B3LStorage, } from '@/utils' -export const QuoteAttachment = () => { - const [fileInfo, setFileInfo] = useState('') +import { + GlobaledContext, +} from '@/shared/global' + +import { + FileObjects, + FileUpload, +} from './FileUpload' + +interface QuoteAttachmentProps{ + allowUpload?: boolean, + defaultFileList?: FileObjects[] +} + +export const QuoteAttachment = (props: QuoteAttachmentProps) => { + const { + allowUpload = true, + defaultFileList = [], + } = props + + const { + state: { + customer: { + firstName = '', + lastName = '', + }, + }, + } = useContext(GlobaledContext) + + const [fileList, setFileList] = useState(defaultFileList) useEffect(() => { - // const { - // fileInfo = '', - // } = B3LStorage.get('MyQuoteInfo') || {} + if (defaultFileList.length <= 0) { + const { + fileInfo = [], + }: CustomFieldItems = B3LStorage.get('MyQuoteInfo') || {} - setFileInfo('file') + setFileList(typeof fileInfo !== 'object' ? [] : fileInfo) + } }, []) - useEffect(() => { - const quoteInfo = B3LStorage.get('MyQuoteInfo') + const saveQuoteInfo = (newFileInfo: FileObjects[]) => { + const quoteInfo = B3LStorage.get('MyQuoteInfo') || {} if (quoteInfo) { B3LStorage.set('MyQuoteInfo', { ...quoteInfo, - fileInfo, + fileInfo: newFileInfo, }) } - }, [fileInfo]) + } + + const handleChange = (file: FileObjects) => { + const newFileList = [...fileList, { + ...file, + title: `Uploaded by customer: ${firstName} ${lastName}`, + hasDelete: true, + }] + + saveQuoteInfo(newFileList) + + setFileList(newFileList) + } + + const handleDelete = (id: string) => { + const newFileList = fileList.filter((file) => file.id !== id) + + saveQuoteInfo(newFileList) + + setFileList(newFileList) + } return ( - - {fileInfo} + + diff --git a/apps/storefront/src/pages/shoppingListDetails/components/ReAddToCart.tsx b/apps/storefront/src/pages/shoppingListDetails/components/ReAddToCart.tsx index 7ff40917..18d64e3b 100644 --- a/apps/storefront/src/pages/shoppingListDetails/components/ReAddToCart.tsx +++ b/apps/storefront/src/pages/shoppingListDetails/components/ReAddToCart.tsx @@ -259,6 +259,23 @@ export const ReAddToCart = (props: ShoppingProductsProps) => { const handleClearNoStock = () => { const newProduct = products.filter((item: ProductsProps) => item.isStock === '0' || item.stock !== 0) + newProduct.forEach((product) => { + const { + node: { + quantity, + }, + minQuantity = 0, + maxQuantity = 0, + } = product + + const quantityNumber = parseInt(`${quantity}`, 10) || 0 + if (minQuantity !== 0 && quantityNumber < minQuantity) { + product.node.quantity = minQuantity + } else if (maxQuantity !== 0 && quantityNumber > maxQuantity) { + product.node.quantity = maxQuantity + } + }) + setValidateFailureProducts(newProduct) } diff --git a/apps/storefront/src/shared/service/b2b/graphql/quote.ts b/apps/storefront/src/shared/service/b2b/graphql/quote.ts index 3f21acef..613f1950 100644 --- a/apps/storefront/src/shared/service/b2b/graphql/quote.ts +++ b/apps/storefront/src/shared/service/b2b/graphql/quote.ts @@ -131,7 +131,8 @@ const quoteCreate = (data: CustomFieldItems) => `mutation{ shippingAddress: ${convertObjectToGraphql(data.shippingAddress)} billingAddress: ${convertObjectToGraphql(data.billingAddress)} contactInfo: ${convertObjectToGraphql(data.contactInfo)} - productList: ${convertArrayToGraphql(data.productList || [])} + productList: ${convertArrayToGraphql(data.productList || [])}, + fileList: ${convertArrayToGraphql(data.fileList || [])} }) { quote{ id, diff --git a/apps/storefront/src/shared/service/request/b3Fetch.ts b/apps/storefront/src/shared/service/request/b3Fetch.ts index c908c72c..f5a4383b 100644 --- a/apps/storefront/src/shared/service/request/b3Fetch.ts +++ b/apps/storefront/src/shared/service/request/b3Fetch.ts @@ -1,3 +1,6 @@ +import { + keyBy, +} from 'lodash' import { b3Fetch, } from './fetch' @@ -47,17 +50,30 @@ import { // timeout: 10000, // } -function request(path: string, config?: T, type?: string) { +interface Config{ + headers?: { + [key: string]: string + } +} + +function request(path: string, config?: T & Config, type?: string) { const url = RequestType.B2BRest === type ? `${B2B_BASIC_URL}${path}` : path const getToken = type === RequestType.BCRest ? { 'x-xsrf-token': getCookie('XSRF-TOKEN'), } : { authToken: `${B3SStorage.get('B3B2BToken') || ''}`, } + + const { + headers = { + 'content-type': 'application/json', + }, + } = config || {} + const init = { ...config, headers: { - 'content-type': 'application/json', + ...headers, ...getToken, }, }