From 4428b81a293ef106ad8f69a59711da8da6477fcc Mon Sep 17 00:00:00 2001 From: BrianJiang2021 <80307788+BrianJiang2021@users.noreply.github.com> Date: Mon, 6 Jan 2025 10:52:36 +0800 Subject: [PATCH] feat: quote add extra fields (#153) * feat: quote add extra fields * fix: optimize and fix bugs * fix: quote extra fields fix bugs * fix: submit quote info * feat: quote add cc email * fix: quoteExtraFieldsConfig graphql add channelId --- .../src/components/B3CustomForm.tsx | 11 + .../form/B2BControlMultiTextField.tsx | 222 ++++++++++++++++++ apps/storefront/src/components/form/index.ts | 1 + .../src/pages/QuoteDetail/index.tsx | 48 +++- .../storefront/src/pages/QuoteDraft/index.tsx | 111 ++++++++- .../pages/quote/components/ContactInfo.tsx | 206 +++++++++++++--- .../quote/components/QuoteDetailHeader.tsx | 16 -- .../src/pages/quote/components/QuoteInfo.tsx | 34 ++- .../QuoteInfoAndExtrafieldsItem.tsx | 75 ++++++ .../pages/quote/utils/getQuoteExtraFields.ts | 70 ++++++ .../src/shared/service/b2b/api/global.ts | 6 + .../src/shared/service/b2b/graphql/quote.ts | 29 +++ .../src/shared/service/b2b/index.ts | 5 +- apps/storefront/src/store/slices/quoteInfo.ts | 3 + apps/storefront/src/types/quotes.ts | 70 ++++++ packages/lang/locales/en.json | 8 +- 16 files changed, 844 insertions(+), 71 deletions(-) create mode 100644 apps/storefront/src/components/form/B2BControlMultiTextField.tsx create mode 100644 apps/storefront/src/pages/quote/components/QuoteInfoAndExtrafieldsItem.tsx create mode 100644 apps/storefront/src/pages/quote/utils/getQuoteExtraFields.ts diff --git a/apps/storefront/src/components/B3CustomForm.tsx b/apps/storefront/src/components/B3CustomForm.tsx index 2c6e64492..fbd33c91a 100644 --- a/apps/storefront/src/components/B3CustomForm.tsx +++ b/apps/storefront/src/components/B3CustomForm.tsx @@ -2,6 +2,7 @@ import { Grid } from '@mui/material'; import B3UI from './form/ui'; import { + B2BControlMultiTextField, B3ControlAutocomplete, B3ControlCheckbox, B3ControlFileUpload, @@ -91,6 +92,16 @@ export default function B3CustomForm(props: B3UI.B3CustomFormProps) { getValues={getValues} /> )} + {['multiInputText'].includes(fieldType) && ( + + )} ); diff --git a/apps/storefront/src/components/form/B2BControlMultiTextField.tsx b/apps/storefront/src/components/form/B2BControlMultiTextField.tsx new file mode 100644 index 000000000..c8d107056 --- /dev/null +++ b/apps/storefront/src/components/form/B2BControlMultiTextField.tsx @@ -0,0 +1,222 @@ +import { KeyboardEvent } from 'react'; +import { Controller } from 'react-hook-form'; +import { useB3Lang } from '@b3/lang'; +import { Add, Clear } from '@mui/icons-material'; +import { Box, TextField, Typography } from '@mui/material'; +import { concat, debounce, uniq } from 'lodash-es'; +import isEmpty from 'lodash-es/isEmpty'; + +import Form from './ui'; + +export default function B2BControlMultiTextField({ control, errors, ...rest }: Form.B3UIProps) { + const { + fieldType, + isAutoComplete = false, + name, + default: defaultValue, + required, + label, + validate, + variant, + rows, + min, + max, + minLength, + maxLength, + fullWidth = true, + disabled, + labelName, + size, + readOnly, + sx = {}, + isTip = false, + tipText = '', + extraPadding, + isEnterTrigger, + handleSave, + getValues, + InputProps = {}, + existValue, + setError, + setValue, + } = rest; + const b3Lang = useB3Lang(); + + const requiredText = b3Lang('global.validate.required', { + label: labelName || label, + }); + + const fieldsProps = { + type: fieldType, + name, + defaultValue, + rules: { + required: required && requiredText, + validate: validate && ((v: string) => validate(v, b3Lang)), + }, + control, + }; + + const textField = { + type: fieldType, + name, + label, + rows, + disabled, + variant, + fullWidth, + required, + size, + }; + + const otherProps = { + inputProps: { + min, + max, + maxLength, + minLength, + readOnly, + }, + }; + + const handleAddNewItem = () => { + const currentValue = getValues(name).trim(); + const isValidValue = validate(currentValue, b3Lang); + if (isValidValue) { + setError(name, { + type: 'custom', + message: isValidValue, + }); + } else { + const newItems = uniq(concat(existValue, currentValue.length ? [currentValue] : [])); + + setValue(name, ''); + if (handleSave) handleSave(newItems); + } + }; + + const handleDelete = (currentItem: string) => { + const newItems = existValue.filter((item: string) => item !== currentItem); + if (handleSave) handleSave(newItems); + }; + + const handleKeyDown = debounce((event: KeyboardEvent) => { + if (event.key === 'Enter') { + handleAddNewItem(); + } else { + event.preventDefault(); + } + }, 300); + + const autoCompleteFn = () => { + if (!isAutoComplete) { + return { + autoComplete: 'off', + }; + } + return {}; + }; + + return ['multiInputText'].includes(fieldType) ? ( + + {labelName && ( + + {`${labelName} :`} + + )} + ( + {}} + InputProps={ + !isEmpty(InputProps) + ? { ...InputProps } + : { + endAdornment: ( + + ), + } + } + {...autoCompleteFn()} + /> + )} + /> + {existValue.length ? ( + + {existValue.map((item: string) => ( + + {item} + { + handleDelete(item); + }} + /> + + ))} + + ) : null} + {isTip && ( + + {tipText} + + )} + + ) : null; +} diff --git a/apps/storefront/src/components/form/index.ts b/apps/storefront/src/components/form/index.ts index c3efb98e6..73eb5a6d6 100644 --- a/apps/storefront/src/components/form/index.ts +++ b/apps/storefront/src/components/form/index.ts @@ -9,3 +9,4 @@ export { default as B3ControlSelect } from './B3ControlSelect'; export { default as B3ControlSwatchRadio } from './B3ControlSwatchRadio'; export { default as B3ControlTextField } from './B3ControlTextField'; export { default as B3ControlAutocomplete } from './B3ControlAutocomplete'; +export { default as B2BControlMultiTextField } from './B2BControlMultiTextField'; diff --git a/apps/storefront/src/pages/QuoteDetail/index.tsx b/apps/storefront/src/pages/QuoteDetail/index.tsx index 414de26ff..feef83eef 100644 --- a/apps/storefront/src/pages/QuoteDetail/index.tsx +++ b/apps/storefront/src/pages/QuoteDetail/index.tsx @@ -25,6 +25,7 @@ import { useAppSelector, } from '@/store'; import { Currency } from '@/types'; +import { QuoteExtraFieldsData } from '@/types/quotes'; import { snackbar } from '@/utils'; import { getVariantInfoOOSAndPurchase } from '@/utils/b3Product/b3Product'; import { conversionProductsList } from '@/utils/b3Product/shared/config'; @@ -40,6 +41,7 @@ import QuoteInfo from '../quote/components/QuoteInfo'; import QuoteNote from '../quote/components/QuoteNote'; import QuoteTermsAndConditions from '../quote/components/QuoteTermsAndConditions'; import { ProductInfoProps } from '../quote/shared/config'; +import getB2BQuoteExtraFields from '../quote/utils/getQuoteExtraFields'; import { handleQuoteCheckout } from '../quote/utils/quoteCheckout'; function QuoteDetail() { @@ -237,6 +239,26 @@ function QuoteDetail() { return undefined; }; + const getQuoteExtraFields = async (currentExtraFields: QuoteExtraFieldsData[]) => { + const extraFieldsInfo = await getB2BQuoteExtraFields(); + const quoteCurrentExtraFields: QuoteExtraFieldsData[] = []; + if (extraFieldsInfo.length) { + extraFieldsInfo.forEach((item) => { + const extraField = item; + const currentExtraField = currentExtraFields.find( + (field: QuoteExtraFieldsData) => field.fieldName === extraField.name, + ); + + quoteCurrentExtraFields.push({ + fieldName: extraField.name || '', + fieldValue: currentExtraField?.fieldValue || extraField.default, + }); + }); + } + + return quoteCurrentExtraFields; + }; + const getQuoteDetail = async () => { setIsRequestLoading(true); setIsShowFooter(false); @@ -254,8 +276,12 @@ function QuoteDetail() { const { quote } = await fn(data); const productsWithMoreInfo = await handleGetProductsById(quote.productsList); + const quoteExtraFieldInfos = await getQuoteExtraFields(quote.extraFields); - setQuoteDetail(quote); + setQuoteDetail({ + ...quote, + extraFields: quoteExtraFieldInfos, + }); setQuoteSummary({ originalSubtotal: quote.subtotal, discount: quote.discount, @@ -512,6 +538,24 @@ function QuoteDetail() { return true; }; + const quoteAndExtraFieldsInfo = useMemo(() => { + const currentExtraFields = quoteDetail?.extraFields?.map( + (field: { fieldName: string; fieldValue: string | number }) => ({ + fieldName: field.fieldName, + value: field.fieldValue, + }), + ); + + return { + info: { + quoteTitle: quoteDetail?.quoteTitle || '', + referenceNumber: quoteDetail?.referenceNumber || '', + }, + extraFields: currentExtraFields || [], + recipients: quoteDetail?.recipients || [], + }; + }, [quoteDetail]); + useScrollBar(false); return ( @@ -531,7 +575,6 @@ function QuoteDetail() { exportPdf={exportPdf} printQuote={printQuote} role={role} - quoteTitle={quoteDetail.quoteTitle} salesRepInfo={quoteDetail.salesRepInfo} /> @@ -541,6 +584,7 @@ function QuoteDetail() { }} > (false); const [quoteId, setQuoteId] = useState(''); const [currentCreatedAt, setCurrentCreatedAt] = useState(''); + const [extraFields, setExtraFields] = useState([]); const quoteSummaryRef = useRef(null); @@ -252,6 +262,25 @@ function QuoteDraft({ setOpenPage }: PageProps) { })); setAddressList(list); } + + const extraFieldsInfo = await getB2BQuoteExtraFields(); + if (extraFieldsInfo.length) { + setExtraFields(extraFieldsInfo); + const preExtraFields = quoteInfo.extraFields; + const defaultValues = extraFieldsInfo?.map((field) => { + const defaultValue = + preExtraFields?.find((item: QuoteExtraFields) => item.fieldName === field.name) + ?.value || field?.default; + + return { + id: +field.id, + fieldName: field.name, + value: defaultValue || '', + }; + }); + quoteInfo.extraFields = defaultValues; + } + if ( quoteInfo && (!quoteInfo?.contactInfo || validateObject(quoteInfo, 'contactInfo')) && @@ -274,6 +303,19 @@ function QuoteDraft({ setOpenPage }: PageProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const quoteAndExtraFieldsInfo = useMemo(() => { + const contactInfo: CustomFieldItems = quoteinfo.contactInfo || {}; + + return { + info: { + quoteTitle: contactInfo?.quoteTitle || '', + referenceNumber: quoteinfo?.referenceNumber || '', + }, + extraFields: quoteinfo.extraFields || [], + recipients: quoteinfo.recipients || [], + }; + }, [quoteinfo]); + const getAddress = () => { const addresssaveInfo = { shippingAddress, @@ -290,12 +332,49 @@ function QuoteDraft({ setOpenPage }: PageProps) { return addresssaveInfo; }; - const handleSaveInfoClick = async () => { + const handleSaveCCEmail = (ccEmail: string[]) => { const saveInfo = cloneDeep(quoteinfo); + saveInfo.recipients = ccEmail; + + dispatch(setDraftQuoteInfo(saveInfo)); + }; + + const handleCollectingData = async (saveInfo: QuoteInfoType) => { if (contactInfoRef?.current) { const contactInfo = await contactInfoRef.current.getContactInfoValue(); - if (!contactInfo) return; - saveInfo.contactInfo = contactInfo; + if (!contactInfo) return false; + + const currentRecipients = saveInfo?.recipients || []; + if (contactInfo.ccEmail.trim().length) { + saveInfo.recipients = uniq(concat(currentRecipients, [contactInfo.ccEmail])); + } + + saveInfo.contactInfo = { + name: contactInfo?.name, + email: contactInfo?.email, + companyName: contactInfo?.companyName || '', + phoneNumber: contactInfo?.phoneNumber, + quoteTitle: contactInfo?.quoteTitle, + }; + saveInfo.referenceNumber = contactInfo?.referenceNumber || ''; + + const extraFieldsInfo = extraFields.map((field) => ({ + id: +field.id, + fieldName: field.name, + value: field.name ? contactInfo[field.name] : '', + })); + saveInfo.extraFields = extraFieldsInfo; + + return true; + } + return false; + }; + + const handleSaveInfoClick = async () => { + const saveInfo = cloneDeep(quoteinfo); + if (contactInfoRef?.current) { + const datas = await handleCollectingData(saveInfo); + if (!datas) return; } const { shippingAddress, billingAddress } = getAddress(); @@ -372,6 +451,11 @@ function QuoteDraft({ setOpenPage }: PageProps) { setLoading(true); try { const info = cloneDeep(quoteinfo); + if (isEdit && contactInfoRef?.current) { + const datas = await handleCollectingData(info); + if (!datas) return; + } + const contactInfo = info?.contactInfo || {}; const quoteTitle = contactInfo?.quoteTitle || ''; @@ -521,6 +605,9 @@ function QuoteDraft({ setOpenPage }: PageProps) { thousandsToken: currency.thousands_token, currencyCode: currency.currency_code, }, + referenceNumber: `${info.referenceNumber}` || '', + extraFields: info.extraFields || [], + recipients: info.recipients || [], }; const fn = +role === 99 ? createBCQuote : createQuote; @@ -551,11 +638,7 @@ function QuoteDraft({ setOpenPage }: PageProps) { setQuoteSubmissionResponseOpen(true); } } catch (error: any) { - if (error.message && error.message.length > 0) { - snackbar.error(error.message, { - isClose: true, - }); - } + b2bLogger.error(error); } finally { setLoading(false); } @@ -720,6 +803,7 @@ function QuoteDraft({ setOpenPage }: PageProps) { {!isEdit && ( void; +} + const emailValidate = validatorRules(['email']); -const getContactInfo = (isMobile: boolean, b3Lang: LangFormatFunction) => { +const getContactInfo = (isMobile: boolean, b3Lang: LangFormatFunction, isGuest: boolean) => { const contactInfo = [ { name: 'name', @@ -23,6 +38,7 @@ const getContactInfo = (isMobile: boolean, b3Lang: LangFormatFunction) => { xs: isMobile ? 12 : 6, variant: 'filled', size: 'small', + disabled: !isGuest, }, { name: 'email', @@ -34,10 +50,11 @@ const getContactInfo = (isMobile: boolean, b3Lang: LangFormatFunction) => { variant: 'filled', size: 'small', validate: emailValidate, + disabled: !isGuest, }, { - name: 'companyName', - label: b3Lang('quoteDraft.contactInfo.companyName'), + name: 'phoneNumber', + label: b3Lang('quoteDraft.contactInfo.phone'), required: false, default: '', fieldType: 'text', @@ -45,9 +62,28 @@ const getContactInfo = (isMobile: boolean, b3Lang: LangFormatFunction) => { variant: 'filled', size: 'small', }, + ]; + + return contactInfo; +}; + +const getQuoteInfo = ({ + isMobile, + b3Lang, + quoteExtraFields, + referenceNumber, + recipients, + handleSaveClick, +}: GetQuoteInfoProps) => { + const currentExtraFields = quoteExtraFields.map((field) => ({ + ...field, + xs: isMobile ? 12 : 6, + })); + + const quoteInfo = [ { - name: 'phoneNumber', - label: b3Lang('quoteDraft.contactInfo.phone'), + name: 'quoteTitle', + label: b3Lang('quoteDraft.contactInfo.quoteTitle'), required: false, default: '', fieldType: 'text', @@ -56,26 +92,58 @@ const getContactInfo = (isMobile: boolean, b3Lang: LangFormatFunction) => { size: 'small', }, { - name: 'quoteTitle', - label: b3Lang('quoteDraft.contactInfo.quoteTitle'), + name: 'referenceNumber', + label: b3Lang('quoteDraft.contactInfo.referenceNumber'), required: false, - default: '', + default: referenceNumber || '', fieldType: 'text', xs: isMobile ? 12 : 6, variant: 'filled', size: 'small', }, + ...currentExtraFields, + { + name: 'ccEmail', + label: b3Lang('quoteDraft.contactInfo.ccEmail'), + required: false, + default: '', + fieldType: 'multiInputText', + xs: isMobile ? 12 : 6, + variant: 'filled', + size: 'small', + isEmail: true, + existValue: recipients, + validate: emailValidate, + isEnterTrigger: true, + handleSave: handleSaveClick, + }, ]; - return contactInfo; + return quoteInfo; }; interface ContactInfoProps { info: ContactInfoType; + quoteExtraFields: QuoteFormattedItemsProps[]; emailAddress?: string; + referenceNumber?: string | undefined; + extraFieldsDefault: QuoteExtraFields[]; + recipients: string[] | undefined; + handleSaveCCEmail: (ccEmail: string[]) => void; } -function ContactInfo({ info, emailAddress }: ContactInfoProps, ref: any) { +function ContactInfo( + { + info, + emailAddress, + quoteExtraFields, + referenceNumber, + extraFieldsDefault, + recipients, + handleSaveCCEmail, + }: ContactInfoProps, + ref: any, +) { const { control, getValues, @@ -86,6 +154,8 @@ function ContactInfo({ info, emailAddress }: ContactInfoProps, ref: any) { } = useForm({ mode: 'onSubmit', }); + const role = useAppSelector(({ company }) => company.customer.role); + const isGuest = role === CustomerRole.GUEST; const isValidUserType = useAppSelector(isValidUserTypeSelector); @@ -99,9 +169,15 @@ function ContactInfo({ info, emailAddress }: ContactInfoProps, ref: any) { setValue(item, info && info[item as keyof ContactInfoType]); }); } + + if (extraFieldsDefault.length) { + extraFieldsDefault.forEach((item) => { + if (item.fieldName) setValue(item.fieldName, item.value); + }); + } // Disable eslint exhaustive-deps rule for setValue dispatcher // eslint-disable-next-line react-hooks/exhaustive-deps - }, [info]); + }, [info, extraFieldsDefault]); const validateEmailValue = async (emailValue: string) => { if (emailAddress === trim(emailValue)) return true; @@ -116,6 +192,42 @@ function ContactInfo({ info, emailAddress }: ContactInfoProps, ref: any) { return isValidUserType; }; + const validateQuoteExtraFieldsInfo = async () => { + const values = getValues(); + const extraFields = quoteExtraFields.map((field) => ({ + fieldName: field.name, + fieldValue: field.name ? values[field.name] : '', + })); + + const res = await validateQuoteExtraFields({ + extraFields, + }); + + if (res.code !== 200) { + const message = res.data?.errMsg || res.message || ''; + + const messageArr = message.split(':'); + + if (messageArr.length >= 2) { + const field = quoteExtraFields?.find((field) => field.name === messageArr[0]); + if (field && field.name) { + setError(field.name, { + type: 'manual', + message: messageArr[1], + }); + + return false; + } + } + return false; + } + return true; + }; + + const handleSaveClick = (ccEmails: string[]) => { + handleSaveCCEmail(ccEmails); + }; + const getContactInfoValue = async () => { let isValid = true; await handleSubmit( @@ -127,6 +239,10 @@ function ContactInfo({ info, emailAddress }: ContactInfoProps, ref: any) { }, )(); + if (isValid) { + isValid = await validateQuoteExtraFieldsInfo(); + } + return isValid ? getValues() : isValid; }; @@ -134,28 +250,56 @@ function ContactInfo({ info, emailAddress }: ContactInfoProps, ref: any) { getContactInfoValue, })); - const contactInfo = getContactInfo(isMobile, b3Lang); + const contactInfo = getContactInfo(isMobile, b3Lang, isGuest); + const quoteInfo = getQuoteInfo({ + isMobile, + b3Lang, + quoteExtraFields, + referenceNumber, + recipients, + handleSaveClick, + }); + + const formDatas = [ + { + title: b3Lang('quoteDraft.contactInfo.contact'), + infos: contactInfo, + }, + { + title: b3Lang('quoteDraft.quoteInfo.title'), + infos: quoteInfo, + style: { + mt: '20px', + }, + }, + ]; return ( - - {b3Lang('quoteDraft.contactInfo.contact')} - - - + {formDatas.map((data) => ( + + + {data.title} + + + + + ))} ); } diff --git a/apps/storefront/src/pages/quote/components/QuoteDetailHeader.tsx b/apps/storefront/src/pages/quote/components/QuoteDetailHeader.tsx index 2b745087c..1b57581e8 100644 --- a/apps/storefront/src/pages/quote/components/QuoteDetailHeader.tsx +++ b/apps/storefront/src/pages/quote/components/QuoteDetailHeader.tsx @@ -26,7 +26,6 @@ interface QuoteDetailHeaderProps { exportPdf: () => void; printQuote: () => Promise; role: string | number; - quoteTitle: string; salesRepInfo: { [key: string]: string }; } @@ -42,7 +41,6 @@ function QuoteDetailHeader(props: QuoteDetailHeaderProps) { exportPdf, printQuote, role, - quoteTitle, salesRepInfo, } = props; @@ -145,20 +143,6 @@ function QuoteDetailHeader(props: QuoteDetailHeaderProps) { - {quoteTitle && ( - - - {b3Lang('quoteDetail.header.title')} - - {quoteTitle} - - )} {(salesRepInfo?.salesRepName || salesRepInfo?.salesRepEmail) && ( void; @@ -20,7 +28,7 @@ interface InfoProps { type Keys = string | string[]; -const contactInfoKeys: string[] = ['name', 'email', 'companyName', 'phoneNumber', 'quoteTitle']; +const contactInfoKeys: string[] = ['name', 'email', 'phoneNumber']; const addressVerifyKeys: string[] = [ 'label', @@ -70,7 +78,7 @@ function QuoteInfoItem({ flag, title, info, status }: QuoteInfoItemProps) { return ( @@ -135,6 +143,7 @@ function QuoteInfoItem({ flag, title, info, status }: QuoteInfoItemProps) { } function QuoteInfo({ + quoteAndExtraFieldsInfo, contactInfo, shippingAddress, billingAddress, @@ -161,12 +170,19 @@ function QuoteInfo({ flexDirection: isMobile ? 'column' : 'row', }} > - + + + + + + {b3Lang('quoteDraft.quoteInfo.title')} + + + {(quoteTitle || status === 'Draft') && ( + {`${b3Lang('quoteDraft.quoteInfo.titleText')} ${quoteTitle}`} + )} + {(referenceNumber || status === 'Draft') && ( + {`${b3Lang( + 'quoteDraft.quoteInfo.referenceText', + )} ${referenceNumber}`} + )} + + {status === 'Draft' && !recipients.length ? ( + {b3Lang('quoteDraft.quoteInfo.ccEmailText')} + ) : ( + recipients.map((ccEmail) => ( + {`${b3Lang('quoteDraft.quoteInfo.ccEmailText')} ${ccEmail}`} + )) + )} + + {extraFields.map( + (field) => + (field.value || status === 'Draft') && ( + {`${field.fieldName}: ${field.value}`} + ), + )} + + + ); +} + +export default QuoteInfoAndExtrafieldsItem; diff --git a/apps/storefront/src/pages/quote/utils/getQuoteExtraFields.ts b/apps/storefront/src/pages/quote/utils/getQuoteExtraFields.ts new file mode 100644 index 000000000..092c3aa7e --- /dev/null +++ b/apps/storefront/src/pages/quote/utils/getQuoteExtraFields.ts @@ -0,0 +1,70 @@ +import { getQuoteExtraFieldsConfig } from '@/shared/service/b2b'; +import { QuoteExtraFieldsOrigin, QuoteFormattedItemsProps } from '@/types/quotes'; +import b2bLogger from '@/utils/b3Logger'; + +const handleConversionExtraItemFormat = (quoteExtraFields: QuoteExtraFieldsOrigin[]) => { + const formattedQuoteExtraFields = quoteExtraFields.map((item) => { + const { listOfValue } = item; + + const currentItems: QuoteFormattedItemsProps = { + isExtraFields: true, + name: item.fieldName || '', + label: item.labelName || '', + required: item.isRequired, + default: item.defaultValue || '', + fieldType: item.fieldCategory || '', + xs: 6, + variant: 'filled', + size: 'small', + id: item.id, + }; + + switch (item.fieldCategory) { + case 'dropdown': + if (listOfValue) { + const options = listOfValue?.map((option) => ({ + label: option, + value: option, + })); + + if (options.length > 0) { + currentItems.options = options; + } + } + + break; + case 'number': + currentItems.max = item.maximumValue || ''; + break; + case 'multiline': + currentItems.rows = item.numberOfRows || ''; + break; + default: + currentItems.maxLength = item.maximumLength || ''; + break; + } + + return currentItems; + }); + + return formattedQuoteExtraFields; +}; + +const getB2BQuoteExtraFields = async () => { + let quoteExtraFieldsList: QuoteFormattedItemsProps[] = []; + try { + const { quoteExtraFieldsConfig } = await getQuoteExtraFieldsConfig(); + + const visibleFields = quoteExtraFieldsConfig.filter((item) => item.visibleToEnduser); + + const formattedQuoteExtraFields = handleConversionExtraItemFormat(visibleFields); + + quoteExtraFieldsList = formattedQuoteExtraFields; + } catch (err) { + b2bLogger.error(err); + } + + return quoteExtraFieldsList; +}; + +export default getB2BQuoteExtraFields; diff --git a/apps/storefront/src/shared/service/b2b/api/global.ts b/apps/storefront/src/shared/service/b2b/api/global.ts index 375fb6add..cc56ec2fc 100644 --- a/apps/storefront/src/shared/service/b2b/api/global.ts +++ b/apps/storefront/src/shared/service/b2b/api/global.ts @@ -24,3 +24,9 @@ export const setChannelStoreType = () => storefrontType: 1, storeHash, }); + +export const validateQuoteExtraFields = (data: CustomFieldItems) => + B3Request.post('/api/v2/extra-fields/quote/validate', RequestType.B2BRest, { + ...data, + storeHash, + }); diff --git a/apps/storefront/src/shared/service/b2b/graphql/quote.ts b/apps/storefront/src/shared/service/b2b/graphql/quote.ts index b5995c5af..d27e089a3 100644 --- a/apps/storefront/src/shared/service/b2b/graphql/quote.ts +++ b/apps/storefront/src/shared/service/b2b/graphql/quote.ts @@ -1,3 +1,4 @@ +import { QuoteExtraFieldsType } from '@/types/quotes'; import { channelId, convertArrayToGraphql, convertObjectToGraphql, storeHash } from '@/utils'; import B3Request from '../../request/b3Fetch'; @@ -129,6 +130,9 @@ const quoteCreate = (data: CustomFieldItems) => `mutation{ productList: ${convertArrayToGraphql(data.productList || [])}, fileList: ${convertArrayToGraphql(data.fileList || [])}, quoteTitle: "${data.quoteTitle}" + ${data?.extraFields ? `extraFields: ${convertArrayToGraphql(data?.extraFields || [])}` : ''} + ${data?.referenceNumber ? `referenceNumber: "${data?.referenceNumber}"` : ''} + ${data?.recipients ? `recipients: ${convertArrayToGraphql(data?.recipients || [])}` : ''} }) { quote{ id, @@ -350,6 +354,25 @@ query getStorefrontProductSettings($storeHash: String!, $channelId: Int) { } `; +const getQuoteExtraFields = `query getQuoteExtraFields($storeHash: String, $channelId: Int) { + quoteExtraFieldsConfig(storeHash: $storeHash, channelId: $channelId) { + fieldName, + fieldType, + isRequired, + defaultValue, + maximumLength, + numberOfRows, + maximumValue, + listOfValue, + visibleToEnduser, + labelName, + id, + isUnique, + valueConfigs, + fieldCategory, + } +}`; + export const getBCCustomerAddresses = () => B3Request.graphqlB2B({ query: getCustomerAddresses(), @@ -450,3 +473,9 @@ export const getBCStorefrontProductSettings = () => query: getStorefrontProductSettings, variables: { storeHash, channelId }, }); + +export const getQuoteExtraFieldsConfig = (): Promise => + B3Request.graphqlB2B({ + query: getQuoteExtraFields, + variables: { storeHash, channelId }, + }); diff --git a/apps/storefront/src/shared/service/b2b/index.ts b/apps/storefront/src/shared/service/b2b/index.ts index 23190ecd2..e7c0a084d 100644 --- a/apps/storefront/src/shared/service/b2b/index.ts +++ b/apps/storefront/src/shared/service/b2b/index.ts @@ -1,5 +1,5 @@ import validateAddressExtraFields from './api/address'; -import { setChannelStoreType, uploadB2BFile } from './api/global'; +import { setChannelStoreType, uploadB2BFile, validateQuoteExtraFields } from './api/global'; import { validateBCCompanyExtraFields, validateBCCompanyUserExtraFields } from './api/register'; import { createB2BAddress, @@ -64,6 +64,7 @@ import { getBCQuotesList, getBCStorefrontProductSettings, getQuoteCreatedByUsers, + getQuoteExtraFieldsConfig, quoteDetailAttachFileCreate, quoteDetailAttachFileDelete, updateB2BQuote, @@ -210,6 +211,7 @@ export { getOrdersCreatedByUser, getOrderStatusType, getQuoteCreatedByUsers, + getQuoteExtraFieldsConfig, getShoppingListsCreatedByUser, getStorefrontConfig, getStorefrontConfigs, @@ -242,6 +244,7 @@ export { validateAddressExtraFields, validateBCCompanyExtraFields, validateBCCompanyUserExtraFields, + validateQuoteExtraFields, }; export { default as getTranslation } from './api/translation'; diff --git a/apps/storefront/src/store/slices/quoteInfo.ts b/apps/storefront/src/store/slices/quoteInfo.ts index 5d9f1982e..0abe2fee5 100644 --- a/apps/storefront/src/store/slices/quoteInfo.ts +++ b/apps/storefront/src/store/slices/quoteInfo.ts @@ -72,6 +72,9 @@ const initialState: QuoteInfoState = { }, fileInfo: [], note: '', + referenceNumber: '', + extraFields: [], + recipients: [], }, quoteDetailToCheckoutUrl: '', }; diff --git a/apps/storefront/src/types/quotes.ts b/apps/storefront/src/types/quotes.ts index 98be51288..a55f2866c 100644 --- a/apps/storefront/src/types/quotes.ts +++ b/apps/storefront/src/types/quotes.ts @@ -1,3 +1,5 @@ +import { ExtraFieldsConfigType, Maybe, QuoteExtraFieldsConfigType } from '@/types/gql/graphql'; + import { Product } from './products'; export interface ContactInfo { @@ -88,6 +90,12 @@ export interface QuoteItem { }; } +export interface QuoteExtraFields { + id?: number; + fieldName: Maybe | undefined; + value: string | number; +} + export interface QuoteInfo { userId?: number; contactInfo: ContactInfo; @@ -95,4 +103,66 @@ export interface QuoteInfo { billingAddress: BillingAddress; fileInfo?: FileInfo[]; note?: string; + referenceNumber?: string; + extraFields?: QuoteExtraFields[]; + recipients?: string[]; +} + +export interface QuoteInfoAndExtrafieldsItemProps { + info: { + quoteTitle: string; + referenceNumber: string; + }; + extraFields: QuoteExtraFields[] | undefined; + recipients: string[]; +} + +export interface B2bExtraFieldsProps { + defaultValue: string; + fieldName: string; + fieldType: 0 | 1 | 2 | 3; + isRequired: boolean; + labelName: string; + listOfValue: null | Array; + maximumLength: string | number | null; + maximumValue: string | number | null; + numberOfRows: string | number | null; + visibleToEnduser: boolean; + id: number | string; +} + +export interface FieldsOptionProps { + label: Maybe; + value: Maybe; +} + +export interface QuoteFormattedItemsProps { + isExtraFields: boolean; + name: Maybe | undefined; + label: Maybe | undefined; + required?: boolean | null; + default: string | number; + fieldType: string; + xs: number; + variant: string; + size: string; + options?: FieldsOptionProps[]; + max?: string | number | null; + rows?: string | number | null; + maxLength?: string | number | null; + id: number | string; +} + +export type QuoteExtraFieldsOrigin = Omit & + QuoteExtraFieldsConfigType & { + fieldCategory: string; + }; + +export interface QuoteExtraFieldsType { + quoteExtraFieldsConfig: QuoteExtraFieldsOrigin[]; +} + +export interface QuoteExtraFieldsData { + fieldName: string | undefined; + fieldValue: string | number; } diff --git a/packages/lang/locales/en.json b/packages/lang/locales/en.json index c32f74d1e..1f9cb3145 100644 --- a/packages/lang/locales/en.json +++ b/packages/lang/locales/en.json @@ -541,7 +541,13 @@ "quoteDraft.contactInfo.quoteTitle": "Quote Title", "quoteDraft.quoteInfo.quoteTitle": "Quote title:", "quoteDraft.contactInfo.emailExists": "Email already exists", - "quoteDraft.contactInfo.contact": "Contact", + "quoteDraft.contactInfo.contact": "Buyer info", + "quoteDraft.quoteInfo.title": "Quote info", + "quoteDraft.contactInfo.referenceNumber": "Reference number", + "quoteDraft.quoteInfo.titleText": "Title:", + "quoteDraft.quoteInfo.referenceText": "Reference:", + "quoteDraft.quoteInfo.ccEmailText": "CC:", + "quoteDraft.contactInfo.ccEmail": "CC email", "quoteDraft.quoteAddress.chooseFromSaved": "Choose from saved", "quoteDraft.chooseAddress.chooseFromSaved": "Choose from saved", "quoteDraft.chooseAddress.searchAddress": "Search address",