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",