diff --git a/apps/storefront/src/components/B3ProductList.tsx b/apps/storefront/src/components/B3ProductList.tsx index 76c22dea..b377780d 100644 --- a/apps/storefront/src/components/B3ProductList.tsx +++ b/apps/storefront/src/components/B3ProductList.tsx @@ -20,6 +20,10 @@ import { PRODUCT_DEFAULT_IMAGE } from '@/constants' import { useMobile } from '@/hooks' import { store } from '@/store' import { currencyFormat, ordersCurrencyFormat } from '@/utils' +import { + getDisplayPrice, + judgmentBuyerProduct, +} from '@/utils/b3Product/b3Product' import { MoneyFormat, ProductItem } from '../types' @@ -135,6 +139,7 @@ interface ProductProps { totalText?: string canToProduct?: boolean textAlign?: string + type?: string } export default function B3ProductList(props: ProductProps) { @@ -152,6 +157,7 @@ export default function B3ProductList(props: ProductProps) { canToProduct = false, textAlign = 'left', money, + type, } = props const [list, setList] = useState([]) @@ -229,6 +235,32 @@ export default function B3ProductList(props: ProductProps) { const itemStyle = isMobile ? mobileItemStyle : defaultItemStyle + const showTypePrice = ( + newMoney: string | number, + product: CustomFieldItems + ): string | number => { + if (type === 'quote') { + return getDisplayPrice({ + price: newMoney, + productInfo: product, + isProduct: true, + }) + } + if (type === 'shoppingList') { + const { isPriceHidden } = product + const isBuyerProduct = judgmentBuyerProduct({ + price: newMoney, + productInfo: product, + isProduct: true, + }) + return isPriceHidden && !isBuyerProduct ? '' : newMoney + } + if (type === 'quickOrder') { + return newMoney + } + return newMoney + } + return products.length > 0 ? ( {!isMobile && ( @@ -277,6 +309,7 @@ export default function B3ProductList(props: ProductProps) { )} {products.map((product) => { + console.log(product, 'product') const { variants = [] } = product const currentVariant = variants[0] let productPrice = +product.base_price @@ -300,6 +333,22 @@ export default function B3ProductList(props: ProductProps) { productPrice ) + const getPrice = () => { + const newMoney = money + ? `${ordersCurrencyFormat(money, productPrice)}` + : `${currencyFormat(productPrice)}` + + return showTypePrice(newMoney, product) + } + + const getTotalPrice = () => { + const newMoney = money + ? `${ordersCurrencyFormat(money, totalPrice)}` + : `${currencyFormat(totalPrice)}` + + return showTypePrice(newMoney, product) + } + return ( {showCheckbox && ( @@ -358,9 +407,8 @@ export default function B3ProductList(props: ProductProps) { } > {isMobile && Price:} - {money - ? `${ordersCurrencyFormat(money, productPrice)}` - : `${currencyFormat(productPrice)}`} + + {getPrice()} (props: ProductProps) { } > {isMobile && {totalText}:} - {money - ? `${ordersCurrencyFormat(money, totalPrice)}` - : `${currencyFormat(totalPrice)}`} + {getTotalPrice()} {renderAction && ( diff --git a/apps/storefront/src/hooks/dom/useCartToQuote.ts b/apps/storefront/src/hooks/dom/useCartToQuote.ts index de93573e..2b861187 100644 --- a/apps/storefront/src/hooks/dom/useCartToQuote.ts +++ b/apps/storefront/src/hooks/dom/useCartToQuote.ts @@ -163,7 +163,7 @@ const useCartToQuote = ({ let cartQuoteBtnDom: CustomFieldItems | null = null if (!addToQuoteAll.length && !CustomAddToQuoteAll.length) return - if (!cartQuoteEnabled) { + if (!cartQuoteEnabled || window?.location?.pathname?.includes('checkout')) { document.querySelector('.b2b-cart-to-quote')?.remove() return } diff --git a/apps/storefront/src/hooks/dom/useMyQuote.ts b/apps/storefront/src/hooks/dom/useMyQuote.ts index c250feeb..8de89985 100644 --- a/apps/storefront/src/hooks/dom/useMyQuote.ts +++ b/apps/storefront/src/hooks/dom/useMyQuote.ts @@ -25,6 +25,7 @@ import { CustomStyleContext } from '@/shared/customStyleButtton' import { B3LStorage, setCartPermissions } from '@/utils' import useDomVariation from './useDomVariation' +import usePurchasableQuote from './usePurchasableQuote' import { addProductFromProductPageToQuote, removeElement } from './utils' type DispatchProps = Dispatch> @@ -56,9 +57,11 @@ const useMyQuote = ({ }, [B3UserId]) const cache = useRef({}) const { - state: { addQuoteBtn }, + state: { addQuoteBtn, quoteOnNonPurchasableProductPageBtn }, } = useContext(CustomStyleContext) + // const [isPurchasable, setPurchasable] = useState(true) + // quote method and goto draft const { addToQuote, addLoadding } = addProductFromProductPageToQuote(setOpenPage) @@ -80,6 +83,8 @@ const useMyQuote = ({ const [openQuickView] = useDomVariation(globalB3['dom.setToQuote'], cd) + const [isBuyPurchasable] = usePurchasableQuote(openQuickView) + const { color = '', text = '', @@ -88,13 +93,26 @@ const useMyQuote = ({ locationSelector = '', enabled = false, } = addQuoteBtn + + const { + color: noPuchasableQuoteColor = '', + text: noPuchasableQuoteText = '', + customCss: noPuchasableQuoteCustomCss = '', + classSelector: noPuchasableQuoteClassSelector = '', + locationSelector: noPuchasableQuoteLocationSelector = '', + enabled: noPuchasableQuoteEnabled = false, + } = quoteOnNonPurchasableProductPageBtn + + const newText = isBuyPurchasable ? text : noPuchasableQuoteText const myQuoteBtnLabel = useGetButtonText( TRANSLATION_ADD_TO_QUOTE_VARIABLE, - text, + newText, ADD_TO_QUOTE_DEFAULT_VALUE ) - const cssInfo = splitCustomCssValue(customCss) + const cssInfo = splitCustomCssValue( + isBuyPurchasable ? customCss : noPuchasableQuoteCustomCss + ) const { cssValue, mediaBlocks, @@ -102,59 +120,111 @@ const useMyQuote = ({ cssValue: string mediaBlocks: string[] } = cssInfo - const customTextColor = getStyles(cssValue).color || getContrastColor(color) + const customTextColor = + getStyles(cssValue).color || + getContrastColor(isBuyPurchasable ? color : noPuchasableQuoteColor) - useEffect(() => { - const addToQuoteAll = document.querySelectorAll(globalB3['dom.setToQuote']) - const CustomAddToQuoteAll = locationSelector - ? document.querySelectorAll(locationSelector) - : [] + const clearQuoteDom = () => { + const myQuoteBtn = document.querySelectorAll('.b2b-add-to-quote') + myQuoteBtn.forEach((item: CustomFieldItems) => { + removeElement(item) + }) + } - if (!addToQuoteAll.length && !CustomAddToQuoteAll.length) return + const clearNoPuchasableQuoteDom = () => { + const myNoPuchasableQuoteBtn = document.querySelectorAll( + '.b2b-add-to-no-puchasable-quote' + ) + myNoPuchasableQuoteBtn.forEach((item: CustomFieldItems) => { + removeElement(item) + }) + } - if (!productQuoteEnabled) { - document.querySelector('.b2b-add-to-quote')?.remove() - return - } + const addBtnStyle = () => { + const myQuoteBtn = document.querySelectorAll('.b2b-add-to-quote') + myQuoteBtn.forEach((myQuote: CustomFieldItems) => { + myQuote.innerHTML = myQuoteBtnLabel + myQuote.setAttribute( + 'style', + isBuyPurchasable ? customCss : noPuchasableQuoteCustomCss + ) + myQuote.style.backgroundColor = isBuyPurchasable + ? color + : noPuchasableQuoteColor + myQuote.style.color = customTextColor + myQuote.setAttribute( + 'class', + `b2b-add-to-quote ${ + isBuyPurchasable ? classSelector : noPuchasableQuoteClassSelector + }` + ) + + setMediaStyle( + mediaBlocks, + `b2b-add-to-quote ${ + isBuyPurchasable ? classSelector : noPuchasableQuoteClassSelector + }` + ) + }) + } + + const purchasableQuote = ( + CustomAddToQuoteAll: NodeListOf | never[], + addToQuoteAll: NodeListOf, + isBuyer: boolean + ) => { + const quoteNode = isBuyer + ? '.b2b-add-to-quote' + : '.b2b-add-to-no-puchasable-quote' + const quoteNodeStyle = isBuyer + ? 'b2b-add-to-quote' + : 'b2b-add-to-no-puchasable-quote' - if (document.querySelectorAll('.b2b-add-to-quote')?.length) { + if (document.querySelectorAll(quoteNode)?.length) { const cacheQuoteDom = cache.current + const isAddStyle = Object.keys(cacheQuoteDom).every( (key: string) => (cacheQuoteDom as CustomFieldItems)[key] === (addQuoteBtn as CustomFieldItems)[key] ) if (!isAddStyle) { - const myQuoteBtn = document.querySelectorAll('.b2b-add-to-quote') - myQuoteBtn.forEach((myQuote: CustomFieldItems) => { - myQuote.innerHTML = myQuoteBtnLabel - myQuote.setAttribute('style', customCss) - myQuote.style.backgroundColor = color - myQuote.style.color = customTextColor - myQuote.setAttribute('class', `b2b-add-to-quote ${classSelector}`) - - setMediaStyle(mediaBlocks, `b2b-add-to-quote ${classSelector}`) - }) + addBtnStyle() cache.current = cloneDeep(addQuoteBtn) } } - if (enabled) { + if (isBuyPurchasable ? enabled : noPuchasableQuoteEnabled) { ;(CustomAddToQuoteAll.length ? CustomAddToQuoteAll : addToQuoteAll ).forEach((node: CustomFieldItems) => { - const children = node.parentNode.querySelectorAll('.b2b-add-to-quote') + const children = node.parentNode.querySelectorAll(quoteNode) if (!children.length) { let myQuote: CustomFieldItems | null = null myQuote = document.createElement('div') myQuote.innerHTML = myQuoteBtnLabel - myQuote.setAttribute('style', customCss) - myQuote.style.backgroundColor = color + myQuote.setAttribute( + 'style', + isBuyPurchasable ? customCss : noPuchasableQuoteCustomCss + ) + myQuote.style.backgroundColor = isBuyPurchasable + ? color + : noPuchasableQuoteColor myQuote.style.color = customTextColor - myQuote.setAttribute('class', `b2b-add-to-quote ${classSelector}`) + myQuote.setAttribute( + 'class', + `${quoteNodeStyle} ${ + isBuyPurchasable ? classSelector : noPuchasableQuoteClassSelector + }` + ) - setMediaStyle(mediaBlocks, `b2b-add-to-quote ${classSelector}`) + setMediaStyle( + mediaBlocks, + `${quoteNodeStyle} ${ + isBuyPurchasable ? classSelector : noPuchasableQuoteClassSelector + }` + ) if (CustomAddToQuoteAll.length) { node.appendChild(myQuote) } else { @@ -167,10 +237,43 @@ const useMyQuote = ({ }) cache.current = cloneDeep(addQuoteBtn) } else { - const myQuoteBtn = document.querySelectorAll('.b2b-add-to-quote') - myQuoteBtn.forEach((item: CustomFieldItems) => { - removeElement(item) - }) + clearQuoteDom() + } + } + + useEffect(() => { + if (!productQuoteEnabled) { + clearQuoteDom() + clearNoPuchasableQuoteDom() + return + } + + if (!isBuyPurchasable) { + clearQuoteDom() + const noPuchasableQuoteAll = document.querySelectorAll( + globalB3['dom.setToNoPuchasable'] + ) + + const CustomAddToQuoteAll = noPuchasableQuoteLocationSelector + ? document.querySelectorAll(noPuchasableQuoteLocationSelector) + : [] + + if (!noPuchasableQuoteAll.length && !CustomAddToQuoteAll.length) return + + if (noPuchasableQuoteAll.length) { + purchasableQuote(CustomAddToQuoteAll, noPuchasableQuoteAll, false) + } + } else { + clearNoPuchasableQuoteDom() + const addToQuoteAll = document.querySelectorAll( + globalB3['dom.setToQuote'] + ) + const CustomAddToQuoteAll = locationSelector + ? document.querySelectorAll(locationSelector) + : [] + + if (!addToQuoteAll.length && !CustomAddToQuoteAll.length) return + purchasableQuote(CustomAddToQuoteAll, addToQuoteAll, true) } // eslint-disable-next-line @@ -180,7 +283,7 @@ const useMyQuote = ({ item.removeEventListener('click', quoteCallBack) }) } - }, [openQuickView, productQuoteEnabled, addQuoteBtn]) + }, [openQuickView, productQuoteEnabled, addQuoteBtn, isBuyPurchasable]) } export default useMyQuote diff --git a/apps/storefront/src/hooks/dom/usePurchasableQuote.ts b/apps/storefront/src/hooks/dom/usePurchasableQuote.ts new file mode 100644 index 00000000..cb33f23b --- /dev/null +++ b/apps/storefront/src/hooks/dom/usePurchasableQuote.ts @@ -0,0 +1,212 @@ +import { useEffect, useRef, useState } from 'react' + +import { getB2BProductPurchasable } from '@/shared/service/b2b/graphql/product' +import { store } from '@/store' + +interface MyMutationRecord extends MutationRecord { + target: HTMLElement +} + +interface ProductInfoProps { + availability: boolean + inventoryLevel: number + inventoryTracking: boolean + purchasingDisabled: boolean +} + +const usePurchasableQuote = (openQuickView: boolean) => { + const [isBuyPurchasable, setBuyPurchasable] = useState(true) + + const productInfoRef = useRef({ + availability: false, + inventoryLevel: 0, + inventoryTracking: false, + purchasingDisabled: false, + }) + + const { + global: { + blockPendingQuoteNonPurchasableOOS: { isEnableProduct }, + }, + } = store.getState() + + const isOOStockPurchaseQuantity = ( + qty: number, + productPurchasable: ProductInfoProps + ): boolean => { + const { inventoryLevel, inventoryTracking } = productPurchasable + + if (inventoryTracking && qty > inventoryLevel) return true + + return false + } + + const callback = async ( + newSkuValue: string, + isDetailOpen: boolean, + isInit?: boolean + ): Promise => { + const modal = document.getElementById('modal') as HTMLElement + + const dom = isDetailOpen ? document : modal + const productViewQty = + (dom.querySelector('[name="qty[]"]') as CustomFieldItems).value ?? 1 + + const productId = ( + dom.querySelector('input[name=product_id]') as CustomFieldItems + )?.value + + const { + productPurchasable: { + availability, + inventoryLevel, + inventoryTracking, + purchasingDisabled, + }, + } = await getB2BProductPurchasable({ + productId, + sku: newSkuValue || '', + isProduct: !!isInit, + }) + + const productPurchasable = { + availability: availability === 'available', + inventoryLevel, + inventoryTracking: + inventoryTracking === 'product' || inventoryTracking === 'variant', + purchasingDisabled, + } + if (productInfoRef?.current) { + productInfoRef.current = productPurchasable + } + + const isOOStock = isOOStockPurchaseQuantity( + +productViewQty, + productPurchasable + ) + if ( + purchasingDisabled === '1' || + isOOStock || + availability !== 'available' + ) { + setBuyPurchasable(false) + } else { + setBuyPurchasable(true) + } + } + + useEffect(() => { + const modal = document.getElementById('modal') as HTMLElement + + let productViewSku: Element | null = + document.querySelector('[data-product-sku]') || null + let qtyDom: HTMLInputElement | null = + document.querySelector('[name="qty[]"]') || null + let isDetailOpen = true + let dataQuantityChangeDom = + document.querySelector('[data-quantity-change]') || null + // information about multiple products exists + if (modal && modal.classList.contains('open')) { + productViewSku = modal.querySelector('[data-product-sku]') + qtyDom = modal.querySelector('[name="qty[]"]') + dataQuantityChangeDom = modal.querySelector('[data-quantity-change]') + isDetailOpen = false + } + + if (productViewSku && isEnableProduct) { + const sku = productViewSku.innerHTML.trim() + callback(sku, isDetailOpen, true) + } + + const observer = new MutationObserver((mutations: MutationRecord[]) => { + let sku = '' + mutations.forEach((mutation) => { + const myMutation: MyMutationRecord = mutation as MyMutationRecord + if ( + myMutation.type === 'childList' && + myMutation.target.hasAttribute('data-product-sku') + ) { + const newSkuValue = myMutation.target.innerHTML.trim() + sku = newSkuValue + } + }) + if (sku) callback(sku, isDetailOpen, false) + }) + + const config: MutationObserverInit = { childList: true, subtree: true } + + if (productViewSku && isEnableProduct) { + observer.observe(productViewSku, config) + } + + const judgmentBuyPurchasable = (newQuantity: number | string) => { + const isOOStock = isOOStockPurchaseQuantity( + +newQuantity, + productInfoRef.current + ) + + if (isOOStock) { + setBuyPurchasable(false) + } else { + setBuyPurchasable(true) + } + } + + const handleQuantityChange = () => { + const newQuantity = qtyDom ? qtyDom.value : 0 + + judgmentBuyPurchasable(newQuantity) + } + + if (qtyDom && isEnableProduct) { + qtyDom.addEventListener('input', handleQuantityChange) + } + + const handleBtnQuantityChange = (button: Element) => { + if (qtyDom) { + const action = button.getAttribute('data-action') + const val = qtyDom?.value || '1' + + const isNumber = (str: string) => /^\d+$/.test(str) + + if (!isNumber(val)) { + judgmentBuyPurchasable(action === 'dec' ? 0 : 1) + + return + } + + if (action === 'dec' && (val === '0' || val === '1')) { + judgmentBuyPurchasable(val) + return + } + + const newNum = action === 'dec' ? +val - 1 : +val + 1 + + judgmentBuyPurchasable(newNum) + } + } + + if (dataQuantityChangeDom && isEnableProduct) { + const buttons = dataQuantityChangeDom.querySelectorAll('button') + buttons.forEach((button) => { + button.addEventListener('click', () => { + handleBtnQuantityChange(button) + }) + return () => { + button.removeEventListener('click', () => { + handleBtnQuantityChange(button) + }) + } + }) + } + + return () => { + if (observer) observer.disconnect() + if (qtyDom) qtyDom.removeEventListener('input', handleQuantityChange) + } + }, [openQuickView, isEnableProduct]) + + return [isBuyPurchasable] +} + +export default usePurchasableQuote diff --git a/apps/storefront/src/pages/quickorder/components/QuickOrderFooter.tsx b/apps/storefront/src/pages/quickorder/components/QuickOrderFooter.tsx index 6b67acf6..9ef9d872 100644 --- a/apps/storefront/src/pages/quickorder/components/QuickOrderFooter.tsx +++ b/apps/storefront/src/pages/quickorder/components/QuickOrderFooter.tsx @@ -260,6 +260,10 @@ function QuickOrderFooter(props: QuickOrderFooterProps) { }), isClose: true, }) + } else if (res && res.errors) { + snackbar.error(res.errors[0].message, { + isClose: true, + }) } else { snackbar.error('Error has occurred', { isClose: true, diff --git a/apps/storefront/src/pages/quickorder/components/QuickOrderPad.tsx b/apps/storefront/src/pages/quickorder/components/QuickOrderPad.tsx index 74b6eaab..ecf337fa 100644 --- a/apps/storefront/src/pages/quickorder/components/QuickOrderPad.tsx +++ b/apps/storefront/src/pages/quickorder/components/QuickOrderPad.tsx @@ -13,7 +13,7 @@ import { import { B3Upload, CustomButton, successTip } from '@/components' import { useBlockPendingAccountViewPrice, useMobile } from '@/hooks' -import { globalStateSelector } from '@/store' +import { globalStateSelector, store } from '@/store' import { B3SStorage, b3TriggerCartNumber, snackbar } from '@/utils' import { callCart } from '@/utils/cartUtils' @@ -40,6 +40,12 @@ export default function QuickOrderPad(props: QuickOrderPadProps) { const { storeInfo } = useSelector(globalStateSelector) const storePlatform = storeInfo?.platform + const { + global: { + blockPendingQuoteNonPurchasableOOS: { isEnableProduct }, + }, + } = store.getState() + const getSnackbarMessage = (res: any) => { if (res && !res.errors) { snackbar.success('', { @@ -320,7 +326,7 @@ export default function QuickOrderPad(props: QuickOrderPadProps) { productSku = currentVariant.sku || sku } - if (purchasingDisabled) { + if (purchasingDisabled && !isEnableProduct) { snackbar.error( b3Lang('purchasedProducts.quickOrderPad.notPurchaseableSku', { notPurchaseSku: productSku, @@ -431,6 +437,7 @@ export default function QuickOrderPad(props: QuickOrderPadProps) { searchDialogTitle={b3Lang( 'purchasedProducts.quickOrderPad.quickOrderPad' )} + type="quickOrder" addButtonText={b3Lang('purchasedProducts.quickOrderPad.addToCart')} isB2BUser={isB2BUser} /> diff --git a/apps/storefront/src/pages/quote/QuoteDetail.tsx b/apps/storefront/src/pages/quote/QuoteDetail.tsx index 42aae309..8ffd1a28 100644 --- a/apps/storefront/src/pages/quote/QuoteDetail.tsx +++ b/apps/storefront/src/pages/quote/QuoteDetail.tsx @@ -1,4 +1,4 @@ -import { useContext, useEffect, useState } from 'react' +import { useCallback, useContext, useEffect, useState } from 'react' import { useLocation, useNavigate, useParams } from 'react-router-dom' import { useB3Lang } from '@b3/lang' import { Box, Button, Grid } from '@mui/material' @@ -16,7 +16,12 @@ import { searchBcProducts, } from '@/shared/service/b2b' import { store, TaxZoneRates, TaxZoneRatesProps } from '@/store' -import { getDefaultCurrencyInfo, getSearchVal, snackbar } from '@/utils' +import { + getDefaultCurrencyInfo, + getSearchVal, + getVariantInfoDisplayPrice, + snackbar, +} from '@/utils' import { conversionProductsList } from '@/utils/b3Product/shared/config' import Message from './components/Message' @@ -51,6 +56,9 @@ function QuoteDetail() { const [quoteDetail, setQuoteDetail] = useState({}) const [productList, setProductList] = useState([]) const [fileList, setFileList] = useState([]) + const [isHandleApprove, setHandleApprove] = useState(false) + + const [isHideQuoteCheckout, setIsHideQuoteCheckout] = useState(false) const [quoteSummary, setQuoteSummary] = useState({ originalSubtotal: 0, @@ -65,12 +73,61 @@ function QuoteDetail() { const [quoteDetailTax, setQuoteDetailTax] = useState(0) + const [noBuyerProductName, setNoBuyerProductName] = useState('') + const location = useLocation() const { - global: { taxZoneRates, enteredInclusive: enteredInclusiveTax }, + global: { + taxZoneRates, + enteredInclusive: enteredInclusiveTax, + blockPendingQuoteNonPurchasableOOS: { isEnableProduct }, + }, } = store.getState() + useEffect(() => { + let productName = '' + + const isHideCheckout = productList.some((item: CustomFieldItems) => { + if (!getVariantInfoDisplayPrice(item.basePrice, item)) { + if (isEnableProduct && !item?.purchaseHandled) { + productName += `${item.productName}${productName ? ',' : ''}` + return true + } + + if (!isEnableProduct) { + productName += `${item.productName}${productName ? ',' : ''}` + return true + } + } + + return false + }) + + if (isEnableProduct && isHandleApprove && isHideCheckout) { + snackbar.error( + b3Lang('quoteDetail.message.insufficientStock', { + ProductName: productName, + }) + ) + } + + setIsHideQuoteCheckout(isHideCheckout) + + setNoBuyerProductName(productName) + }, [isEnableProduct, isHandleApprove, productList]) + + const proceedingCheckoutFn = useCallback(() => { + if (isHideQuoteCheckout) { + snackbar.error( + b3Lang('quoteDetail.message.insufficientStock', { + ProductName: noBuyerProductName, + }) + ) + } + return isHideQuoteCheckout + }, [isHideQuoteCheckout, noBuyerProductName]) + const classRates: TaxZoneRates[] = [] if (taxZoneRates.length) { const defaultTaxZone: TaxZoneRatesProps = taxZoneRates.find( @@ -194,7 +251,14 @@ function QuoteDetail() { setQuoteDetailTax(taxPrice) } - const { backendAttachFiles = [], storefrontAttachFiles = [] } = quote + const { + backendAttachFiles = [], + storefrontAttachFiles = [], + salesRep, + salesRepEmail, + } = quote + + setHandleApprove(!!salesRep || !!salesRepEmail) const newFileList: CustomFieldItems[] = [] storefrontAttachFiles.forEach((file: CustomFieldItems) => { @@ -373,6 +437,17 @@ function QuoteDetail() { } }, []) + const isEnableProductShowCheckout = () => { + if (isEnableProduct) { + if (isHandleApprove && isHideQuoteCheckout) return true + if (!isHideQuoteCheckout) return true + + return false + } + + return true + } + return ( @@ -473,6 +549,7 @@ function QuoteDetail() { }} > )} diff --git a/apps/storefront/src/pages/quote/QuoteDraft.tsx b/apps/storefront/src/pages/quote/QuoteDraft.tsx index 6b0cb027..fb7b7511 100644 --- a/apps/storefront/src/pages/quote/QuoteDraft.tsx +++ b/apps/storefront/src/pages/quote/QuoteDraft.tsx @@ -7,7 +7,7 @@ import { useState, } from 'react' import { useNavigate } from 'react-router-dom' -import { CallbackKey,useCallbacks } from '@b3/hooks' +import { CallbackKey, useCallbacks } from '@b3/hooks' import { useB3Lang } from '@b3/lang' import { ArrowBackIosNew } from '@mui/icons-material' import { @@ -89,6 +89,7 @@ interface InfoProps { interface QuoteTableRef extends HTMLInputElement { refreshList: () => void + getList: () => CustomFieldItems[] } interface QuoteSummaryRef extends HTMLInputElement { diff --git a/apps/storefront/src/pages/quote/components/AddToQuote.tsx b/apps/storefront/src/pages/quote/components/AddToQuote.tsx index 9c6298df..f4d5d96c 100644 --- a/apps/storefront/src/pages/quote/components/AddToQuote.tsx +++ b/apps/storefront/src/pages/quote/components/AddToQuote.tsx @@ -291,6 +291,7 @@ export default function AddToQuote(props: AddToListProps) { boolean } function QuoteDetailFooter(props: QuoteDetailFooterProps) { - const { quoteId, role, isAgenting, status } = props + const { quoteId, role, isAgenting, status, proceedingCheckoutFn } = props const globalState = useSelector(globalStateSelector) const [isMobile] = useMobile() const b3Lang = useB3Lang() @@ -38,6 +39,9 @@ function QuoteDetailFooter(props: QuoteDetailFooterProps) { const handleQuoteCheckout = async () => { try { + const isHideQuoteCheckout = proceedingCheckoutFn() + if (isHideQuoteCheckout) return + const fn = +role === 99 ? bcQuoteCheckout : b2bQuoteCheckout const date = getSearchVal(location.search, 'date') diff --git a/apps/storefront/src/pages/quote/components/QuoteDetailSummary.tsx b/apps/storefront/src/pages/quote/components/QuoteDetailSummary.tsx index bf207965..1de5144f 100644 --- a/apps/storefront/src/pages/quote/components/QuoteDetailSummary.tsx +++ b/apps/storefront/src/pages/quote/components/QuoteDetailSummary.tsx @@ -17,6 +17,7 @@ interface QuoteDetailSummaryProps { quoteDetailTax: number status: string quoteDetail: CustomFieldItems + isHideQuoteCheckout: boolean } export default function QuoteDetailSummary(props: QuoteDetailSummaryProps) { @@ -26,6 +27,7 @@ export default function QuoteDetailSummary(props: QuoteDetailSummaryProps) { quoteDetailTax = 0, status, quoteDetail, + isHideQuoteCheckout, } = props const { @@ -89,6 +91,12 @@ export default function QuoteDetailSummary(props: QuoteDetailSummaryProps) { const shippingAndTax = getShippingAndTax() + const showPrice = (price: string | number): string | number => { + if (isHideQuoteCheckout) return b3Lang('quoteDraft.quoteSummary.tbd') + + return price + } + return ( @@ -113,7 +121,9 @@ export default function QuoteDetailSummary(props: QuoteDetailSummaryProps) { {b3Lang('quoteDetail.summary.originalSubtotal')} - {priceFormat(getCurrentPrice(subtotalPrice, quoteDetailTax))} + {showPrice( + priceFormat(getCurrentPrice(subtotalPrice, quoteDetailTax)) + )} @@ -158,7 +168,9 @@ export default function QuoteDetailSummary(props: QuoteDetailSummaryProps) { color: '#212121', }} > - {priceFormat(getCurrentPrice(quotedSubtotal, quoteDetailTax))} + {showPrice( + priceFormat(getCurrentPrice(quotedSubtotal, quoteDetailTax)) + )} @@ -179,7 +191,9 @@ export default function QuoteDetailSummary(props: QuoteDetailSummaryProps) { > {shippingAndTax.shippingText} - {shippingAndTax.shippingVal} + + {showPrice(shippingAndTax.shippingVal)} + {shippingAndTax.taxText} - {shippingAndTax.taxVal} + {showPrice(shippingAndTax.taxVal)} )} @@ -215,7 +229,7 @@ export default function QuoteDetailSummary(props: QuoteDetailSummaryProps) { color: '#212121', }} > - {priceFormat(+totalAmount)} + {showPrice(priceFormat(+totalAmount))} diff --git a/apps/storefront/src/pages/quote/components/QuoteDetailTable.tsx b/apps/storefront/src/pages/quote/components/QuoteDetailTable.tsx index b0c13b93..897bccbc 100644 --- a/apps/storefront/src/pages/quote/components/QuoteDetailTable.tsx +++ b/apps/storefront/src/pages/quote/components/QuoteDetailTable.tsx @@ -7,7 +7,7 @@ import { TableColumnItem } from '@/components/table/B3Table' import { PRODUCT_DEFAULT_IMAGE } from '@/constants' import { store } from '@/store' import { currencyFormat } from '@/utils' -import { getBCPrice } from '@/utils/b3Product/b3Product' +import { getBCPrice, getDisplayPrice } from '@/utils/b3Product/b3Product' import QuoteDetailTableCard from './QuoteDetailTableCard' @@ -47,6 +47,7 @@ interface ShoppingDetailTableProps { edges: any[] totalCount: number }> + isHandleApprove: boolean getTaxRate: (taxClassId: number, variants: any) => number } @@ -102,10 +103,13 @@ const StyledImage = styled('img')(() => ({ function QuoteDetailTable(props: ShoppingDetailTableProps, ref: Ref) { const b3Lang = useB3Lang() - const { total, getQuoteTableDetails, getTaxRate } = props + const { total, getQuoteTableDetails, getTaxRate, isHandleApprove } = props const { - global: { enteredInclusive: enteredInclusiveTax }, + global: { + enteredInclusive: enteredInclusiveTax, + blockPendingQuoteNonPurchasableOOS: { isEnableProduct }, + }, } = store.getState() const paginationTableRef = useRef(null) @@ -124,6 +128,17 @@ function QuoteDetailTable(props: ShoppingDetailTableProps, ref: Ref) { }, })) + const showPrice = (price: string, row: CustomFieldItems): string | number => { + if (isEnableProduct) { + if (isHandleApprove) return price + return getDisplayPrice({ + price, + productInfo: row, + showText: b3Lang('quoteDraft.quoteSummary.tbd'), + }) + } + return price + } const columnItems: TableColumnItem[] = [ { key: 'Product', @@ -236,7 +251,7 @@ function QuoteDetailTable(props: ShoppingDetailTableProps, ref: Ref) { textDecoration: 'line-through', }} > - {`${currencyFormat(price)}`} + {showPrice(`${currencyFormat(price)}`, row)} )} @@ -246,7 +261,7 @@ function QuoteDetailTable(props: ShoppingDetailTableProps, ref: Ref) { color: isDiscount ? '#2E7D32' : '#212121', }} > - {`${currencyFormat(discountPrice)}`} + {showPrice(`${currencyFormat(discountPrice)}`, row)} ) @@ -308,7 +323,7 @@ function QuoteDetailTable(props: ShoppingDetailTableProps, ref: Ref) { textDecoration: 'line-through', }} > - {`${currencyFormat(total)}`} + {showPrice(`${currencyFormat(total)}`, row)} )} ) { color: isDiscount ? '#2E7D32' : '#212121', }} > - {`${currencyFormat(totalWithDiscount)}`} + {showPrice(`${currencyFormat(totalWithDiscount)}`, row)} ) @@ -364,6 +379,7 @@ function QuoteDetailTable(props: ShoppingDetailTableProps, ref: Ref) { diff --git a/apps/storefront/src/pages/quote/components/QuoteDetailTableCard.tsx b/apps/storefront/src/pages/quote/components/QuoteDetailTableCard.tsx index 14e86d68..a46363c0 100644 --- a/apps/storefront/src/pages/quote/components/QuoteDetailTableCard.tsx +++ b/apps/storefront/src/pages/quote/components/QuoteDetailTableCard.tsx @@ -11,6 +11,7 @@ interface QuoteTableCardProps { len: number getTaxRate: (taxClassId: number, variants: any) => number itemIndex?: number + showPrice: (price: string, row: CustomFieldItems) => string | number } const StyledImage = styled('img')(() => ({ @@ -20,7 +21,7 @@ const StyledImage = styled('img')(() => ({ })) function QuoteDetailTableCard(props: QuoteTableCardProps) { - const { item: quoteTableItem, len, itemIndex, getTaxRate } = props + const { item: quoteTableItem, len, itemIndex, getTaxRate, showPrice } = props const b3Lang = useB3Lang() const { @@ -143,7 +144,7 @@ function QuoteDetailTableCard(props: QuoteTableCardProps) { textDecoration: 'line-through', }} > - {`${currencyFormat(price)}`} + {`${showPrice(currencyFormat(price), quoteTableItem)}`} )} - {`${currencyFormat(+offeredPrice)}`} + {`${showPrice(currencyFormat(offeredPrice), quoteTableItem)}`} @@ -178,7 +179,7 @@ function QuoteDetailTableCard(props: QuoteTableCardProps) { textDecoration: 'line-through', }} > - {`${currencyFormat(total)}`} + {`${showPrice(currencyFormat(total), quoteTableItem)}`} )} - {`${currencyFormat(totalWithDiscount)}`} + {`${showPrice( + currencyFormat(totalWithDiscount), + quoteTableItem + )}`} diff --git a/apps/storefront/src/pages/quote/components/QuoteSummary.tsx b/apps/storefront/src/pages/quote/components/QuoteSummary.tsx index 74c9c2db..02bbea5c 100644 --- a/apps/storefront/src/pages/quote/components/QuoteSummary.tsx +++ b/apps/storefront/src/pages/quote/components/QuoteSummary.tsx @@ -12,6 +12,8 @@ import { store } from '@/store' import { B3LStorage, currencyFormat } from '@/utils' import { getBCPrice } from '@/utils/b3Product/b3Product' +import getQuoteDraftShowPriceTBD from '../shared/utils' + interface Summary { subtotal: number shipping: number @@ -33,6 +35,8 @@ const QuoteSummary = forwardRef((_, ref: Ref) => { ...defaultSummary, }) + const [isHideQuoteDraftPrice, setHideQuoteDraftPrice] = + useState(false) const { global: { showInclusiveTaxPrice }, } = store.getState() @@ -42,6 +46,10 @@ const QuoteSummary = forwardRef((_, ref: Ref) => { const getSummary = () => { const productList = B3LStorage.get('b2bQuoteDraftList') || [] + const isHidePrice = getQuoteDraftShowPriceTBD(productList) + + setHideQuoteDraftPrice(isHidePrice) + const newQuoteSummary = productList.reduce( (summary: Summary, product: CustomFieldItems) => { const { basePrice, taxPrice: productTax = 0, quantity } = product.node @@ -84,6 +92,12 @@ const QuoteSummary = forwardRef((_, ref: Ref) => { const priceFormat = (price: number) => `${currencyFormat(price)}` + const showPrice = (price: string | number): string | number => { + if (isHideQuoteDraftPrice) return b3Lang('quoteDraft.quoteSummary.tbd') + + return price + } + return ( @@ -107,7 +121,9 @@ const QuoteSummary = forwardRef((_, ref: Ref) => { {b3Lang('quoteDraft.quoteSummary.subTotal')} - {priceFormat(quoteSummary.subtotal)} + + {showPrice(priceFormat(quoteSummary.subtotal))} + ) => { }} > {b3Lang('quoteDraft.quoteSummary.tax')} - {priceFormat(quoteSummary.tax)} + + {showPrice(priceFormat(quoteSummary.tax))} + ) => { fontWeight: 'bold', }} > - {priceFormat(quoteSummary.grandTotal)} + {showPrice(priceFormat(quoteSummary.grandTotal))} diff --git a/apps/storefront/src/pages/quote/components/QuoteTable.tsx b/apps/storefront/src/pages/quote/components/QuoteTable.tsx index 2641291d..9bc91fe9 100644 --- a/apps/storefront/src/pages/quote/components/QuoteTable.tsx +++ b/apps/storefront/src/pages/quote/components/QuoteTable.tsx @@ -14,7 +14,7 @@ import { setModifierQtyPrice, snackbar, } from '@/utils' -import { getBCPrice } from '@/utils/b3Product/b3Product' +import { getBCPrice, getDisplayPrice } from '@/utils/b3Product/b3Product' import { getProductOptionsFields } from '@/utils/b3Product/shared/config' import ChooseOptionsDialog from '../../shoppingListDetails/components/ChooseOptionsDialog' @@ -155,6 +155,7 @@ function QuoteTable(props: ShoppingDetailTableProps, ref: Ref) { paginationTableRef.current?.setList([...newListItems]) updateList() + updateSummary() } const handleCheckProductQty = async (row: any, value: number | string) => { @@ -395,7 +396,11 @@ function QuoteTable(props: ShoppingDetailTableProps, ref: Ref) { padding: '12px 0', }} > - {`${currencyFormat(inTaxPrice)}`} + {getDisplayPrice({ + price: `${currencyFormat(inTaxPrice)}`, + productInfo: row, + showText: b3Lang('quoteDraft.quoteSummary.tbd'), + })} ) }, @@ -451,7 +456,11 @@ function QuoteTable(props: ShoppingDetailTableProps, ref: Ref) { padding: '12px 0', }} > - {`${currencyFormat(total)}`} + {getDisplayPrice({ + price: `${currencyFormat(total)}`, + productInfo: row, + showText: b3Lang('quoteDraft.quoteSummary.tbd'), + })} - {`Price: ${currencyFormat( - price - )}`} + {`Price: ${siglePrice}`} - {`Total: ${currencyFormat( - total - )}`} + {`Total: ${totalPrice}`} { + const { + global: { + blockPendingQuoteNonPurchasableOOS: { isEnableProduct }, + }, + } = store.getState() + + if (!isEnableProduct) return false + + const isHidePrice = products.some((product) => { + if (!getVariantInfoDisplayPrice(product.node.basePrice, product)) + return true + + return false + }) + + return isHidePrice +} + +export default getQuoteDraftShowPriceTBD diff --git a/apps/storefront/src/pages/shoppingListDetails/components/AddToShoppingList.tsx b/apps/storefront/src/pages/shoppingListDetails/components/AddToShoppingList.tsx index d255577c..36cda88b 100644 --- a/apps/storefront/src/pages/shoppingListDetails/components/AddToShoppingList.tsx +++ b/apps/storefront/src/pages/shoppingListDetails/components/AddToShoppingList.tsx @@ -1,5 +1,5 @@ import { useContext, useState } from 'react' -import { CallbackKey,useCallbacks } from '@b3/hooks' +import { CallbackKey, useCallbacks } from '@b3/hooks' import { useB3Lang } from '@b3/lang' import UploadFileIcon from '@mui/icons-material/UploadFile' import { Box, Card, CardContent, Divider, Typography } from '@mui/material' @@ -248,6 +248,7 @@ export default function AddToShoppingList(props: AddToListProps) { updateList={updateList} addToList={addToList} isB2BUser={isB2BUser} + type="shoppingList" /> diff --git a/apps/storefront/src/pages/shoppingListDetails/components/ChooseOptionsDialog.tsx b/apps/storefront/src/pages/shoppingListDetails/components/ChooseOptionsDialog.tsx index 1cb12720..437bc3b7 100644 --- a/apps/storefront/src/pages/shoppingListDetails/components/ChooseOptionsDialog.tsx +++ b/apps/storefront/src/pages/shoppingListDetails/components/ChooseOptionsDialog.tsx @@ -92,6 +92,7 @@ interface ChooseOptionsDialogProps { setIsLoading: Dispatch> addButtonText?: string isB2BUser: boolean + type?: string } interface ChooseOptionsProductProps extends ShoppingListProductItem { @@ -115,6 +116,7 @@ export default function ChooseOptionsDialog(props: ChooseOptionsDialogProps) { isLoading, setIsLoading, isB2BUser, + type, ...restProps } = props @@ -124,7 +126,10 @@ export default function ChooseOptionsDialog(props: ChooseOptionsDialogProps) { } = restProps const { - global: { showInclusiveTaxPrice }, + global: { + showInclusiveTaxPrice, + blockPendingQuoteNonPurchasableOOS: { isEnableProduct }, + }, } = store.getState() const [quantity, setQuantity] = useState(1) @@ -358,7 +363,11 @@ export default function ChooseOptionsDialog(props: ChooseOptionsDialogProps) { const validateQuantityNumber = () => { const { purchasing_disabled: purchasingDisabled = true } = variantInfo || {} - if (purchasingDisabled === true) { + if ( + type !== 'shoppingList' && + purchasingDisabled === true && + !isEnableProduct + ) { snackbar.error( b3Lang('shoppingList.chooseOptionsDialog.productNoLongerForSale') ) @@ -382,7 +391,6 @@ export default function ChooseOptionsDialog(props: ChooseOptionsDialogProps) { } cache.current = formValues - if ( Object.keys(formValues).length && formFields.length && diff --git a/apps/storefront/src/pages/shoppingListDetails/components/ProductListDialog.tsx b/apps/storefront/src/pages/shoppingListDetails/components/ProductListDialog.tsx index 5ad7d2de..add450fd 100644 --- a/apps/storefront/src/pages/shoppingListDetails/components/ProductListDialog.tsx +++ b/apps/storefront/src/pages/shoppingListDetails/components/ProductListDialog.tsx @@ -5,6 +5,7 @@ import { Box, InputAdornment, TextField, Typography } from '@mui/material' import { B3Dialog, B3ProductList, B3Sping, CustomButton } from '@/components' import { useMobile } from '@/hooks' +import { store } from '@/store' import { snackbar } from '@/utils' import { ShoppingListProductItem } from '../../../types' @@ -77,6 +78,7 @@ interface ProductListDialogProps { isLoading: boolean searchDialogTitle?: string addButtonText?: string + type?: string } const ProductTable = B3ProductList @@ -94,6 +96,7 @@ export default function ProductListDialog(props: ProductListDialogProps) { onAddToListClick, onChooseOptionsClick, isLoading, + type, searchDialogTitle = b3Lang('shoppingLists.title'), addButtonText = b3Lang('shoppingLists.addButtonText'), } = props @@ -115,8 +118,19 @@ export default function ProductListDialog(props: ProductListDialogProps) { const { purchasing_disabled: purchasingDisabled = true } = variants[0] || {} - if (purchasingDisabled === true) { - snackbar.error('This product is no longer for sale') + const { + global: { + blockPendingQuoteNonPurchasableOOS: { isEnableProduct }, + }, + } = store.getState() + if ( + type !== 'shoppingList' && + purchasingDisabled === true && + !isEnableProduct + ) { + snackbar.error( + b3Lang('shoppingList.chooseOptionsDialog.productNoLongerForSale') + ) return false } @@ -188,6 +202,7 @@ export default function ProductListDialog(props: ProductListDialogProps) { void setAddNoteOpen: (open: boolean) => void setNotes: (value: string) => void + showPrice: (price: string, row: CustomFieldItems) => string | number } const StyledImage = styled('img')(() => ({ @@ -51,6 +52,7 @@ function ShoppingDetailCard(props: ShoppingDetailCardProps) { setAddNoteOpen, setAddNoteItemId, setNotes, + showPrice, } = props const { @@ -175,7 +177,7 @@ function ShoppingDetailCard(props: ShoppingDetailCardProps) { }} > {b3Lang('shoppingList.shoppingDetailCard.price', { - price: currencyFormat(price), + price: showPrice(currencyFormat(price), shoppingDetail), })} @@ -216,7 +218,7 @@ function ShoppingDetailCard(props: ShoppingDetailCardProps) { }} > {b3Lang('shoppingList.shoppingDetailCard.total', { - total: currencyFormat(total), + total: showPrice(currencyFormat(total), shoppingDetail), })} { + const { + productsSearch: { isPriceHidden }, + } = row + return getDisplayPrice({ + price, + productInfo: row, + showText: isPriceHidden ? '' : price, + forcedSkip: true, + }) + } + const columnItems: TableColumnItem[] = [ { key: 'Product', @@ -495,7 +507,7 @@ function ShoppingDetailTable( padding: '12px 0', }} > - {currencyFormat(inTaxPrice)} + {showPrice(currencyFormat(inTaxPrice), row)} ) }, @@ -563,7 +575,7 @@ function ShoppingDetailTable( padding: '12px 0', }} > - {currencyFormat(totalPrice)} + {showPrice(currencyFormat(totalPrice), row)} `{ variantSku ( variantSkus: ${convertArrayToGraphql(skus)}, @@ -26,6 +32,36 @@ const getVariantInfoBySkus = ({ skus = [] }) => `{ } }` +const getSkusInfo = ({ skus = [] }) => `{ + variantSku ( + variantSkus: ${convertArrayToGraphql(skus)}, + storeHash: "${storeHash}" + channelId: ${B3SStorage.get('B3channelId') || 1} + ){ + isStock, + stock, + purchasingDisabled, + } +}` + +const getProductPurchasable = ({ + sku = '', + isProduct = true, + productId, +}: ProductPurchasable) => `{ + productPurchasable( + storeHash: "${storeHash}" + productId: ${+productId}, + sku:"${sku}", + isProduct: ${isProduct} + ){ + availability + inventoryLevel + inventoryTracking + purchasingDisabled + } +}` + const getVariantSkuByProductId = (productId: string) => `{ productVariantsInfo ( productId: "${productId}" @@ -63,6 +99,7 @@ const searchProducts = (data: CustomFieldItems) => `{ channelId, productUrl, taxClassId, + isPriceHidden, } }` @@ -118,6 +155,18 @@ export const getB2BVariantInfoBySkus = ( customMessage ) +export const getB2BSkusInfo = (data: CustomFieldItems): CustomFieldItems => + B3Request.graphqlB2B({ + query: getSkusInfo(data), + }) + +export const getB2BProductPurchasable = ( + data: ProductPurchasable +): CustomFieldItems => + B3Request.graphqlB2B({ + query: getProductPurchasable(data), + }) + export const getB2BVariantSkuByProductId = ( productId: string ): CustomFieldItems => diff --git a/apps/storefront/src/shared/service/b2b/graphql/quote.ts b/apps/storefront/src/shared/service/b2b/graphql/quote.ts index 951ff0ba..840a1745 100644 --- a/apps/storefront/src/shared/service/b2b/graphql/quote.ts +++ b/apps/storefront/src/shared/service/b2b/graphql/quote.ts @@ -238,6 +238,7 @@ const getQuoteInfo = (data: { id: number; date: string }) => `{ orderQuantityMaximum, orderQuantityMinimum, productName, + purchaseHandled, options, notes, costPrice, @@ -372,20 +373,14 @@ export const getBCQuotesList = (data: CustomFieldItems): CustomFieldItems => }) export const createQuote = (data: CustomFieldItems): CustomFieldItems => - B3Request.graphqlB2B( - { - query: quoteCreate(data), - }, - true - ) + B3Request.graphqlB2B({ + query: quoteCreate(data), + }) export const createBCQuote = (data: CustomFieldItems): CustomFieldItems => - B3Request.graphqlB2B( - { - query: quoteCreate(data), - }, - true - ) + B3Request.graphqlB2B({ + query: quoteCreate(data), + }) export const updateB2BQuote = (data: CustomFieldItems): CustomFieldItems => B3Request.graphqlB2B({ diff --git a/apps/storefront/src/shared/service/b2b/index.ts b/apps/storefront/src/shared/service/b2b/index.ts index df7df734..8d767dff 100644 --- a/apps/storefront/src/shared/service/b2b/index.ts +++ b/apps/storefront/src/shared/service/b2b/index.ts @@ -46,6 +46,7 @@ import { import { B2BProductsBulkUploadCSV, BcProductsBulkUploadCSV, + getB2BSkusInfo, getB2BVariantInfoBySkus, getBcVariantInfoBySkus, guestProductsBulkUploadCSV, @@ -178,6 +179,7 @@ export { getB2BRegisterLogo, getB2BShoppingList, getB2BShoppingListDetails, + getB2BSkusInfo, getB2BToken, getB2BVariantInfoBySkus, getBCAllOrders, diff --git a/apps/storefront/src/shared/service/request/fetch.ts b/apps/storefront/src/shared/service/request/fetch.ts index 2c021581..d1541ac6 100644 --- a/apps/storefront/src/shared/service/request/fetch.ts +++ b/apps/storefront/src/shared/service/request/fetch.ts @@ -40,7 +40,6 @@ function b3Fetch( if (type === RequestType.B2BGraphql) { const errors = res?.errors?.length ? res.errors[0] : {} const { message = '', extensions = {} } = errors - if (extensions.code === 40101) { window.location.href = '#/login?loginFlag=3&showTip=false' snackbar.error(message) diff --git a/apps/storefront/src/store/slices/global.ts b/apps/storefront/src/store/slices/global.ts index bc408469..02089a97 100644 --- a/apps/storefront/src/store/slices/global.ts +++ b/apps/storefront/src/store/slices/global.ts @@ -46,6 +46,10 @@ interface GlobalMessageDialog { saveFn?: () => void } +interface GlobalBlockPendingQuoteNonPurchasableOOS { + isEnableProduct?: boolean + isEnableRequest?: boolean +} export interface GlabolState { taxZoneRates?: TaxZoneRatesProps[] isClickEnterBtn?: boolean @@ -60,6 +64,7 @@ export interface GlabolState { bcUrl?: string cartNumber?: number storeInfo?: StoreInfoProps + blockPendingQuoteNonPurchasableOOS?: GlobalBlockPendingQuoteNonPurchasableOOS } const initialState: GlabolState = { @@ -92,6 +97,10 @@ const initialState: GlabolState = { type: '', urls: [], }, + blockPendingQuoteNonPurchasableOOS: { + isEnableProduct: false, + isEnableRequest: false, + }, } export const glabolSlice = createSlice({ @@ -129,6 +138,15 @@ export const glabolSlice = createSlice({ ) => { state.blockPendingAccountViewPrice = payload as Draft }, + setBlockPendingQuoteNonPurchasableOOS: ( + state, + { payload }: PayloadAction + ) => { + state.blockPendingQuoteNonPurchasableOOS = { + ...state.blockPendingQuoteNonPurchasableOOS, + ...(payload as GlobalBlockPendingQuoteNonPurchasableOOS), + } + }, setHeadLessBcUrl: (state, { payload }: PayloadAction) => { state.bcUrl = payload as Draft }, @@ -149,6 +167,7 @@ export const { setOpenPageReducer, setShowInclusiveTaxPrice, setBlockPendingAccountViewPrice, + setBlockPendingQuoteNonPurchasableOOS, setHeadLessBcUrl, setCartNumber, setStoreInfo, diff --git a/apps/storefront/src/types/products.ts b/apps/storefront/src/types/products.ts index f911e1af..9846cde2 100644 --- a/apps/storefront/src/types/products.ts +++ b/apps/storefront/src/types/products.ts @@ -36,6 +36,9 @@ export interface ProductItem { variants?: VariantsProps[] price_inc_tax?: string | number price_ex_tax?: string | number + availability?: string + inventoryLevel?: number + isPriceHidden?: boolean } export interface ProductVariantSkuInfo { diff --git a/apps/storefront/src/types/shoppingList.ts b/apps/storefront/src/types/shoppingList.ts index ce27efed..89271197 100644 --- a/apps/storefront/src/types/shoppingList.ts +++ b/apps/storefront/src/types/shoppingList.ts @@ -86,6 +86,7 @@ export interface ShoppingListProductItem extends ProductItem { orderQuantityMaximum?: number orderQuantityMinimum?: number variantId?: number | string + type?: string } export interface ShoppingListAddProductOption { diff --git a/apps/storefront/src/utils/b3Product/b3Product.ts b/apps/storefront/src/utils/b3Product/b3Product.ts index d431a1f1..673a8fcb 100644 --- a/apps/storefront/src/utils/b3Product/b3Product.ts +++ b/apps/storefront/src/utils/b3Product/b3Product.ts @@ -1256,6 +1256,113 @@ const getValidOptionsList = ( return newOptions } +interface DisplayPriceProps { + price: string | number + productInfo: CustomFieldItems + isProduct?: boolean + showText?: string + forcedSkip?: boolean +} + +const getProductInfoDisplayPrice = ( + price: string | number, + productInfo: CustomFieldItems +) => { + const { availability, inventoryLevel, inventoryTracking, quantity } = + productInfo + + if (availability === 'disabled') { + return '' + } + + if (inventoryTracking === 'none') { + return price + } + if (+quantity > +inventoryLevel) { + return '' + } + + return price +} + +export const getVariantInfoDisplayPrice = ( + price: string | number, + productInfo: CustomFieldItems +) => { + const newProductInfo = productInfo?.node ? productInfo.node : productInfo + + const inventoryTracking: string = newProductInfo?.productsSearch + ? newProductInfo.productsSearch.inventoryTracking + : newProductInfo.inventoryTracking + + const { quantity } = newProductInfo + + const variantSku = newProductInfo?.variantSku || newProductInfo?.sku + + const variants = newProductInfo?.productsSearch + ? newProductInfo.productsSearch.variants + : newProductInfo.variants + + const variant = variants.find((item: Variant) => item.sku === variantSku) + + if (variant?.sku) { + const { + purchasing_disabled: purchasingDisabled, + inventory_level: inventoryLevel, + } = variant + + if (purchasingDisabled) return '' + + if (inventoryTracking === 'none') return price + + if (+quantity > inventoryLevel) return '' + } + + return price +} + +const getDisplayPrice = ({ + price, + productInfo, + isProduct, + showText = '', + forcedSkip = false, +}: DisplayPriceProps): string | number => { + const { + global: { + blockPendingQuoteNonPurchasableOOS: { isEnableProduct }, + }, + } = store.getState() + + if (!isEnableProduct && !forcedSkip) return price + + const newProductInfo = productInfo?.node ? productInfo.node : productInfo + + if (newProductInfo?.purchaseHandled) return price + + const newPrice = isProduct + ? getProductInfoDisplayPrice(price, newProductInfo) + : getVariantInfoDisplayPrice(price, newProductInfo) + + return newPrice || showText || '' +} + +const judgmentBuyerProduct = ({ + productInfo, + isProduct, + price, +}: DisplayPriceProps): boolean => { + const newProductInfo = productInfo?.node ? productInfo.node : productInfo + + if (newProductInfo?.purchaseHandled) return true + + const newPrice = isProduct + ? getProductInfoDisplayPrice(price, newProductInfo) + : getVariantInfoDisplayPrice(price, newProductInfo) + + return !!newPrice +} + export { addQuoteDraftProduce, addQuoteDraftProducts, @@ -1266,11 +1373,13 @@ export { getBCPrice, getCalculatedParams, getCalculatedProductPrice, + getDisplayPrice, getModifiersPrice, getNewProductsList, getProductExtraPrice, getQuickAddProductExtraPrice, getValidOptionsList, + judgmentBuyerProduct, setModifierQtyPrice, validProductQty, } diff --git a/apps/storefront/src/utils/storefrontConfig.ts b/apps/storefront/src/utils/storefrontConfig.ts index aa7d23b9..835a2edc 100644 --- a/apps/storefront/src/utils/storefrontConfig.ts +++ b/apps/storefront/src/utils/storefrontConfig.ts @@ -14,6 +14,7 @@ import { import { getActiveBcCurrency } from '@/shared/service/bc' import { setBlockPendingAccountViewPrice, + setBlockPendingQuoteNonPurchasableOOS, setEnteredInclusive, setShowInclusiveTaxPrice, setStoreInfo, @@ -151,6 +152,18 @@ const storeforntKeys: StoreforntKeysProps[] = [ key: 'css_override', name: 'cssOverride', }, + { + key: 'non_purchasable_quote', + name: 'nonPurchasableQuote', + }, + { + key: 'buyer_non_purchasable_quote', + name: 'buyerNonPurchasableQuote', + }, + { + key: 'quote_on_non_purchasable_product_page', + name: 'quoteOnNonPurchasableProductPageBtn', + }, ] const getTemPlateConfig = async ( @@ -241,6 +254,33 @@ const getTemPlateConfig = async ( ) } + if (storeforntKey.key === 'non_purchasable_quote') { + store.dispatch( + setBlockPendingQuoteNonPurchasableOOS({ + isEnableProduct: item.value === '1', + // isEnableRequest: false + }) + ) + } + + if (storeforntKey.key === 'quote_on_non_purchasable_product_page') { + item.extraFields = { + ...item.extraFields, + locationSelector: + item.extraFields?.locationSelector || '.add-to-cart-buttons', + classSelector: item.extraFields?.classSelector || 'button', + customCss: item.extraFields?.customCss || '', + } + } + + if (storeforntKey.key === 'buyer_non_purchasable_quote') { + store.dispatch( + setBlockPendingQuoteNonPurchasableOOS({ + isEnableRequest: item.value === '1', + }) + ) + } + ;(obj as CustomFieldItems)[(storeforntKey as StoreforntKeysProps).name] = { ...item.extraFields, diff --git a/packages/global-b3/index.ts b/packages/global-b3/index.ts index b685cda3..e2856903 100644 --- a/packages/global-b3/index.ts +++ b/packages/global-b3/index.ts @@ -67,6 +67,7 @@ const globalB3 = { 'dom.navUserLoginElement': '.navUser-item.navUser-item--account', 'dom.setToQuote': '#form-action-addToCart', 'dom.setToShoppingListParentEl': '#add-to-cart-wrapper', + 'dom.setToNoPuchasable': '#add-to-cart-wrapper', 'dom.cartActions.container': '.cart-actions', 'dom.openB3Checkout': 'checkout-customer-continue', 'dom.cart': diff --git a/packages/lang/locales/en.json b/packages/lang/locales/en.json index 66e50ae5..c19ab14b 100644 --- a/packages/lang/locales/en.json +++ b/packages/lang/locales/en.json @@ -110,6 +110,7 @@ "global.customStyles.addToAllQuoteBtn": "Add All To Quote", "global.customStyles.shoppingListBtn": "Add to Shopping List", "global.masquerade.youAreMasqueradeAs": "You are masquerade as", + "global.addtoCart.purchasableAbdoosTip": "Products are out of stock or there are products that cannot be purchased.", "global.companyCredit.alert": "Your account does not currently allow purchasing due to a credit hold. Please contact us to reconcile.", "dashboard.company": "Company", @@ -582,6 +583,7 @@ "quoteDetail.message.message": "Message", "quoteDetail.message.merchantAnswers": "Merchant typically answers within 1 day", "quoteDetail.message.typeMessage": "Type a message...", + "quoteDetail.message.insufficientStock": "{ProductName} does not have sufficient stock. Please contact your Sales Rep to have it re-issued.", "quoteDetail.message.sent": "Sent", "quoteDetail.footer.proceedToCheckout": "Proceed to checkout", "quoteDetail.header.backToQuoteLists": "Back to quote lists",