diff --git a/.cspell.json b/.cspell.json index ca6fc677d..dc0842216 100644 --- a/.cspell.json +++ b/.cspell.json @@ -17,6 +17,8 @@ "msword", "purchasability", "registeredbctob2b", + "skus", + "clickaway", "Turborepo" ], // flagWords - list of words to be always considered incorrect diff --git a/apps/storefront/src/App.tsx b/apps/storefront/src/App.tsx index 5b8ed0620..7ff14b0ac 100644 --- a/apps/storefront/src/App.tsx +++ b/apps/storefront/src/App.tsx @@ -1,4 +1,4 @@ -import { lazy, useContext, useEffect, useState } from 'react'; +import { lazy, useContext, useEffect, useMemo, useState } from 'react'; import { HashRouter } from 'react-router-dom'; import { usePageMask } from '@/components'; @@ -10,8 +10,8 @@ import { CustomStyleContext } from '@/shared/customStyleButton'; import { GlobalContext } from '@/shared/global'; import { gotoAllowedAppPage } from '@/shared/routes'; import { setChannelStoreType } from '@/shared/service/b2b'; -import { CustomerRole } from '@/types'; import { + b2bJumpPath, getQuoteEnabled, handleHideRegisterPage, hideStorefrontElement, @@ -28,7 +28,7 @@ import { getTemPlateConfig, setStorefrontConfig, } from './utils/storefrontConfig'; -import { CHECKOUT_URL } from './constants'; +import { CHECKOUT_URL, PATH_ROUTES } from './constants'; import { isB2BUserSelector, rolePermissionSelector, @@ -46,6 +46,10 @@ const B3MasqueradeGlobalTip = lazy( () => import('@/components/outSideComponents/B3MasqueradeGlobalTip'), ); +const B3CompanyHierarchyExternalButton = lazy( + () => import('@/components/outSideComponents/B3CompanyHierarchyExternalButton'), +); + const HeadlessController = lazy(() => import('@/components/HeadlessController')); const ThemeFrame = lazy(() => import('@/components/ThemeFrame')); @@ -71,28 +75,12 @@ export default function App() { const currentClickedUrl = useAppSelector(({ global }) => global.currentClickedUrl); const isRegisterAndLogin = useAppSelector(({ global }) => global.isRegisterAndLogin); const bcGraphqlToken = useAppSelector(({ company }) => company.tokens.bcGraphqlToken); - const companyRoleName = useAppSelector((state) => state.company.customer.companyRoleName); - - const b2bPermissions = useAppSelector(rolePermissionSelector); - - const { getShoppingListPermission, getOrderPermission } = b2bPermissions; - const [authorizedPages, setAuthorizedPages] = useState('/orders'); - const IsRealJuniorBuyer = - +role === CustomerRole.JUNIOR_BUYER && companyRoleName === 'Junior Buyer'; - - useEffect(() => { - let currentAuthorizedPages = authorizedPages; + const { quotesCreateActionsPermission, shoppingListCreateActionsPermission } = + useAppSelector(rolePermissionSelector); - if (isB2BUser) { - currentAuthorizedPages = getShoppingListPermission ? '/shoppingLists' : '/accountSettings'; - - if (getOrderPermission) - currentAuthorizedPages = IsRealJuniorBuyer ? currentAuthorizedPages : '/orders'; - } - - setAuthorizedPages(currentAuthorizedPages); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [IsRealJuniorBuyer, getShoppingListPermission, getOrderPermission]); + const authorizedPages = useMemo(() => { + return isB2BUser ? b2bJumpPath(role) : PATH_ROUTES.ORDERS; + }, [role, isB2BUser]); const handleAccountClick = (href: string, isRegisterAndLogin: boolean) => { showPageMask(true); @@ -233,7 +221,7 @@ export default function App() { init(); // ignore dispatch, gotoPage, loginAndRegister, setOpenPage, storeDispatch, styleDispatch - // due they are function that do not depend on any reactive value + // due they are functions that do not depend on any reactive value // ignore href because is not a reactive value // eslint-disable-next-line react-hooks/exhaustive-deps }, [b2bId, customerId, emailAddress, isAgenting, isB2BUser, role]); @@ -246,9 +234,15 @@ export default function App() { dispatch({ type: 'common', payload: { - productQuoteEnabled, - cartQuoteEnabled, - shoppingListEnabled, + productQuoteEnabled: isB2BUser + ? productQuoteEnabled && quotesCreateActionsPermission + : productQuoteEnabled, + cartQuoteEnabled: isB2BUser + ? cartQuoteEnabled && quotesCreateActionsPermission + : cartQuoteEnabled, + shoppingListEnabled: isB2BUser + ? shoppingListEnabled && shoppingListCreateActionsPermission + : shoppingListEnabled, registerEnabled, }, }); @@ -260,7 +254,15 @@ export default function App() { // ignore dispatch due it's function that doesn't not depend on any reactive value // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isB2BUser, isAgenting, role, quoteConfig, storefrontConfig]); + }, [ + isB2BUser, + isAgenting, + role, + quoteConfig, + storefrontConfig, + quotesCreateActionsPermission, + shoppingListCreateActionsPermission, + ]); useEffect(() => { if (isOpen) { @@ -280,7 +282,6 @@ export default function App() { role, isRegisterAndLogin, isAgenting, - IsRealJuniorBuyer, authorizedPages, }); @@ -372,6 +373,7 @@ export default function App() { + { +export interface B3DialogProps { customActions?: () => ReactElement; isOpen: boolean; leftStyleBtn?: { [key: string]: string }; @@ -25,10 +34,12 @@ interface B3DialogProps { isShowBordered?: boolean; showRightBtn?: boolean; showLeftBtn?: boolean; - maxWidth?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | false; + maxWidth?: Breakpoint | false; fullWidth?: boolean; disabledSaveBtn?: boolean; - dialogContentSx?: { [key: string]: string }; + dialogContentSx?: SxProps; + dialogSx?: SxProps; + dialogWidth?: string; } export default function B3Dialog({ @@ -49,8 +60,10 @@ export default function B3Dialog({ showLeftBtn = true, maxWidth = 'sm', dialogContentSx = {}, + dialogSx = {}, fullWidth = false, disabledSaveBtn = false, + dialogWidth = '', }: B3DialogProps) { const container = useRef(null); @@ -58,6 +71,17 @@ export default function B3Dialog({ const isAgenting = useAppSelector(({ b2bFeatures }) => b2bFeatures.masqueradeCompany.isAgenting); + const customStyle = dialogWidth + ? { + '& .MuiPaper-elevation': { + width: isMobile ? '100%' : dialogWidth, + }, + ...dialogSx, + } + : { + ...dialogSx, + }; + const handleSaveClick = () => { if (handRightClick) { if (row) handRightClick(row); @@ -88,6 +112,7 @@ export default function B3Dialog({ aria-labelledby="alert-dialog-title" aria-describedby="alert-dialog-description" id="b2b-dialog-container" + sx={customStyle} > {title && ( void; +} -interface B3DropDownProps { +interface B3DropDownProps extends Partial { width?: string; - list: Array; - config?: ConfigProps; + list: Array; title: string; - handleItemClick: (arg0: T) => void; + handleItemClick?: (key: string | number) => void; value?: string; + menuRenderItemName?: (item: ListItemProps) => JSX.Element | string; } -export default function B3DropDown({ - width, - list, - config, - title, - value, - handleItemClick, -}: B3DropDownProps) { +function B3DropDown( + { + width, + list, + title, + value, + handleItemClick, + menuRenderItemName = (item) => item.name, + ...menu + }: B3DropDownProps, + ref: Ref, +) { const [isMobile] = useMobile(); const [isOpen, setIsOpen] = useState(false); - const ref = useRef(null); - const b3Lang = useB3Lang(); + const listRef = useRef(null); + + useImperativeHandle(ref, () => ({ + setOpenDropDown: () => setIsOpen(true), + })); const close = () => { setIsOpen(false); }; - const keyName = config?.name || 'name'; - return ( ({ > {!disableLogoutButton ? ( setIsOpen(true)} sx={{ pr: 0, @@ -81,7 +89,7 @@ export default function B3DropDown({ /> )} ({ '& .MuiList-root.MuiList-padding.MuiMenu-list': { pt: isMobile ? 0 : '8px', pb: isMobile ? 0 : '8px', + maxHeight: isMobile ? 'auto' : '200px', }, }} + {...(menu || {})} > {list.length && - list.map((item: any) => { - const name = item[keyName]; - const color = value === item.key ? '#3385d6' : 'black'; + list.map((item) => { + const { key } = item; + const color = value === key ? '#3385d6' : 'black'; return ( { close(); - handleItemClick(item); + if (handleItemClick) handleItemClick(key); }} > - {b3Lang('global.button.logout')} + {menuRenderItemName(item)} ); })} @@ -125,3 +135,5 @@ export default function B3DropDown({ ); } + +export default forwardRef(B3DropDown); diff --git a/apps/storefront/src/components/B3StoreContainer.tsx b/apps/storefront/src/components/B3StoreContainer.tsx index cfc8814d9..22e485070 100644 --- a/apps/storefront/src/components/B3StoreContainer.tsx +++ b/apps/storefront/src/components/B3StoreContainer.tsx @@ -1,5 +1,6 @@ import { ReactNode, useContext, useLayoutEffect } from 'react'; +import { Z_INDEX } from '@/constants'; import { GlobalContext } from '@/shared/global'; import { getBCStoreChannelId } from '@/shared/service/b2b'; import { getGlobalTranslations, setStoreInfo, setTimeFormat, useAppDispatch } from '@/store'; @@ -27,6 +28,14 @@ export interface StoreBasicInfo { storeName: string; } +type ZIndexType = keyof typeof Z_INDEX; +const setZIndexVariables = () => { + Object.keys(Z_INDEX).forEach((key) => { + const zIndexKey = key as ZIndexType; + document.documentElement.style.setProperty(`--z-index-${key}`, Z_INDEX[zIndexKey].toString()); + }); +}; + export default function B3StoreContainer(props: B3StoreContainerProps) { const showPageMask = usePageMask(); @@ -89,6 +98,7 @@ export default function B3StoreContainer(props: B3StoreContainerProps) { showPageMask(false); } }; + setZIndexVariables(); getStoreBasicInfo(); // disabling because dispatchers are not supposed to be here // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/apps/storefront/src/components/filter/B3Filter.tsx b/apps/storefront/src/components/filter/B3Filter.tsx index 9ee1f7df7..111c6b021 100644 --- a/apps/storefront/src/components/filter/B3Filter.tsx +++ b/apps/storefront/src/components/filter/B3Filter.tsx @@ -59,6 +59,9 @@ interface B3FilterProps { showB3FilterMoreIcon?: boolean; searchValue?: string; resetFilterInfo?: () => void; + pcContainerWidth?: string; + pcSearchContainerWidth?: string; + pcTotalWidth?: string; } function B3Filter(props: B3FilterProps) { @@ -74,6 +77,9 @@ function B3Filter(props: B3FilterProps) { showB3FilterMoreIcon = true, searchValue = '', resetFilterInfo, + pcContainerWidth = '29rem', + pcSearchContainerWidth = '60%', + pcTotalWidth = 'unset', } = props; const [isMobile] = useMobile(); @@ -101,17 +107,22 @@ function B3Filter(props: B3FilterProps) { display: 'flex', justifyContent: 'space-between', mb: '30px', + width: pcTotalWidth, }} > - + {showB3FilterMoreIcon && ( ({ setOpen(true); }; + const filterCounterVal = useMemo(() => { + if (!filterCounter) return 0; + + const values = getValues(); + + const newCounter = filterMoreInfo.reduce((cur, item) => { + const newItem: CustomFieldItems = item; + if (includesFilterType.includes(newItem.fieldType) && values[newItem.name]) { + cur -= 1; + } + + return cur; + }, filterCounter); + + return newCounter; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filterCounter]); + const handleClose = () => { setOpen(false); }; @@ -197,6 +217,10 @@ function B3FilterMore({ ':hover': { backgroundColor: getHoverColor('#FFFFFF', 0.1), }, + '& svg': { + width: '32px', + height: '32px', + }, }} > @@ -214,7 +238,7 @@ function B3FilterMore({ }} > { + if (nameKey) { + setInputValue(getValues()[inputNameKey as string] || ''); + } else { + setInputValue(''); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [nameKey, inputNameKey]); + const muiAttributeProps = muiSelectProps || {}; const fieldsProps = { @@ -114,6 +129,7 @@ export default function B3ControlAutocomplete({ control, errors, ...rest }: Form cache.current.inputValue = value.name; setValue(name, value.id); + setValue(inputNameKey, value.name); if (setValueName) { setValueName(value.name); diff --git a/apps/storefront/src/components/index.ts b/apps/storefront/src/components/index.ts index e389853cb..adf07133e 100644 --- a/apps/storefront/src/components/index.ts +++ b/apps/storefront/src/components/index.ts @@ -14,9 +14,12 @@ export * from './HeadlessController'; export { B3PageMask, Loading, usePageMask } from './loading'; export { default as B3HoverButton } from './outSideComponents/B3HoverButton'; export { default as B3MasqueradeGlobalTip } from './outSideComponents/B3MasqueradeGlobalTip'; +export { default as B3CompanyHierarchyExternalButton } from './outSideComponents/B3CompanyHierarchyExternalButton'; export { default as RegisteredCloseButton } from './RegisteredCloseButton'; export { default as B3NoData } from './table/B3NoData'; export { B3PaginationTable } from './table/B3PaginationTable'; export { B3Table } from './table/B3Table'; export { default as ThemeFrame } from './ThemeFrame'; export { default as B3Upload } from './upload/B3Upload'; +export { default as B2BAutoCompleteCheckbox } from './ui/B2BAutoCompleteCheckbox'; +export { default as B2BSwitchCompanyModal } from './ui/B2BSwitchCompanyModal'; diff --git a/apps/storefront/src/components/layout/B3AccountInfo.tsx b/apps/storefront/src/components/layout/B3AccountInfo.tsx index 53bd64add..86b3a7a45 100644 --- a/apps/storefront/src/components/layout/B3AccountInfo.tsx +++ b/apps/storefront/src/components/layout/B3AccountInfo.tsx @@ -1,10 +1,12 @@ +import { useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; +import { useB3Lang } from '@b3/lang'; import { Box } from '@mui/material'; import { useMobile } from '@/hooks'; import { useAppSelector } from '@/store'; -import B3DropDown from '../B3DropDown'; +import B3DropDown, { ListItemProps } from '../B3DropDown'; interface ListProps { [key: string]: string; @@ -14,7 +16,6 @@ const list: Array = [ { name: 'Log out', key: 'logout', - type: 'button', idLang: 'global.button.logout', }, ]; @@ -31,7 +32,13 @@ export default function B3AccountInfo({ closeSidebar }: B3AccountInfoProps) { const navigate = useNavigate(); - const handleItemClick = async (item: ListProps) => { + const b3Lang = useB3Lang(); + + const handleItemClick = async (key: string | number) => { + const item = list.find((v) => v.key === key); + + if (!item) return; + if (item.key === 'logout') { navigate('/login?loginFlag=loggedOutLogin'); } else if (item.type === 'path' && item.key) { @@ -44,6 +51,15 @@ export default function B3AccountInfo({ closeSidebar }: B3AccountInfoProps) { const name = `${firstName} ${lastName}`; + const newList: ListItemProps[] = useMemo(() => { + return list.map((item) => { + return { + key: item.key, + name: b3Lang(item.idLang), + }; + }); + }, [b3Lang]); + return ( - + ); } diff --git a/apps/storefront/src/components/layout/B3CompanyHierarchy.tsx b/apps/storefront/src/components/layout/B3CompanyHierarchy.tsx new file mode 100644 index 000000000..85ba152ac --- /dev/null +++ b/apps/storefront/src/components/layout/B3CompanyHierarchy.tsx @@ -0,0 +1,186 @@ +import { useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { useB3Lang } from '@b3/lang'; +import CheckIcon from '@mui/icons-material/Check'; +import { Box, Chip, Grid } from '@mui/material'; + +import HierarchyDialog from '@/pages/CompanyHierarchy/components/HierarchyDialog'; +import { CustomStyleContext } from '@/shared/customStyleButton'; +import { setOpenCompanyHierarchyDropDown, useAppDispatch, useAppSelector } from '@/store'; +import { CompanyHierarchyProps, PagesSubsidiariesPermissionProps } from '@/types'; + +import B3DropDown, { DropDownHandle, ListItemProps } from '../B3DropDown'; + +const chipInfo = { + currentInfo: { + langId: 'companyHierarchy.chip.currentCompany', + }, + representingInfo: { + langId: 'companyHierarchy.chip.selectCompany', + }, +}; + +function B3CompanyHierarchy() { + const b3Lang = useB3Lang(); + + const dispatch = useAppDispatch(); + + const [open, setOpen] = useState(false); + + const [currentRow, setCurrentRow] = useState | null>(null); + + const dropDownRef = useRef(null); + + const { + state: { + switchAccountButton: { color = '#ED6C02' }, + }, + } = useContext(CustomStyleContext); + + const { id: currentCompanyId } = useAppSelector(({ company }) => company.companyInfo); + + const salesRepCompanyId = useAppSelector(({ b2bFeatures }) => b2bFeatures.masqueradeCompany.id); + + const { pagesSubsidiariesPermission } = useAppSelector(({ company }) => company); + + const { selectCompanyHierarchyId, companyHierarchyList } = useAppSelector( + ({ company }) => company.companyHierarchyInfo, + ); + + const { isOpenCompanyHierarchyDropDown } = useAppSelector(({ global }) => global); + + const isPagesSubsidiariesPermission = useMemo(() => { + return Object.keys(pagesSubsidiariesPermission).some( + (key) => pagesSubsidiariesPermission[key as keyof PagesSubsidiariesPermissionProps], + ); + }, [pagesSubsidiariesPermission]); + + useEffect(() => { + if (isOpenCompanyHierarchyDropDown && dropDownRef?.current) { + dropDownRef.current?.setOpenDropDown(); + } + }, [isOpenCompanyHierarchyDropDown]); + + const info = useMemo(() => { + const showTitleId = selectCompanyHierarchyId || currentCompanyId || salesRepCompanyId; + + const title = companyHierarchyList.find( + (list) => +list.companyId === +showTitleId, + )?.companyName; + + const list: ListItemProps[] = companyHierarchyList.map((item) => ({ + name: item.companyName, + key: item.companyId, + })); + + return { + title, + list, + }; + }, [selectCompanyHierarchyId, currentCompanyId, companyHierarchyList, salesRepCompanyId]); + + const handleClose = () => { + setOpen(false); + dispatch(setOpenCompanyHierarchyDropDown(false)); + }; + const handleRowClick = (key: number) => { + const item = info.list.find((list) => +list.key === key); + if (!item) return; + setCurrentRow({ + companyId: +item.key, + companyName: item.name, + }); + setOpen(true); + }; + + const menuRenderItemName = (item: ListItemProps) => { + const { name, key } = item; + + const selectId = selectCompanyHierarchyId || currentCompanyId || salesRepCompanyId; + + return ( + + + {name} + + + {key === +selectId && } + + + ); + }; + + const { langId: chipLangId } = selectCompanyHierarchyId + ? chipInfo.representingInfo + : chipInfo.currentInfo; + + if (!info?.list?.length || !isPagesSubsidiariesPermission) return null; + + if (!currentCompanyId && !salesRepCompanyId) return null; + + return ( + <> + + handleRowClick(+item)} + list={info?.list || []} + /> + + + + + + + ); +} + +export default B3CompanyHierarchy; diff --git a/apps/storefront/src/components/layout/B3Layout.tsx b/apps/storefront/src/components/layout/B3Layout.tsx index 5ea2296e7..9c47bc058 100644 --- a/apps/storefront/src/components/layout/B3Layout.tsx +++ b/apps/storefront/src/components/layout/B3Layout.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useContext, useEffect, useState } from 'react'; +import { ReactNode, useContext, useEffect, useMemo, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { useB3Lang } from '@b3/lang'; import { Box, useMediaQuery } from '@mui/material'; @@ -91,19 +91,22 @@ export default function B3Layout({ children }: { children: ReactNode }) { }); }; + const overflowStyle = useMemo(() => { + const overflowXHiddenPage = ['/invoice']; + if (overflowXHiddenPage.includes(location.pathname)) { + return { + overflowX: 'hidden', + }; + } + + return {}; + }, [location]); + return ( {isMobile ? ( {children} ) : ( - // @@ -158,8 +162,6 @@ export default function B3Layout({ children }: { children: ReactNode }) { - - // )} company.companyHierarchyInfo, + ); + const isShowCart = isB2BUser ? purchasabilityPermission : true; const customColor = getContrastColor(backgroundColor); @@ -63,19 +68,29 @@ export default function MainHeader({ title }: { title: string }) { alignItems: 'center', }} > - - {+role === 3 && - (companyInfo?.companyName || - salesRepCompanyName || - b3Lang('global.B3MainHeader.superAdmin'))} - + + {+role === 3 && + (companyInfo?.companyName || + salesRepCompanyName || + b3Lang('global.B3MainHeader.superAdmin'))} + + {isEnabledCompanyHierarchy && } + void; } +const getSubsidiariesPermission = (routes: RouteItem[]) => { + const subsidiariesPermission = routes.reduce((all, cur) => { + if (cur?.subsidiariesCompanyKey) { + const code = cur.permissionCodes?.includes(',') + ? cur.permissionCodes.split(',')[0].trim() + : cur.permissionCodes; + + all[cur.subsidiariesCompanyKey] = validatePermissionWithComparisonType({ + level: 3, + code, + }); + } + + return all; + }, {} as PagesSubsidiariesPermissionProps); + + return subsidiariesPermission; +}; + export default function B3Nav({ closeSidebar }: B3NavProps) { const [isMobile] = useMobile(); const navigate = useNavigate(); @@ -25,6 +51,12 @@ export default function B3Nav({ closeSidebar }: B3NavProps) { const { dispatch } = useContext(DynamicallyVariableContext); const role = useAppSelector(({ company }) => company.customer.role); + const { selectCompanyHierarchyId, isEnabledCompanyHierarchy } = useAppSelector( + ({ company }) => company.companyHierarchyInfo, + ); + + const { permissions } = useAppSelector(({ company }) => company); + const { state: globalState } = useContext(GlobalContext); const { quoteDetailHasNewMessages, registerEnabled } = globalState; @@ -73,12 +105,69 @@ export default function B3Nav({ closeSidebar }: B3NavProps) { closeSidebar(false); } }; - const menuItems = () => { - const newRoutes = getAllowedRoutes(globalState).filter((route) => route.isMenuItem); - return newRoutes; - }; - const newRoutes = menuItems(); + useEffect(() => { + let isHasSubsidiariesCompanyPermission = false; + const { hash } = window.location; + const url = hash.split('#')[1] || ''; + const routes = getAllowedRoutes(globalState).filter((route) => route.isMenuItem); + + if (url) { + const routeItem = getAllowedRoutes(globalState).find((item) => { + return matchPath(item.path, url); + }); + + if (routeItem && routeItem?.subsidiariesCompanyKey) { + const { permissionCodes } = routeItem; + + const code = permissionCodes?.includes(',') + ? permissionCodes.split(',')[0].trim() + : permissionCodes; + + isHasSubsidiariesCompanyPermission = validatePermissionWithComparisonType({ + code, + level: 3, + }); + } + } + + const subsidiariesPermission = getSubsidiariesPermission(routes); + + store.dispatch(setPagesSubsidiariesPermission(subsidiariesPermission)); + + store.dispatch( + setCompanyHierarchyInfoModules({ + isHasCurrentPagePermission: isHasSubsidiariesCompanyPermission, + }), + ); + }, [selectCompanyHierarchyId, globalState, navigate]); + + const newRoutes = useMemo(() => { + let routes = getAllowedRoutes(globalState).filter((route) => route.isMenuItem); + + const subsidiariesPermission = getSubsidiariesPermission(routes); + + if (selectCompanyHierarchyId) { + routes = routes.filter((route) => + route?.subsidiariesCompanyKey + ? subsidiariesPermission[route.subsidiariesCompanyKey] + : false, + ); + } else { + routes = routes.filter((route) => { + if (route?.subsidiariesCompanyKey === 'companyHierarchy') { + return isEnabledCompanyHierarchy && subsidiariesPermission[route.subsidiariesCompanyKey]; + } + return true; + }); + } + + return routes; + + // ignore permissions because verifyCompanyLevelPermissionByCode method with permissions + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectCompanyHierarchyId, permissions, globalState, isEnabledCompanyHierarchy]); + const activePath = (path: string) => { if (location.pathname === path) { B3SStorage.set('prevPath', path); diff --git a/apps/storefront/src/components/outSideComponents/B3CompanyHierarchyExternalButton.tsx b/apps/storefront/src/components/outSideComponents/B3CompanyHierarchyExternalButton.tsx new file mode 100644 index 000000000..c337951d7 --- /dev/null +++ b/apps/storefront/src/components/outSideComponents/B3CompanyHierarchyExternalButton.tsx @@ -0,0 +1,200 @@ +import { useContext, useMemo } from 'react'; +import { useB3Lang } from '@b3/lang'; +import BusinessIcon from '@mui/icons-material/Business'; +import { Box, SnackbarOrigin, SxProps } from '@mui/material'; +import Snackbar from '@mui/material/Snackbar'; + +import { PAGES_SUBSIDIARIES_PERMISSION_KEYS, PATH_ROUTES, Z_INDEX } from '@/constants'; +import useMobile from '@/hooks/useMobile'; +import { type SetOpenPage } from '@/pages/SetOpenPage'; +import { CustomStyleContext } from '@/shared/customStyleButton'; +import { setOpenCompanyHierarchyDropDown, useAppDispatch, useAppSelector } from '@/store'; +import { PagesSubsidiariesPermissionProps } from '@/types'; + +import { + getContrastColor, + getLocation, + getPosition, + getStyles, + setMUIMediaStyle, + splitCustomCssValue, +} from './utils/b3CustomStyles'; + +const bottomHeightPage = ['shoppingList/', 'purchased-products']; + +interface B3CompanyHierarchyExternalButtonProps { + isOpen: boolean; + setOpenPage: SetOpenPage; +} +function B3CompanyHierarchyExternalButton({ + setOpenPage, + isOpen, +}: B3CompanyHierarchyExternalButtonProps) { + const b3Lang = useB3Lang(); + + const { hash } = window.location; + + const [isMobile] = useMobile(); + + const dispatch = useAppDispatch(); + + const { selectCompanyHierarchyId, companyHierarchyList } = useAppSelector( + ({ company }) => company.companyHierarchyInfo, + ); + + const { pagesSubsidiariesPermission } = useAppSelector(({ company }) => company); + + const isAddBottom = bottomHeightPage.some((item: string) => hash.includes(item)); + + const { + state: { switchAccountButton }, + } = useContext(CustomStyleContext); + + const defaultLocation: SnackbarOrigin = { + vertical: 'bottom', + horizontal: 'left', + }; + + const { + color = '', + customCss = '', + location = 'bottomLeft', + horizontalPadding = '', + verticalPadding = '', + } = switchAccountButton; + + const cssInfo = splitCustomCssValue(customCss); + const { + cssValue, + mediaBlocks, + }: { + cssValue: string; + mediaBlocks: string[]; + } = cssInfo; + const MUIMediaStyle = setMUIMediaStyle(mediaBlocks); + + const baseStyles: SxProps = { + backgroundColor: color || '#ED6C02', + color: getContrastColor(color || '#FFFFFF'), + bottom: 0, + ...getStyles(cssValue), + }; + + const desktopStyles: SxProps = isAddBottom ? { bottom: '90px !important' } : {}; + const buyerPortalStyles: SxProps = { + bottom: '24px', + left: '24px', + right: 'auto', + top: 'unset', + }; + const mobileOpenStyles: SxProps = { + width: '100%', + bottom: 0, + left: 0, + }; + + let sx: SxProps = { ...baseStyles }; + + if (isMobile) { + if (isOpen) { + sx = { ...sx, ...mobileOpenStyles }; + } else { + sx = { ...sx, ...getPosition(horizontalPadding, verticalPadding, location) }; + } + } else if (isOpen) { + sx = { ...sx, ...buyerPortalStyles, ...desktopStyles }; + } else { + sx = { ...sx, ...getPosition(horizontalPadding, verticalPadding, location), ...desktopStyles }; + } + + const companyName: string = useMemo(() => { + if (!selectCompanyHierarchyId) { + return ''; + } + + return ( + companyHierarchyList.find((company) => company.companyId === +selectCompanyHierarchyId) + ?.companyName || '' + ); + }, [selectCompanyHierarchyId, companyHierarchyList]); + + const handleHierarchyExternalBtnClick = () => { + const { companyHierarchy } = pagesSubsidiariesPermission; + + if (companyHierarchy) { + const { COMPANY_HIERARCHY } = PATH_ROUTES; + + setOpenPage({ + isOpen: true, + openUrl: COMPANY_HIERARCHY, + }); + + return; + } + + const key = Object.keys(pagesSubsidiariesPermission).find((key) => { + return !!pagesSubsidiariesPermission[key as keyof PagesSubsidiariesPermissionProps]; + }); + + const route = PAGES_SUBSIDIARIES_PERMISSION_KEYS.find((item) => item.key === key); + + if (route && !isOpen) { + setOpenPage({ + isOpen: true, + openUrl: route.path, + }); + } + dispatch(setOpenCompanyHierarchyDropDown(true)); + }; + + return ( + <> + {!!companyName && ( + + handleHierarchyExternalBtnClick()} + > + + + {b3Lang('global.companyHierarchy.externalBtn')} + + + {companyName} + + + + )} + + ); +} + +export default B3CompanyHierarchyExternalButton; diff --git a/apps/storefront/src/components/outSideComponents/utils/b3CustomStyles.ts b/apps/storefront/src/components/outSideComponents/utils/b3CustomStyles.ts index 1bb5f4b56..b0c5c8a6d 100644 --- a/apps/storefront/src/components/outSideComponents/utils/b3CustomStyles.ts +++ b/apps/storefront/src/components/outSideComponents/utils/b3CustomStyles.ts @@ -115,7 +115,7 @@ export const setMediaStyle = (mediaBlocks: string[], className: string) => { }; export const setMUIMediaStyle = (mediaBlocks: string[]) => { - if (mediaBlocks.length === 0) return []; + if (mediaBlocks.length === 0) return {}; const newMedia: CustomFieldItems = {}; mediaBlocks.forEach((media: string) => { const mediaArr = media.split('\n'); diff --git a/apps/storefront/src/components/table/B3PaginationTable.tsx b/apps/storefront/src/components/table/B3PaginationTable.tsx index 3ac2e0c1a..6c682ada8 100644 --- a/apps/storefront/src/components/table/B3PaginationTable.tsx +++ b/apps/storefront/src/components/table/B3PaginationTable.tsx @@ -13,6 +13,7 @@ import isEmpty from 'lodash-es/isEmpty'; import isEqual from 'lodash-es/isEqual'; import { useMobile } from '@/hooks'; +import { useAppSelector } from '@/store'; import { B3Table, TableColumnItem } from './B3Table'; @@ -53,6 +54,7 @@ interface B3PaginationTableProps { sortByFn?: (e: { key: string }) => void; orderBy?: string; pageType?: string; + isAutoRefresh?: boolean; } function PaginationTable( @@ -89,6 +91,7 @@ function PaginationTable( sortByFn = () => {}, orderBy = '', pageType = '', + isAutoRefresh = true, }: B3PaginationTableProps, ref?: Ref, ) { @@ -97,6 +100,11 @@ function PaginationTable( first: rowsPerPageOptions[0], }; + const { selectCompanyHierarchyId } = useAppSelector( + ({ company }) => company.companyHierarchyInfo, + ); + const selectCompanyHierarchyIdCache = useRef(selectCompanyHierarchyId); + const cache = useRef(null); const [loading, setLoading] = useState(); @@ -204,10 +212,19 @@ function PaginationTable( }, [fetchList, pagination]); useEffect(() => { + const isChangeCompany = +selectCompanyHierarchyIdCache.current !== +selectCompanyHierarchyId; if (!isEmpty(searchParams)) { - fetchList(); + if (isChangeCompany) { + if (isAutoRefresh) fetchList(pagination, true); + selectCompanyHierarchyIdCache.current = selectCompanyHierarchyId; + } else { + if (isAutoRefresh && pageType === 'orderListPage') fetchList(pagination, true); + fetchList(); + } } - }, [fetchList, searchParams]); + // ignore pageType because is not a reactive value + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [fetchList, searchParams, selectCompanyHierarchyId, pagination, isAutoRefresh]); useEffect(() => { if (getSelectCheckbox) getSelectCheckbox(selectCheckbox); diff --git a/apps/storefront/src/components/ui/B2BAutoCompleteCheckbox.tsx b/apps/storefront/src/components/ui/B2BAutoCompleteCheckbox.tsx new file mode 100644 index 000000000..f73f098cb --- /dev/null +++ b/apps/storefront/src/components/ui/B2BAutoCompleteCheckbox.tsx @@ -0,0 +1,214 @@ +import { useEffect, useMemo, useState } from 'react'; +import { useB3Lang } from '@b3/lang'; +import { Check } from '@mui/icons-material'; +import { + Checkbox, + FormControl, + InputLabel, + ListItemText, + MenuItem, + Select, + SelectChangeEvent, +} from '@mui/material'; + +import useMobile from '@/hooks/useMobile'; +import { useAppSelector } from '@/store'; + +interface B2BAutoCompleteCheckboxProps { + handleChangeCompanyIds: (companyIds: number[]) => void; +} + +function B2BAutoCompleteCheckbox({ handleChangeCompanyIds }: B2BAutoCompleteCheckboxProps) { + const b3Lang = useB3Lang(); + const [isMobile] = useMobile(); + const { id: currentCompanyId, companyName } = useAppSelector( + ({ company }) => company.companyInfo, + ); + const { selectCompanyHierarchyId, companyHierarchyList, companyHierarchySelectSubsidiariesList } = + useAppSelector(({ company }) => company.companyHierarchyInfo); + + const [isCheckedAll, setIsCheckedAll] = useState(false); + const [companyNames, setCompanyNames] = useState([companyName]); + + const [companyIds, setCompanyIds] = useState([ + +selectCompanyHierarchyId || +currentCompanyId, + ]); + const newCompanyHierarchyList = useMemo(() => { + const allCompany = { + companyId: -1, + companyName: 'All', + parentCompanyId: null, + parentCompanyName: '', + }; + + return [ + allCompany, + ...(selectCompanyHierarchyId ? companyHierarchySelectSubsidiariesList : companyHierarchyList), + ]; + }, [companyHierarchyList, selectCompanyHierarchyId, companyHierarchySelectSubsidiariesList]); + + useEffect(() => { + setCompanyIds([+selectCompanyHierarchyId || +currentCompanyId]); + }, [selectCompanyHierarchyId, currentCompanyId]); + + const handleChange = (event: SelectChangeEvent) => { + const { value } = event.target; + const currentValues = typeof value === 'string' ? [value] : value; + let selectCompanies: number[] = []; + if (currentValues.includes('All')) { + if ( + companyNames.includes('All') && + (currentValues.length !== newCompanyHierarchyList.length || + (newCompanyHierarchyList.length === 2 && isCheckedAll)) + ) { + setIsCheckedAll(false); + selectCompanies = []; + newCompanyHierarchyList.forEach( + ({ companyName, companyId }: { companyName: string; companyId: number }) => { + if (currentValues.includes(companyName) && companyName !== 'All') { + selectCompanies.push(companyId); + } + }, + ); + } else { + selectCompanies = [-1]; + setIsCheckedAll(true); + } + } + + if (!currentValues.includes('All')) { + if (isCheckedAll) { + selectCompanies = [+selectCompanyHierarchyId || +currentCompanyId]; + setIsCheckedAll(false); + } else { + selectCompanies = []; + currentValues.forEach((item: string) => { + const company = newCompanyHierarchyList.find((company) => company.companyName === item); + if (company) { + selectCompanies.push(company.companyId); + } + }); + if (!currentValues.length) { + selectCompanies = [-1]; + setIsCheckedAll(true); + } + } + } + + setCompanyIds(selectCompanies); + let selectedCompanyIds = selectCompanies; + if (selectCompanyHierarchyId && selectCompanies.includes(-1)) { + selectedCompanyIds = []; + companyHierarchySelectSubsidiariesList.forEach(({ companyId }: { companyId: number }) => { + selectedCompanyIds.push(companyId); + }); + } + handleChangeCompanyIds(selectedCompanyIds); + }; + + useEffect(() => { + const newSelectedCompany: string[] = []; + if (companyIds.length) { + companyIds.forEach((id) => { + const currentCompany = newCompanyHierarchyList.find( + (company) => +company.companyId === +id, + ); + + if (currentCompany) { + newSelectedCompany.push(currentCompany.companyName); + } + }); + } else { + const activeCompany = selectCompanyHierarchyId || currentCompanyId; + const currentCompany = newCompanyHierarchyList.find( + (company) => +company.companyId === +activeCompany, + ); + if (currentCompany) { + newSelectedCompany.push(currentCompany.companyName); + } + } + + setCompanyNames(newSelectedCompany); + // ignore selectCompanyHierarchyId because it is not a value that must be monitored + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [companyIds, newCompanyHierarchyList, currentCompanyId]); + + const showName = useMemo(() => { + if (companyNames.includes('All')) { + return ['All']; + } + return companyNames; + }, [companyNames]); + + const MenuProps = { + PaperProps: { + style: { + maxHeight: 300, + }, + }, + }; + + return ( + + + {b3Lang('global.B2BAutoCompleteCheckbox.input.label')} + + + + ); +} + +export default B2BAutoCompleteCheckbox; diff --git a/apps/storefront/src/components/ui/B2BSwitchCompanyModal.tsx b/apps/storefront/src/components/ui/B2BSwitchCompanyModal.tsx new file mode 100644 index 000000000..08a4c754d --- /dev/null +++ b/apps/storefront/src/components/ui/B2BSwitchCompanyModal.tsx @@ -0,0 +1,127 @@ +import { useState } from 'react'; +import { useB3Lang } from '@b3/lang'; +import { Box } from '@mui/material'; +import Cookies from 'js-cookie'; + +import B3Dialog from '@/components/B3Dialog'; +import { endUserMasqueradingCompany, startUserMasqueradingCompany } from '@/shared/service/b2b'; +import { deleteCart } from '@/shared/service/bc/graphql/cart'; +import { store, useAppSelector } from '@/store'; +import { setCompanyHierarchyInfoModules } from '@/store/slices/company'; +import { setCartNumber } from '@/store/slices/global'; +import b2bLogger from '@/utils/b3Logger'; +import { deleteCartData } from '@/utils/cartUtils'; + +interface B2BSwitchCompanyModalPropsTypes { + open: boolean; + title: string; + fullWidth?: boolean; + tipText: string; + setIsOpenSwitchCompanyModal: (value: boolean) => void; + switchCompanyId: string | number | undefined; + rightSizeBtn?: string; +} + +function B2BSwitchCompanyModal(props: B2BSwitchCompanyModalPropsTypes) { + const { + open, + title, + fullWidth = true, + tipText, + setIsOpenSwitchCompanyModal, + switchCompanyId = 0, + rightSizeBtn = '', + } = props; + const b3Lang = useB3Lang(); + + const { id: currentCompanyId } = useAppSelector(({ company }) => company.companyInfo); + + const { companyHierarchyList } = useAppSelector(({ company }) => company.companyHierarchyInfo); + + const [loading, setLoading] = useState(false); + + const handleClose = () => { + setIsOpenSwitchCompanyModal(false); + }; + + const handleSwitchCompanyClick = async () => { + setLoading(true); + try { + if (+switchCompanyId === +currentCompanyId) { + await endUserMasqueradingCompany(); + } else if (switchCompanyId) { + await startUserMasqueradingCompany(+switchCompanyId); + } + + const cartEntityId = Cookies.get('cartId'); + if (cartEntityId) { + const deleteCartObject = deleteCartData(cartEntityId); + + await deleteCart(deleteCartObject); + + store.dispatch(setCartNumber(0)); + } + + if (switchCompanyId) { + store.dispatch( + setCompanyHierarchyInfoModules({ + selectCompanyHierarchyId: + +switchCompanyId === +currentCompanyId ? '' : +switchCompanyId, + companyHierarchyList: companyHierarchyList || [], + }), + ); + } + } catch (error) { + b2bLogger.error(error); + } finally { + setLoading(false); + handleClose(); + } + }; + + return ( + + + + {tipText} + + + + ); +} + +export default B2BSwitchCompanyModal; diff --git a/apps/storefront/src/constants/index.ts b/apps/storefront/src/constants/index.ts index 81f8a5a95..f824f5e43 100644 --- a/apps/storefront/src/constants/index.ts +++ b/apps/storefront/src/constants/index.ts @@ -42,6 +42,7 @@ export enum HeadlessRoutes { ADDRESSES = '/addresses', USER_MANAGEMENT = '/user-management', ACCOUNT_SETTINGS = '/accountSettings', + COMPANY_HIERARCHY = '/company-hierarchy', INVOICE = '/invoice', CLOSE = 'close', } @@ -76,3 +77,36 @@ const { const CART_FALLBACK_VALUE = platform === 'bigcommerce' ? '/cart.php' : '/cart'; export const CART_URL = cartUrl ?? CART_FALLBACK_VALUE; export const CHECKOUT_URL = '/checkout'; + +export const permissionLevels = { + USER: 1, + COMPANY: 2, + COMPANY_SUBSIDIARIES: 3, +}; + +export const PATH_ROUTES = { + ...HeadlessRoutes, +}; + +export const Z_INDEX: Record< + 'IFRAME' | 'BASE' | 'STICKY' | 'OVERLAY' | 'MODAL' | 'TOOLTIP' | 'NOTIFICATION', + number +> = { + IFRAME: 12000, + BASE: 12001, + STICKY: 12002, + OVERLAY: 12003, + MODAL: 12005, + TOOLTIP: 12004, + NOTIFICATION: 12004, +}; + +export const PAGES_SUBSIDIARIES_PERMISSION_KEYS = [ + { key: 'order', path: HeadlessRoutes.ORDERS }, + { key: 'invoice', path: HeadlessRoutes.INVOICE }, + { key: 'addresses', path: HeadlessRoutes.ADDRESSES }, + { key: 'userManagement', path: HeadlessRoutes.USER_MANAGEMENT }, + { key: 'shoppingLists', path: HeadlessRoutes.SHOPPING_LISTS }, + { key: 'quotes', path: HeadlessRoutes.QUOTES }, + { key: 'companyHierarchy', path: HeadlessRoutes.COMPANY_HIERARCHY }, +] as const; diff --git a/apps/storefront/src/hooks/dom/useDomHooks.ts b/apps/storefront/src/hooks/dom/useDomHooks.ts index 311f21064..6acb50a1c 100644 --- a/apps/storefront/src/hooks/dom/useDomHooks.ts +++ b/apps/storefront/src/hooks/dom/useDomHooks.ts @@ -4,7 +4,7 @@ import { GlobalContext } from '@/shared/global'; import { useAppSelector } from '@/store'; import { CustomerRole } from '@/types'; import { OpenPageState } from '@/types/hooks'; -import { setCartPermissions } from '@/utils/b3RolePermissions'; +import { setCartPermissions } from '@/utils'; import useCartToQuote from './useCartToQuote'; import useHideGoogleCustomerReviews from './useHideGoogleCustomerReviews'; diff --git a/apps/storefront/src/hooks/index.ts b/apps/storefront/src/hooks/index.ts index cc38389b2..d881549a9 100644 --- a/apps/storefront/src/hooks/index.ts +++ b/apps/storefront/src/hooks/index.ts @@ -12,3 +12,4 @@ export { default as useScrollBar } from './useScrollBar'; export { default as useSetOpen } from './useSetOpen'; export { default as useSort } from './useSort'; export { default as useTableRef } from './useTableRef'; +export * from './useVerifyPermission'; diff --git a/apps/storefront/src/hooks/useVerifyPermission.ts b/apps/storefront/src/hooks/useVerifyPermission.ts new file mode 100644 index 000000000..a8e9cc93f --- /dev/null +++ b/apps/storefront/src/hooks/useVerifyPermission.ts @@ -0,0 +1,114 @@ +import { useEffect, useMemo, useState } from 'react'; + +import { permissionLevels } from '@/constants'; +import { useAppSelector } from '@/store'; +import { + levelComparison, + validateBasePermissionWithComparisonType, + ValidatePermissionWithComparisonTypeProps, + VerifyLevelPermissionProps, +} from '@/utils'; + +export const useVerifyLevelPermission = ({ + code, + companyId = 0, + userEmail = '', + userId = 0, +}: VerifyLevelPermissionProps) => { + const [isVerified, setIsVerified] = useState(false); + + const { selectCompanyHierarchyId } = useAppSelector( + ({ company }) => company.companyHierarchyInfo, + ); + const { companyInfo, customer, permissions } = useAppSelector(({ company }) => company); + + useEffect(() => { + const info = permissions.find((permission) => permission.code.includes(code)); + + if (!info) return; + + const { permissionLevel } = info; + + if (!permissionLevel) return; + + setIsVerified( + levelComparison({ + permissionLevel: +permissionLevel, + customer, + companyInfo, + params: { + companyId, + userEmail, + userId, + }, + }), + ); + }, [ + selectCompanyHierarchyId, + code, + companyId, + userEmail, + userId, + companyInfo, + customer, + permissions, + ]); + + return [isVerified]; +}; + +export const useValidatePermissionWithComparisonType = ({ + level = 0, + code = '', + containOrEqual = 'equal', +}: ValidatePermissionWithComparisonTypeProps) => { + const { permissions } = useAppSelector(({ company }) => company); + + const [isValidate, setIsValidate] = useState(false); + + useEffect(() => { + if (!permissions?.length) return; + + const isPermissions = validateBasePermissionWithComparisonType({ + level, + code, + containOrEqual, + permissions, + }); + setIsValidate(isPermissions); + }, [permissions, level, code, containOrEqual]); + + return [isValidate]; +}; + +export const useVerifyCreatePermission = (codes: string[]) => { + const { selectCompanyHierarchyId } = useAppSelector( + ({ company }) => company.companyHierarchyInfo, + ); + + const { permissions } = useAppSelector(({ company }) => company); + const level = useMemo(() => { + return selectCompanyHierarchyId ? permissionLevels.COMPANY_SUBSIDIARIES : permissionLevels.USER; + }, [selectCompanyHierarchyId]); + + const [permissionInfo, setPermissionsInfo] = useState([]); + + useEffect(() => { + if (!permissions?.length) return; + + const info = codes.map((code) => { + const isPermissions = validateBasePermissionWithComparisonType({ + level, + code, + containOrEqual: 'contain', + permissions, + }); + + return isPermissions; + }); + + setPermissionsInfo(info); + }, [permissions, level, codes]); + + return permissionInfo; +}; diff --git a/apps/storefront/src/main.css b/apps/storefront/src/main.css index 078da65e4..836cfc2e6 100644 --- a/apps/storefront/src/main.css +++ b/apps/storefront/src/main.css @@ -11,7 +11,7 @@ height: 100%; position: fixed; overscroll-behavior: contain; - z-index: 9999999999; + z-index: var(--z-index-IFRAME); pointer-events: pointer; } @@ -197,7 +197,7 @@ input[type='number']::-webkit-outer-spin-button { position: fixed; top: 10px; right: 20px; - z-index: 10000; + z-index: var(--z-index-TOOLTIP); min-width: 300px; } @@ -311,7 +311,7 @@ input[type='number']::-webkit-outer-spin-button { top: 0; left: 0; background-color: #fef9f5; - z-index: 99999999995; + z-index: var(--z-index-MODAL); display: flex; justify-content: center; align-items: center; diff --git a/apps/storefront/src/pages/Address/components/AddressItemCard.tsx b/apps/storefront/src/pages/Address/components/AddressItemCard.tsx index b231a031e..e51b7e9f1 100644 --- a/apps/storefront/src/pages/Address/components/AddressItemCard.tsx +++ b/apps/storefront/src/pages/Address/components/AddressItemCard.tsx @@ -21,6 +21,8 @@ export interface OrderItemCardProps { onSetDefault: (data: AddressItemType) => void; editPermission: boolean; isBCPermission: boolean; + updateActionsPermission: boolean; + deleteActionsPermission: boolean; } interface TagBoxProps { @@ -52,6 +54,8 @@ export function AddressItemCard(props: OrderItemCardProps) { onDelete, onSetDefault, editPermission: hasPermission, + updateActionsPermission = false, + deleteActionsPermission = false, isBCPermission, } = props; @@ -111,7 +115,7 @@ export function AddressItemCard(props: OrderItemCardProps) { {hasPermission && ( - {!isBCPermission && ( + {!isBCPermission && updateActionsPermission && ( - { - onEdit(addressInfo); - }} - > - - - { - onDelete(addressInfo); - }} - > - - + {(updateActionsPermission || isBCPermission) && ( + { + onEdit(addressInfo); + }} + > + + + )} + + {(deleteActionsPermission || isBCPermission) && ( + { + onDelete(addressInfo); + }} + > + + + )} )} diff --git a/apps/storefront/src/pages/Address/index.tsx b/apps/storefront/src/pages/Address/index.tsx index 96b833f20..b9f0b127c 100644 --- a/apps/storefront/src/pages/Address/index.tsx +++ b/apps/storefront/src/pages/Address/index.tsx @@ -1,11 +1,11 @@ -import { useContext, useEffect, useRef, useState } from 'react'; +import { useContext, useEffect, useMemo, useRef, useState } from 'react'; import { useB3Lang } from '@b3/lang'; import { Box } from '@mui/material'; import B3Filter from '@/components/filter/B3Filter'; import B3Spin from '@/components/spin/B3Spin'; import { B3PaginationTable } from '@/components/table/B3PaginationTable'; -import { useCardListColumn, useTableRef } from '@/hooks'; +import { useCardListColumn, useTableRef, useVerifyCreatePermission } from '@/hooks'; import { GlobalContext } from '@/shared/global'; import { getB2BAddress, @@ -13,9 +13,9 @@ import { getB2BCountries, getBCCustomerAddress, } from '@/shared/service/b2b'; -import { isB2BUserSelector, rolePermissionSelector, useAppSelector } from '@/store'; +import { isB2BUserSelector, useAppSelector } from '@/store'; import { CustomerRole } from '@/types'; -import { snackbar } from '@/utils'; +import { b2bPermissionsMap, snackbar } from '@/utils'; import b2bLogger from '@/utils/b3Logger'; import { AddressConfigItem, AddressItemType, BCAddressItemType } from '../../types/address'; @@ -27,6 +27,11 @@ import SetDefaultDialog from './components/SetDefaultDialog'; import { convertBCToB2BAddress, filterFormConfig } from './shared/config'; import { CountryProps, getAddressFields } from './shared/getAddressFields'; +const permissionKeys = [ + b2bPermissionsMap.addressesCreateActionsPermission, + b2bPermissionsMap.addressesUpdateActionsPermission, + b2bPermissionsMap.addressesDeleteActionsPermission, +]; interface RefCurrentProps extends HTMLInputElement { handleOpenAddEditAddressClick: (type: string, data?: AddressItemType) => void; } @@ -53,7 +58,9 @@ function Address() { dispatch, } = useContext(GlobalContext); - const { addressesActionsPermission } = useAppSelector(rolePermissionSelector); + const { selectCompanyHierarchyId } = useAppSelector( + ({ company }) => company.companyHierarchyInfo, + ); const b3Lang = useB3Lang(); const isExtraLarge = useCardListColumn(); @@ -70,12 +77,8 @@ function Address() { const companyId = role === CustomerRole.SUPER_ADMIN && isAgenting ? salesRepCompanyId : companyInfoId; - let hasAdminPermission = false; - let isBCPermission = false; - if (isB2BUser && (!role || (role === CustomerRole.SUPER_ADMIN && isAgenting))) { - hasAdminPermission = true; - } + let isBCPermission = false; if (!isB2BUser || (role === CustomerRole.SUPER_ADMIN && !isAgenting)) { isBCPermission = true; @@ -156,20 +159,22 @@ function Address() { paginationTableRef.current?.refresh(); }; - const [editPermission, setEditPermission] = useState( - isB2BUser ? addressesActionsPermission : false, - ); + const [editPermission, setEditPermission] = useState(false); const [isOpenSetDefault, setIsOpenSetDefault] = useState(false); const [isOpenDelete, setIsOpenDelete] = useState(false); const [currentAddress, setCurrentAddress] = useState(); + const [isCreatePermission, updateActionsPermission, deleteActionsPermission] = + useVerifyCreatePermission(permissionKeys); + useEffect(() => { const getEditPermission = async () => { if (isBCPermission) { setEditPermission(true); return; } - if (hasAdminPermission) { + + if (updateActionsPermission) { try { let configList = addressConfig; if (!configList) { @@ -190,8 +195,7 @@ function Address() { (configList || []).find((config: AddressConfigItem) => config.key === 'address_book') ?.isEnabled === '1' && (configList || []).find((config: AddressConfigItem) => config.key === key) - ?.isEnabled === '1' && - addressesActionsPermission; + ?.isEnabled === '1'; setEditPermission(editPermission); } catch (error) { @@ -202,7 +206,7 @@ function Address() { getEditPermission(); // Disabling the next line as dispatch is not required to be in the dependency array // eslint-disable-next-line react-hooks/exhaustive-deps - }, [addressConfig, hasAdminPermission, isBCPermission, role]); + }, [addressConfig, updateActionsPermission, isBCPermission, role, selectCompanyHierarchyId]); const handleCreate = () => { if (!editPermission) { @@ -238,10 +242,15 @@ function Address() { setIsOpenSetDefault(true); }; - const AddButtonConfig = { - isEnabled: editPermission, - customLabel: b3Lang('addresses.addNewAddress'), - }; + const AddButtonConfig = useMemo(() => { + return { + isEnabled: isBCPermission || (editPermission && isCreatePermission), + customLabel: b3Lang('addresses.addNewAddress'), + }; + + // ignore b3Lang due it's function that doesn't not depend on any reactive value + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [editPermission, selectCompanyHierarchyId, isCreatePermission]); const translatedFilterFormConfig = JSON.parse(JSON.stringify(filterFormConfig)); @@ -252,6 +261,8 @@ function Address() { return element; }); + const currentUseCompanyHierarchyId = +selectCompanyHierarchyId || +companyId; + return ( )} @@ -294,7 +307,7 @@ function Address() { updateAddressList={updateAddressList} addressFields={addressFields} ref={addEditAddressRef} - companyId={companyId} + companyId={currentUseCompanyHierarchyId} isBCPermission={isBCPermission} countries={countries} /> @@ -306,7 +319,7 @@ function Address() { setIsLoading={setIsRequestLoading} addressData={currentAddress} updateAddressList={updateAddressList} - companyId={companyId} + companyId={currentUseCompanyHierarchyId} /> )} {editPermission && ( @@ -316,7 +329,7 @@ function Address() { setIsLoading={setIsRequestLoading} addressData={currentAddress} updateAddressList={updateAddressList} - companyId={companyId} + companyId={currentUseCompanyHierarchyId} isBCPermission={isBCPermission} /> )} diff --git a/apps/storefront/src/pages/CompanyHierarchy/components/CompanyTableRowCard.tsx b/apps/storefront/src/pages/CompanyHierarchy/components/CompanyTableRowCard.tsx new file mode 100644 index 000000000..4f83c3c8e --- /dev/null +++ b/apps/storefront/src/pages/CompanyHierarchy/components/CompanyTableRowCard.tsx @@ -0,0 +1,160 @@ +import { useContext, useMemo, useState } from 'react'; +import { useB3Lang } from '@b3/lang'; +import { Business as BusinessIcon, MoreHoriz as MoreHorizIcon } from '@mui/icons-material'; +import { Box, Card, Chip, IconButton, Menu, MenuItem } from '@mui/material'; + +import { CustomStyleContext } from '@/shared/customStyleButton'; + +import { RecursiveNode, TreeNodeProps } from './types'; + +interface CompanyTableRowCardProps { + company: RecursiveNode; + currentCompanyId?: string | number; + selectCompanyId?: string | number; + onSwitchCompany?: (node: T) => void; + getDisplayName?: (node: T) => string; + getNodeId?: (node: T) => string | number; +} + +function CompanyTableRowCard({ + company, + currentCompanyId = '', + selectCompanyId = '', + onSwitchCompany, + getDisplayName = (node) => node.companyName, + getNodeId = (node) => node.companyId, +}: CompanyTableRowCardProps) { + const nodeId = getNodeId(company); + const [anchorEl, setAnchorEl] = useState(null); + const b3Lang = useB3Lang(); + const { + state: { + switchAccountButton: { color = '#ED6C02' }, + }, + } = useContext(CustomStyleContext); + const isCurrentCompanyId = +nodeId === +currentCompanyId; + const isSelectCompanyId = +nodeId === +selectCompanyId; + + const open = Boolean(anchorEl); + const isDisabledAction = useMemo(() => { + if (selectCompanyId) { + return +selectCompanyId !== +company.companyId; + } + + return +currentCompanyId !== +company.companyId; + }, [currentCompanyId, selectCompanyId, company]); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleSwitchClick = () => { + handleClose(); + onSwitchCompany?.(company); + }; + + const openIcon = open + ? { + borderRadius: '10%', + backgroundColor: 'rgba(0, 0, 0, 0.14)', + } + : {}; + + return ( + + + + + + {getDisplayName(company)} + + {company?.channelFlag && isDisabledAction && ( + + + + )} + + {isSelectCompanyId && ( + + )} + {isCurrentCompanyId && ( + + )} + + + + {b3Lang('companyHierarchy.dialog.title')} + + + + ); +} + +export default CompanyTableRowCard; diff --git a/apps/storefront/src/pages/CompanyHierarchy/components/HierarchyDialog.tsx b/apps/storefront/src/pages/CompanyHierarchy/components/HierarchyDialog.tsx new file mode 100644 index 000000000..30883ac0e --- /dev/null +++ b/apps/storefront/src/pages/CompanyHierarchy/components/HierarchyDialog.tsx @@ -0,0 +1,163 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useB3Lang } from '@b3/lang'; +import { Box } from '@mui/material'; +import Cookies from 'js-cookie'; + +import B3Dialog from '@/components/B3Dialog'; +import { PAGES_SUBSIDIARIES_PERMISSION_KEYS } from '@/constants'; +import { endUserMasqueradingCompany, startUserMasqueradingCompany } from '@/shared/service/b2b'; +import { deleteCart } from '@/shared/service/bc/graphql/cart'; +import { store, useAppSelector } from '@/store'; +import { setCompanyHierarchyInfoModules } from '@/store/slices/company'; +import { setCartNumber } from '@/store/slices/global'; +import { + CompanyHierarchyListProps, + CompanyHierarchyProps, + PagesSubsidiariesPermissionProps, +} from '@/types'; +import { buildHierarchy, flattenBuildHierarchyCompanies } from '@/utils'; +import b2bLogger from '@/utils/b3Logger'; +import { deleteCartData } from '@/utils/cartUtils'; + +interface HierarchyDialogProps { + open: boolean; + handleClose: () => void; + currentRow: Partial | null; + companyHierarchyAllList?: CompanyHierarchyListProps[] | []; + title?: string; + context?: string; + dialogParams?: { [key: string]: string }; +} +function HierarchyDialog({ + open = false, + handleClose, + currentRow, + companyHierarchyAllList, + title, + context, + dialogParams = {}, +}: HierarchyDialogProps) { + const b3Lang = useB3Lang(); + const navigate = useNavigate(); + + const { id: currentCompanyId } = useAppSelector(({ company }) => company.companyInfo); + + const { pagesSubsidiariesPermission } = useAppSelector(({ company }) => company); + + const { isHasCurrentPagePermission, companyHierarchyAllList: allList } = useAppSelector( + ({ company }) => company.companyHierarchyInfo, + ); + + const [loading, setLoading] = useState(false); + + const handleSwitchCompanyClick = async () => { + if (!currentRow) return; + try { + setLoading(true); + + const cartEntityId = Cookies.get('cartId'); + + const { companyId } = currentRow; + + if (!companyId) return; + + if (companyId === +currentCompanyId) { + await endUserMasqueradingCompany(); + } else if (companyId) { + await startUserMasqueradingCompany(+companyId); + } + + if (cartEntityId) { + const deleteCartObject = deleteCartData(cartEntityId); + + await deleteCart(deleteCartObject); + + store.dispatch(setCartNumber(0)); + } + + const selectCompanyHierarchyId = companyId === +currentCompanyId ? '' : companyId; + + const buildData = companyHierarchyAllList || allList; + + store.dispatch( + setCompanyHierarchyInfoModules({ + selectCompanyHierarchyId, + ...(companyHierarchyAllList && { companyHierarchyAllList }), + companyHierarchySelectSubsidiariesList: flattenBuildHierarchyCompanies( + buildHierarchy({ + data: buildData, + companyId, + })[0], + ), + }), + ); + + if (companyId === +currentCompanyId) { + const { hash } = window.location; + if (hash.includes('/shoppingList/')) { + navigate('/shoppingLists'); + } + } + + if (companyId !== +currentCompanyId && !isHasCurrentPagePermission) { + const key = Object.keys(pagesSubsidiariesPermission).find((key) => { + return !!pagesSubsidiariesPermission[key as keyof PagesSubsidiariesPermissionProps]; + }); + + const route = PAGES_SUBSIDIARIES_PERMISSION_KEYS.find((item) => item.key === key); + + if (route) { + handleClose(); + setLoading(false); + navigate(route.path); + } + } + } catch (error) { + b2bLogger.error(error); + } finally { + setLoading(false); + + handleClose(); + } + }; + + return ( + + + + {context || b3Lang('companyHierarchy.dialog.content')} + + + + ); +} + +export default HierarchyDialog; diff --git a/apps/storefront/src/pages/CompanyHierarchy/components/TableTree.tsx b/apps/storefront/src/pages/CompanyHierarchy/components/TableTree.tsx new file mode 100644 index 000000000..a30a03548 --- /dev/null +++ b/apps/storefront/src/pages/CompanyHierarchy/components/TableTree.tsx @@ -0,0 +1,297 @@ +import { useContext, useMemo, useState } from 'react'; +import { useB3Lang } from '@b3/lang'; +import { + Business as BusinessIcon, + KeyboardArrowDown as KeyboardArrowDownIcon, + MoreHoriz as MoreHorizIcon, +} from '@mui/icons-material'; +import { + Box, + Chip, + IconButton, + Menu, + MenuItem, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, +} from '@mui/material'; + +import useMobile from '@/hooks/useMobile'; +import { CustomStyleContext } from '@/shared/customStyleButton'; + +import CompanyTableRowCard from './CompanyTableRowCard'; +import { RecursiveNode, TreeNodeProps } from './types'; + +interface CompanyTableProps { + data: RecursiveNode[]; + currentCompanyId?: string | number; + selectCompanyId?: string | number; + onSwitchCompany?: (node: T) => void; + getDisplayName?: (node: T) => string; + getNodeId?: (node: T) => string | number; +} + +interface CompanyTableRowProps { + node: RecursiveNode; + level?: number; + currentCompanyId?: string | number; + selectCompanyId?: string | number; + onSwitchCompany?: (node: T) => void; + getDisplayName?: (node: T) => string; + getNodeId?: (node: T) => string | number; +} + +function CompanyTableRow({ + node, + level = 0, + currentCompanyId = '', + selectCompanyId = '', + onSwitchCompany, + getDisplayName = (node) => node.companyName, + getNodeId = (node) => node.companyId, +}: CompanyTableRowProps) { + const [expanded, setExpanded] = useState(true); + const [anchorEl, setAnchorEl] = useState(null); + + const b3Lang = useB3Lang(); + + const { + state: { + switchAccountButton: { color = '#ED6C02' }, + }, + } = useContext(CustomStyleContext); + + const hasChildren = node.children && node.children.length > 0; + const nodeId = getNodeId(node); + const isCurrentCompanyId = +nodeId === +currentCompanyId; + + const isSelectCompanyId = +nodeId === +selectCompanyId; + const open = Boolean(anchorEl); + + const isDisabledAction = useMemo(() => { + if (selectCompanyId) { + return +selectCompanyId !== +node.companyId; + } + + return +currentCompanyId !== +node.companyId; + }, [currentCompanyId, selectCompanyId, node]); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleSwitchClick = () => { + handleClose(); + onSwitchCompany?.(node); + }; + + return ( + <> + td': { bgcolor: 'background.paper' }, + height: '3.25rem', + }} + > + + + + {hasChildren ? ( + setExpanded(!expanded)} sx={{ mr: 1 }}> + + + ) : ( + + )} + + + + + {getDisplayName(node)} + + {isSelectCompanyId && ( + + )} + {isCurrentCompanyId && ( + + )} + + + + + {node?.channelFlag && isDisabledAction && ( + + + + )} + + + {b3Lang('companyHierarchy.dialog.title')} + + + + + {expanded && + hasChildren && + (node?.children || []).map((child) => ( + + ))} + + ); +} + +function CompanyHierarchyTableTree({ + data, + currentCompanyId, + selectCompanyId, + onSwitchCompany, + getDisplayName = (node) => node.companyName, + getNodeId = (node) => node.companyId, +}: CompanyTableProps) { + const [isMobile] = useMobile(); + const b3Lang = useB3Lang(); + + const handleExpandCompanyData = ( + companies: RecursiveNode[] | [], + companyData: RecursiveNode[], + ) => { + if (companies.length === 0) return companyData; + companies.forEach((company) => { + companyData.push({ + ...company, + children: [], + }); + + const isHasChildren = company.children && company.children.length > 0; + + if (isHasChildren) { + handleExpandCompanyData(company?.children || [], companyData); + } + }); + + return companyData; + }; + const mobileCompanyData = handleExpandCompanyData(data, []); + + return ( + <> + {isMobile ? ( + <> + {mobileCompanyData.map((company) => ( + + ))} + + ) : ( + + + + + + + {b3Lang('companyHierarchy.table.name')} + + + + + + {data.map((company) => ( + + ))} + +
+
+
+ )} + + ); +} + +export default CompanyHierarchyTableTree; diff --git a/apps/storefront/src/pages/CompanyHierarchy/components/types.tsx b/apps/storefront/src/pages/CompanyHierarchy/components/types.tsx new file mode 100644 index 000000000..b30b9adbe --- /dev/null +++ b/apps/storefront/src/pages/CompanyHierarchy/components/types.tsx @@ -0,0 +1,9 @@ +export interface TreeNodeProps { + companyId: string | number; + companyName: string; + channelFlag: boolean; +} + +export type RecursiveNode = T & { + children?: RecursiveNode[]; +}; diff --git a/apps/storefront/src/pages/CompanyHierarchy/index.tsx b/apps/storefront/src/pages/CompanyHierarchy/index.tsx new file mode 100644 index 000000000..c4f2a6164 --- /dev/null +++ b/apps/storefront/src/pages/CompanyHierarchy/index.tsx @@ -0,0 +1,92 @@ +import { useEffect, useRef, useState } from 'react'; +import { Box } from '@mui/material'; + +import B3Spin from '@/components/spin/B3Spin'; +import { getCompanySubsidiaries } from '@/shared/service/b2b'; +import { useAppSelector } from '@/store'; +import { CompanyHierarchyListProps, CompanyHierarchyProps } from '@/types'; +import { buildHierarchy } from '@/utils'; + +import HierarchyDialog from './components/HierarchyDialog'; +import CompanyHierarchyTableTree from './components/TableTree'; + +function CompanyHierarchy() { + const [data, setData] = useState([]); + + const [open, setOpen] = useState(false); + + const [currentRow, setCurrentRow] = useState(null); + + const [loading, setLoading] = useState(false); + + const originDataRef = useRef([]); + + const { id: currentCompanyId } = useAppSelector(({ company }) => company.companyInfo); + + const { selectCompanyHierarchyId } = useAppSelector( + ({ company }) => company.companyHierarchyInfo, + ); + + const init = async () => { + setLoading(true); + + const { companySubsidiaries } = await getCompanySubsidiaries(); + + const list = buildHierarchy({ + data: companySubsidiaries || [], + }); + + originDataRef.current = companySubsidiaries; + + setData(list); + + setLoading(false); + }; + + useEffect(() => { + if (currentCompanyId) { + init(); + } + + // ignore init + // due they are funtions that do not depend on any reactive value + }, [currentCompanyId]); + + const handleClose = () => { + setOpen(false); + }; + + const handleRowClick = (row: CompanyHierarchyProps) => { + setCurrentRow(row); + setOpen(true); + }; + + return ( + + + + data={data} + onSwitchCompany={handleRowClick} + currentCompanyId={currentCompanyId} + selectCompanyId={selectCompanyHierarchyId} + /> + + + + + ); +} + +export default CompanyHierarchy; diff --git a/apps/storefront/src/pages/Invoice/InvoiceItemCard.tsx b/apps/storefront/src/pages/Invoice/InvoiceItemCard.tsx index 7b789b1de..d11ca58a6 100644 --- a/apps/storefront/src/pages/Invoice/InvoiceItemCard.tsx +++ b/apps/storefront/src/pages/Invoice/InvoiceItemCard.tsx @@ -15,13 +15,15 @@ export interface InvoiceItemCardProps { item: any; checkBox?: (disable: boolean) => ReactElement; handleSetSelectedInvoiceAccount: (value: string, id: string) => void; - handleViewInvoice: (id: string, status: string | number) => void; + handleViewInvoice: (id: string, status: string | number, invoiceCompanyId: string) => void; setIsRequestLoading: (bool: boolean) => void; setInvoiceId: (id: string) => void; handleOpenHistoryModal: (bool: boolean) => void; selectedPay: CustomFieldItems | InvoiceListNode[]; handleGetCorrespondingCurrency: (code: string) => string; addBottom: boolean; + isCurrentCompany: boolean; + invoicePay: boolean; } const StyleCheckoutContainer = styled(Box)(() => ({ @@ -43,11 +45,13 @@ export function InvoiceItemCard(props: InvoiceItemCardProps) { selectedPay = [], handleGetCorrespondingCurrency, addBottom, + isCurrentCompany, + invoicePay, } = props; const b3Lang = useB3Lang(); const navigate = useNavigate(); - const { id, status, dueDate, openBalance } = item; + const { id, status, dueDate, openBalance, companyInfo } = item; const currentCode = openBalance.code || 'USD'; const currentCurrencyToken = handleGetCorrespondingCurrency(currentCode); @@ -223,7 +227,7 @@ export function InvoiceItemCard(props: InvoiceItemCardProps) { textDecoration: 'underline', }} onClick={() => { - handleViewInvoice(id, status); + handleViewInvoice(id, status, companyInfo.companyId); }} > {id || '-'} @@ -236,6 +240,8 @@ export function InvoiceItemCard(props: InvoiceItemCardProps) { setInvoiceId={setInvoiceId} handleOpenHistoryModal={handleOpenHistoryModal} setIsRequestLoading={setIsRequestLoading} + isCurrentCompany={isCurrentCompany} + invoicePay={invoicePay} />
diff --git a/apps/storefront/src/pages/Invoice/components/B3Pulldown.tsx b/apps/storefront/src/pages/Invoice/components/B3Pulldown.tsx index 2b2f3cf38..c50f10a76 100644 --- a/apps/storefront/src/pages/Invoice/components/B3Pulldown.tsx +++ b/apps/storefront/src/pages/Invoice/components/B3Pulldown.tsx @@ -7,7 +7,7 @@ import { styled } from '@mui/material/styles'; import { rolePermissionSelector, useAppSelector } from '@/store'; import { InvoiceList } from '@/types/invoice'; -import { snackbar } from '@/utils'; +import { b2bPermissionsMap, snackbar, verifyLevelPermission } from '@/utils'; import { gotoInvoiceCheckoutUrl } from '../utils/payment'; import { getInvoiceDownloadPDFUrl, handlePrintPDF } from '../utils/pdf'; @@ -25,6 +25,8 @@ interface B3PulldownProps { setIsRequestLoading: (bool: boolean) => void; setInvoiceId: (id: string) => void; handleOpenHistoryModal: (bool: boolean) => void; + isCurrentCompany: boolean; + invoicePay: boolean; } function B3Pulldown({ @@ -32,18 +34,23 @@ function B3Pulldown({ setIsRequestLoading, setInvoiceId, handleOpenHistoryModal, + isCurrentCompany, + invoicePay, }: B3PulldownProps) { const platform = useAppSelector(({ global }) => global.storeInfo.platform); const ref = useRef(null); const [isOpen, setIsOpen] = useState(false); - const [isCanPay, setIsCanPay] = useState(true); + const [isPay, setIsPay] = useState(true); const navigate = useNavigate(); const b3Lang = useB3Lang(); - const { getOrderPermission, invoicePayPermission, purchasabilityPermission } = - useAppSelector(rolePermissionSelector); + const { invoicePayPermission, purchasabilityPermission } = useAppSelector(rolePermissionSelector); + const { getOrderPermission: getOrderPermissionCode } = b2bPermissionsMap; + + const [isCanViewOrder, setIsCanViewOrder] = useState(false); + const close = () => { setIsOpen(false); }; @@ -132,11 +139,21 @@ function B3Pulldown({ }; useEffect(() => { - const { openBalance } = row; + const { openBalance, orderUserId, companyInfo } = row; const payPermissions = +openBalance.value > 0 && invoicePayPermission && purchasabilityPermission; - setIsCanPay(payPermissions); + setIsPay(payPermissions); + const isPayInvoice = isCurrentCompany ? payPermissions : payPermissions && invoicePay; + setIsPay(isPayInvoice); + + const viewOrderPermission = verifyLevelPermission({ + code: getOrderPermissionCode, + companyId: +companyInfo.companyId, + userId: +orderUserId, + }); + + setIsCanViewOrder(viewOrderPermission); // disabling as we only need to run this once and values at starting render are good enough // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -174,7 +191,7 @@ function B3Pulldown({ > {b3Lang('invoice.actions.viewInvoice')} - {getOrderPermission && ( + {isCanViewOrder && ( )} - {isCanPay && ( + {isPay && ( company.customer.role); const isAgenting = useAppSelector(({ b2bFeatures }) => b2bFeatures.masqueradeCompany.isAgenting); + const companyInfoId = useAppSelector(({ company }) => company.companyInfo.id); + const { selectCompanyHierarchyId, isEnabledCompanyHierarchy } = useAppSelector( + ({ company }) => company.companyHierarchyInfo, + ); + const salesRepCompanyId = useAppSelector(({ b2bFeatures }) => b2bFeatures.masqueradeCompany.id); + const currentCompanyId = + role === CustomerRole.SUPER_ADMIN && isAgenting ? +salesRepCompanyId : +companyInfoId; + const { invoicePayPermission, purchasabilityPermission } = useAppSelector(rolePermissionSelector); + + const { invoice: invoiceSubViewPermission } = useAppSelector( + ({ company }) => company.pagesSubsidiariesPermission, + ); + const navigate = useNavigate(); const [isMobile] = useMobile(); const paginationTableRef = useRef(null); @@ -91,6 +113,12 @@ function Invoice() { const [filterChangeFlag, setFilterChangeFlag] = useState(false); const [filterLists, setFilterLists] = useState([]); + const [selectAllPay, setSelectAllPay] = useState(invoicePayPermission); + + const invoiceSubPayPermission = validatePermissionWithComparisonType({ + level: permissionLevels.COMPANY_SUBSIDIARIES, + code: b2bPermissionsMap.invoicePayPermission, + }); const { state: { bcLanguage }, @@ -142,6 +170,7 @@ function Invoice() { const { invoiceStats } = await getInvoiceStats( filterData?.status ? +filterData.status : 0, +decimalPlaces, + filterData?.companyIds || [], ); if (invoiceStats) { @@ -208,16 +237,25 @@ function Invoice() { return newItems; }); - setCheckedArr([...checkedItems]); + const newEnableItems = checkedItems.filter( + (item: InvoiceListNode | undefined) => item && !item.node.disableCurrentCheckbox, + ); + setCheckedArr([...newEnableItems]); } else { setCheckedArr([]); } }; - const handleViewInvoice = async (id: string, status: string | number) => { + const handleViewInvoice = async ( + id: string, + status: string | number, + invoiceCompanyId: string, + ) => { try { + const invoicePay = + +invoiceCompanyId === +currentCompanyId ? invoicePayPermission : invoiceSubPayPermission; setIsRequestLoading(true); - const isPayNow = purchasabilityPermission && invoicePayPermission && status !== 2; + const isPayNow = purchasabilityPermission && invoicePay && status !== 2; const pdfUrl = await handlePrintPDF(id, isPayNow); if (!pdfUrl) { @@ -297,6 +335,7 @@ function Invoice() { endDateAt: filterData?.endDateAt || null, status: invoiceStatus, orderBy: orderByFiled, + companyIds: filterData?.companyIds || [], }; const { invoicesExport } = await exportInvoicesAsCSV({ @@ -315,6 +354,10 @@ function Invoice() { }; useEffect(() => { + const newInitFilter = { + ...initFilter, + companyIds: [+selectCompanyHierarchyId || +currentCompanyId], + }; if (location?.search) { const params = new URLSearchParams(location.search); const getInvoiceId = params.get('invoiceId') || ''; @@ -322,7 +365,7 @@ function Invoice() { if (getInvoiceId) { setFilterData({ - ...initFilter, + ...newInitFilter, q: getInvoiceId, }); setType(InvoiceListType.DETAIL); @@ -332,17 +375,32 @@ function Invoice() { // open Successful page setType(InvoiceListType.CHECKOUT); setFilterData({ - ...initFilter, + ...newInitFilter, }); setReceiptId(getReceiptId); } } else { setType(InvoiceListType.NORMAL); setFilterData({ - ...initFilter, + ...newInitFilter, }); } - }, [location]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [location, selectCompanyHierarchyId]); + + const handleSelectCompanies = (company: number[]) => { + const newCompanyIds = company.includes(-1) ? [] : company; + setFilterData({ + ...filterData, + companyIds: newCompanyIds, + }); + + setSelectAllPay( + company.includes(currentCompanyId) || company.includes(-1) + ? invoicePayPermission + : invoiceSubPayPermission, + ); + }; useEffect(() => { const selectedInvoice = @@ -414,6 +472,13 @@ function Invoice() { openBalance.originValue = `${+openBalance.value}`; openBalance.value = formattingNumericValues(+openBalance.value, decimalPlaces); + + item.node.disableCurrentCheckbox = +openBalance.value === 0; + + const { companyInfo } = item.node; + if (+companyInfo.companyId !== +currentCompanyId) { + item.node.disableCurrentCheckbox = !invoiceSubPayPermission || +openBalance.value === 0; + } }); setList(invoicesList); handleStatisticsInvoiceAmount(); @@ -467,7 +532,8 @@ function Invoice() { }, }} onClick={() => { - handleViewInvoice(item.id, item.status); + const companyInfo = item?.companyInfo || {}; + handleViewInvoice(item.id, item.status, companyInfo?.companyId); }} > {item?.invoiceNumber ? item?.invoiceNumber : item?.id} @@ -475,6 +541,17 @@ function Invoice() { ), width: '8%', }, + { + key: 'companyInfo', + title: b3Lang('invoice.headers.companyName'), + isSortable: false, + render: (item: InvoiceList) => { + const { companyName } = item?.companyInfo || {}; + + return {companyName}; + }, + width: '15%', + }, { key: 'orderNumber', title: b3Lang('invoice.headers.order'), @@ -495,14 +572,14 @@ function Invoice() { {item?.orderNumber || '-'} ), - width: '8%', + width: '12%', }, { key: 'createdAt', title: b3Lang('invoice.headers.invoiceDate'), isSortable: true, render: (item: InvoiceList) => `${item.createdAt ? displayFormat(+item.createdAt) : '–'}`, - width: '10%', + width: '15%', }, { key: 'updatedAt', @@ -523,7 +600,7 @@ function Invoice() { ); }, - width: '10%', + width: '15%', }, { key: 'originalBalance', @@ -640,7 +717,7 @@ function Invoice() { key: 'companyName', title: b3Lang('invoice.headers.action'), render: (row: InvoiceList) => { - const { id } = row; + const { id, companyInfo } = row; let actionRow = row; if (selectedPay.length > 0) { const currentSelected = selectedPay.find((item: InvoiceListNode) => { @@ -662,6 +739,12 @@ function Invoice() { setInvoiceId={setCurrentInvoiceId} handleOpenHistoryModal={setIsOpenHistory} setIsRequestLoading={setIsRequestLoading} + isCurrentCompany={+currentCompanyId === +companyInfo.companyId} + invoicePay={ + +currentCompanyId === +companyInfo.companyId + ? invoicePayPermission + : invoiceSubPayPermission + } /> ); }, @@ -716,6 +799,7 @@ function Invoice() { - div': { + width: isMobile ? '100%' : 'auto', + }, }} - searchValue={filterData?.q || ''} - /> + > + {isEnabledCompanyHierarchy && invoiceSubViewPermission && ( + + + + )} + + )} /> @@ -842,9 +959,9 @@ function Invoice() { )} - {selectedPay.length > 0 && (role === 0 || isAgenting) && ( - - )} + {selectedPay.length > 0 && + (((invoicePayPermission || invoiceSubPayPermission) && purchasabilityPermission) || + isAgenting) && } company.companyHierarchyInfo, + ); + const { state: { loginPageButton, @@ -140,6 +148,10 @@ export default function Login(props: PageProps) { if (isAgenting) { await endMasquerade(); } + + if (selectCompanyHierarchyId) { + await endUserMasqueradingCompany(); + } } catch (e) { b2bLogger.error(e); } finally { @@ -157,7 +169,7 @@ export default function Login(props: PageProps) { }; logout(); - }, [b3Lang, endMasquerade, isLoggedIn, isAgenting, searchParams]); + }, [b3Lang, endMasquerade, isLoggedIn, isAgenting, searchParams, selectCompanyHierarchyId]); const tipInfo = (loginFlag?: LoginFlagType, email = '') => { if (!loginFlag) return ''; @@ -265,27 +277,13 @@ export default function Login(props: PageProps) { if (!isLoginLandLocation) return; - const { getShoppingListPermission, getOrderPermission } = getB3PermissionsList(); - if ( - info?.role === CustomerRole.JUNIOR_BUYER && - info?.companyRoleName === 'Junior Buyer' - ) { - const currentJuniorActivePage = getShoppingListPermission - ? '/shoppingLists' - : '/accountSettings'; - - navigate(currentJuniorActivePage); - } else { - let currentActivePage = getOrderPermission ? '/orders' : '/shoppingLists'; + if (info?.userType === UserTypes.B2C) { + navigate(PATH_ROUTES.ORDERS); + } - currentActivePage = - getShoppingListPermission || getOrderPermission - ? currentActivePage - : '/accountSettings'; + const path = b2bJumpPath(info?.role); - currentActivePage = info?.userType === UserTypes.B2C ? '/orders' : currentActivePage; - navigate(currentActivePage); - } + navigate(path); } } catch (error) { snackbar.error(b3Lang('login.loginTipInfo.accountIncorrect')); diff --git a/apps/storefront/src/pages/OrderDetail/components/OrderAction.tsx b/apps/storefront/src/pages/OrderDetail/components/OrderAction.tsx index a800ddfa2..f9c8a3335 100644 --- a/apps/storefront/src/pages/OrderDetail/components/OrderAction.tsx +++ b/apps/storefront/src/pages/OrderDetail/components/OrderAction.tsx @@ -6,17 +6,20 @@ import { Box, Card, CardContent, Divider, Typography } from '@mui/material'; import throttle from 'lodash-es/throttle'; import CustomButton from '@/components/button/CustomButton'; +import HierarchyDialog from '@/pages/CompanyHierarchy/components/HierarchyDialog'; import { GlobalContext } from '@/shared/global'; import { isB2BUserSelector, rolePermissionSelector, useAppSelector } from '@/store'; +import { Address, MoneyFormat, OrderProductItem } from '@/types'; import { b2bPrintInvoice, currencyFormat, displayFormat, ordersCurrencyFormat, snackbar, + verifyLevelPermission, } from '@/utils'; +import { b2bPermissionsMap } from '@/utils/b3CheckPermissions/config'; -import { Address, MoneyFormat, OrderProductItem } from '../../../types'; import { OrderDetailsContext, OrderDetailsState } from '../context/OrderDetailsContext'; import OrderDialog from './OrderDialog'; @@ -89,6 +92,8 @@ interface OrderCardProps { role: number | string; ipStatus: number; invoiceId?: number | string | undefined | null; + isCurrentCompany: boolean; + switchCompanyId: number | string | undefined; } interface DialogData { @@ -110,6 +115,8 @@ function OrderCard(props: OrderCardProps) { role, invoiceId, ipStatus, + isCurrentCompany, + switchCompanyId, } = props; const displayAsNegativeNumber = ['coupon', 'discountAmount']; const b3Lang = useB3Lang(); @@ -139,6 +146,7 @@ function OrderCard(props: OrderCardProps) { const navigate = useNavigate(); + const [openSwitchCompany, setOpenSwitchCompany] = useState(false); const [open, setOpen] = useState(false); const [type, setType] = useState(''); const [currentDialogData, setCurrentDialogData] = useState(); @@ -153,6 +161,16 @@ function OrderCard(props: OrderCardProps) { infoValue = Object.values(info); } + const handleShowSwitchCompanyModal = () => { + if (!isCurrentCompany && switchCompanyId) { + setOpenSwitchCompany(true); + + return true; + } + + return false; + }; + const handleOpenDialog = (name: string) => { if (name === 'viewInvoice') { if (ipStatus !== 0) { @@ -163,6 +181,8 @@ function OrderCard(props: OrderCardProps) { } else if (name === 'printInvoice') { window.open(`/account.php?action=print_invoice&order_id=${orderId}`); } else { + const isNeedSwitch = handleShowSwitchCompanyModal(); + if (isNeedSwitch) return; if (!isAgenting && +role === 3) { snackbar.error(b3Lang('orderDetail.orderCard.errorMasquerade')); return; @@ -270,12 +290,27 @@ function OrderCard(props: OrderCardProps) { itemKey={itemKey} orderId={+orderId} /> + + setOpenSwitchCompany(false)} + // loading + title={b3Lang('orderDetail.switchCompany.title')} + context={b3Lang('orderDetail.switchCompany.content.tipsText')} + dialogParams={{ + rightSizeBtn: b3Lang('global.B2BSwitchCompanyModal.confirm.button'), + }} + /> ); } interface OrderActionProps { detailsData: OrderDetailsState; + isCurrentCompany: boolean; } interface OrderData { @@ -287,7 +322,7 @@ interface OrderData { } export default function OrderAction(props: OrderActionProps) { - const { detailsData } = props; + const { detailsData, isCurrentCompany } = props; const b3Lang = useB3Lang(); const isB2BUser = useAppSelector(isB2BUserSelector); const emailAddress = useAppSelector(({ company }) => company.customer.emailAddress); @@ -311,6 +346,8 @@ export default function OrderAction(props: OrderActionProps) { ipStatus = 0, invoiceId, poNumber, + customerId, + companyInfo: { companyId } = {}, } = detailsData; const getPaymentMessage = useCallback(() => { @@ -334,8 +371,13 @@ export default function OrderAction(props: OrderActionProps) { return null; } - const { purchasabilityPermission, shoppingListActionsPermission, getInvoicesPermission } = - b2bPermissions; + const { purchasabilityPermission, shoppingListCreateActionsPermission } = b2bPermissions; + const { getInvoicesPermission } = b2bPermissionsMap; + const invoiceViewPermission = verifyLevelPermission({ + code: getInvoicesPermission, + companyId: companyId ? +companyId : 0, + userId: customerId ? +customerId : 0, + }); const getCompanyName = (company: string) => { if (addressLabelPermission) { @@ -426,7 +468,7 @@ export default function OrderAction(props: OrderActionProps) { name: 'shoppingList', variant: 'outlined', isCanShow: isB2BUser - ? shoppingListActionsPermission && shoppingListEnabled + ? shoppingListCreateActionsPermission && shoppingListEnabled : shoppingListEnabled, }, ]; @@ -461,7 +503,7 @@ export default function OrderAction(props: OrderActionProps) { name: isB2BUser ? 'viewInvoice' : 'printInvoice', variant: 'outlined', isCanShow: isB2BUser - ? invoiceBtnPermissions && getInvoicesPermission + ? invoiceBtnPermissions && invoiceViewPermission : invoiceBtnPermissions, }, ], @@ -493,6 +535,8 @@ export default function OrderAction(props: OrderActionProps) { ipStatus={ipStatus} invoiceId={invoiceId} key={item.key} + isCurrentCompany={isCurrentCompany} + switchCompanyId={companyId} /> ))} diff --git a/apps/storefront/src/pages/OrderDetail/components/OrderBilling.tsx b/apps/storefront/src/pages/OrderDetail/components/OrderBilling.tsx index beb1b8ade..26248d94e 100644 --- a/apps/storefront/src/pages/OrderDetail/components/OrderBilling.tsx +++ b/apps/storefront/src/pages/OrderDetail/components/OrderBilling.tsx @@ -7,7 +7,11 @@ import { useMobile } from '@/hooks'; import { OrderBillings } from '../../../types'; import { OrderDetailsContext } from '../context/OrderDetailsContext'; -export default function OrderBilling() { +type OrderBillingProps = { + isCurrentCompany: boolean; +}; + +export default function OrderBilling({ isCurrentCompany }: OrderBillingProps) { const { state: { billings = [], addressLabelPermission, orderId, orderIsDigital }, } = useContext(OrderDetailsContext); @@ -84,7 +88,7 @@ export default function OrderBilling() { diff --git a/apps/storefront/src/pages/OrderDetail/components/OrderShipping.tsx b/apps/storefront/src/pages/OrderDetail/components/OrderShipping.tsx index 591e1daf4..ad5d4af5d 100644 --- a/apps/storefront/src/pages/OrderDetail/components/OrderShipping.tsx +++ b/apps/storefront/src/pages/OrderDetail/components/OrderShipping.tsx @@ -16,7 +16,11 @@ const ShipmentTitle = styled('span')(() => ({ color: '#313440', })); -export default function OrderShipping() { +type OrderShippingProps = { + isCurrentCompany: boolean; +}; + +export default function OrderShipping({ isCurrentCompany }: OrderShippingProps) { const { state: { shippings = [], addressLabelPermission, orderIsDigital, money }, } = useContext(OrderDetailsContext); @@ -158,8 +162,8 @@ export default function OrderShipping() { products={shipment.itemsInfo} money={money} totalText="Total" - canToProduct - textAlign={isMobile ? 'left' : 'right'} + canToProduct={isCurrentCompany} + textAlign="right" /> ) : null, @@ -182,7 +186,7 @@ export default function OrderShipping() { products={shipping.notShip.itemsInfo} money={money} totalText="Total" - canToProduct + canToProduct={isCurrentCompany} textAlign={isMobile ? 'left' : 'right'} /> diff --git a/apps/storefront/src/pages/OrderDetail/context/OrderDetailsContext.tsx b/apps/storefront/src/pages/OrderDetail/context/OrderDetailsContext.tsx index 71435ba39..d57596f52 100644 --- a/apps/storefront/src/pages/OrderDetail/context/OrderDetailsContext.tsx +++ b/apps/storefront/src/pages/OrderDetail/context/OrderDetailsContext.tsx @@ -1,6 +1,7 @@ import { createContext, Dispatch, ReactNode, useMemo, useReducer } from 'react'; import { + CompanyInfoTypes, MoneyFormat, OrderBillings, OrderHistoryItem, @@ -34,6 +35,8 @@ export interface OrderDetailsState { canReturn?: boolean; createdEmail?: string; orderIsDigital?: boolean; + companyInfo?: CompanyInfoTypes; + customerId?: number; } interface OrderDetailsAction { type: string; @@ -87,6 +90,17 @@ const initState = { canReturn: false, createdEmail: '', orderIsDigital: false, + companyInfo: { + companyId: '', + companyName: '', + companyAddress: '', + companyCountry: '', + companyState: '', + companyCity: '', + companyZipCode: '', + phoneNumber: '', + bcId: '', + }, }; export const OrderDetailsContext = createContext({ diff --git a/apps/storefront/src/pages/OrderDetail/index.tsx b/apps/storefront/src/pages/OrderDetail/index.tsx index e0410d576..6e1818ba8 100644 --- a/apps/storefront/src/pages/OrderDetail/index.tsx +++ b/apps/storefront/src/pages/OrderDetail/index.tsx @@ -1,7 +1,7 @@ import { useContext, useEffect, useState } from 'react'; import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { useB3Lang } from '@b3/lang'; -import { ArrowBackIosNew } from '@mui/icons-material'; +import { ArrowBackIosNew, InfoOutlined } from '@mui/icons-material'; import { Box, Grid, Stack, Typography } from '@mui/material'; import { b3HexToRgb, getContrastColor } from '@/components/outSideComponents/utils/b3CustomStyles'; @@ -17,9 +17,9 @@ import { getOrderStatusType, } from '@/shared/service/b2b'; import { isB2BUserSelector, useAppSelector } from '@/store'; +import { AddressConfigItem, CustomerRole, OrderProductItem, OrderStatusItem } from '@/types'; import b2bLogger from '@/utils/b3Logger'; -import { AddressConfigItem, OrderProductItem, OrderStatusItem } from '../../types'; import OrderStatus from '../order/components/OrderStatus'; import { orderStatusTranslationVariables } from '../order/shared/getOrderStatus'; @@ -41,6 +41,17 @@ interface LocationState { function OrderDetail() { const isB2BUser = useAppSelector(isB2BUserSelector); + const role = useAppSelector(({ company }) => company.customer.role); + const isAgenting = useAppSelector(({ b2bFeatures }) => b2bFeatures.masqueradeCompany.isAgenting); + + const companyInfoId = useAppSelector(({ company }) => company.companyInfo.id); + const { selectCompanyHierarchyId } = useAppSelector( + ({ company }) => company.companyHierarchyInfo, + ); + const salesRepCompanyId = useAppSelector(({ b2bFeatures }) => b2bFeatures.masqueradeCompany.id); + const companyId = + role === CustomerRole.SUPER_ADMIN && isAgenting ? +salesRepCompanyId : +companyInfoId; + const currentCompanyId = +selectCompanyHierarchyId || companyId; const params = useParams(); @@ -54,7 +65,7 @@ function OrderDetail() { } = useContext(GlobalContext); const { - state: { poNumber, status = '', customStatus, orderSummary, orderStatus = [] }, + state: { poNumber, status = '', customStatus, orderSummary, orderStatus = [], products }, state: detailsData, dispatch, } = useContext(OrderDetailsContext); @@ -67,21 +78,20 @@ function OrderDetail() { const customColor = getContrastColor(backgroundColor); - const localtion = useLocation(); + const location = useLocation(); const [isMobile] = useMobile(); const [preOrderId, setPreOrderId] = useState(''); const [orderId, setOrderId] = useState(''); const [isRequestLoading, setIsRequestLoading] = useState(false); + const [isCurrentCompany, setIsCurrentCompany] = useState(false); useEffect(() => { setOrderId(params.id || ''); }, [params]); const goToOrders = () => { - navigate( - `${(localtion.state as LocationState).isCompanyOrder ? '/company-orders' : '/orders'}`, - ); + navigate(`${(location.state as LocationState).isCompanyOrder ? '/company-orders' : '/orders'}`); }; useEffect(() => { @@ -98,7 +108,7 @@ function OrderDetail() { const order = isB2BUser ? await getB2BOrderDetails(id) : await getBCOrderDetails(id); if (order) { - const { products } = order; + const { products, companyInfo } = order; const newOrder = { ...order, @@ -110,6 +120,8 @@ function OrderDetail() { }), }; + setIsCurrentCompany(+companyInfo.companyId === +currentCompanyId); + const data = isB2BUser ? convertB2BOrderDetails(newOrder, b3Lang) : convertBCOrderDetails(newOrder, b3Lang); @@ -146,7 +158,7 @@ function OrderDetail() { } // Disabling rule since dispatch does not need to be in the dep array and b3Lang has rendering errors // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isB2BUser, orderId, preOrderId]); + }, [isB2BUser, orderId, preOrderId, selectCompanyHierarchyId]); const handlePageChange = (orderId: string | number) => { setOrderId(orderId.toString()); @@ -225,7 +237,7 @@ function OrderDetail() { }} onClick={goToOrders} > - {localtion.state !== null ? ( + {location.state !== null ? ( <> - {localtion?.state && ( + {location?.state && ( handlePageChange(orderId)} color={customColor} @@ -283,6 +295,35 @@ function OrderDetail() { )} + {products?.length && !isCurrentCompany ? ( + + + + {b3Lang('orderDetail.anotherCompany.tips')} + + + ) : null} - + {/* Digital Order Display */} - + @@ -328,7 +369,7 @@ function OrderDetail() { } > {JSON.stringify(orderSummary) === '{}' ? null : ( - + )} diff --git a/apps/storefront/src/pages/OrderDetail/shared/B2BOrderData.ts b/apps/storefront/src/pages/OrderDetail/shared/B2BOrderData.ts index e35545a49..d65c8e581 100644 --- a/apps/storefront/src/pages/OrderDetail/shared/B2BOrderData.ts +++ b/apps/storefront/src/pages/OrderDetail/shared/B2BOrderData.ts @@ -221,6 +221,7 @@ const convertB2BOrderDetails = (data: B2BOrderData, b3Lang: LangFormatFunction) canReturn: data.canReturn, createdEmail: data.createdEmail, orderIsDigital: data.orderIsDigital, + companyInfo: data.companyInfo, }); export default convertB2BOrderDetails; diff --git a/apps/storefront/src/pages/QuoteDetail/index.tsx b/apps/storefront/src/pages/QuoteDetail/index.tsx index feef83eef..91b3b561c 100644 --- a/apps/storefront/src/pages/QuoteDetail/index.tsx +++ b/apps/storefront/src/pages/QuoteDetail/index.tsx @@ -26,7 +26,8 @@ import { } from '@/store'; import { Currency } from '@/types'; import { QuoteExtraFieldsData } from '@/types/quotes'; -import { snackbar } from '@/utils'; +import { snackbar, verifyLevelPermission } from '@/utils'; +import { b2bPermissionsMap } from '@/utils/b3CheckPermissions/config'; import { getVariantInfoOOSAndPurchase } from '@/utils/b3Product/b3Product'; import { conversionProductsList } from '@/utils/b3Product/shared/config'; import { getSearchVal } from '@/utils/loginInfo'; @@ -57,20 +58,13 @@ function QuoteDetail() { const emailAddress = useAppSelector(({ company }) => company.customer.emailAddress); const customerGroupId = useAppSelector(({ company }) => company.customer.customerGroupId); const role = useAppSelector(({ company }) => company.customer.role); + const { selectCompanyHierarchyId } = useAppSelector( + ({ company }) => company.companyHierarchyInfo, + ); const isAgenting = useAppSelector(({ b2bFeatures }) => b2bFeatures.masqueradeCompany.isAgenting); const [isMobile] = useMobile(); - const { - quoteConvertToOrderPermission: quoteConvertToOrderPermissionRename, - purchasabilityPermission, - } = useAppSelector(rolePermissionSelector); - - const quoteConvertToOrderPermission = isB2BUser - ? quoteConvertToOrderPermissionRename - : +role !== 2; - const quotePurchasabilityPermission = isB2BUser ? purchasabilityPermission : +role !== 2; - const b3Lang = useB3Lang(); const [quoteDetail, setQuoteDetail] = useState({}); @@ -95,7 +89,12 @@ function QuoteDetail() { nonPurchasable: '', }); - const [quoteCheckoutLoadding, setQuoteCheckoutLoadding] = useState(false); + const [quotePurchasabilityPermissionInfo, setQuotePurchasabilityPermission] = useState({ + quotePurchasabilityPermission: false, + quoteConvertToOrderPermission: false, + }); + + const [quoteCheckoutLoading, setQuoteCheckoutLoading] = useState(false); const location = useLocation(); const currency = useAppSelector(activeCurrencyInfoSelector); @@ -107,6 +106,42 @@ function QuoteDetail() { ({ global }) => global.blockPendingQuoteNonPurchasableOOS?.isEnableProduct, ); + const { purchasabilityPermission } = useAppSelector(rolePermissionSelector); + + useEffect(() => { + if (!quoteDetail?.id) return; + + const { quoteConvertToOrderPermission: quoteCheckoutPermissionCode } = b2bPermissionsMap; + + const getPurchasabilityAndConvertToOrderPermission = () => { + if (isB2BUser) { + const companyId = quoteDetail?.companyId?.id || null; + const userEmail = quoteDetail?.contactInfo?.email || ''; + return { + quotePurchasabilityPermission: purchasabilityPermission, + quoteConvertToOrderPermission: verifyLevelPermission({ + code: quoteCheckoutPermissionCode, + companyId, + userEmail, + }), + }; + } + + return { + quotePurchasabilityPermission: true, + quoteConvertToOrderPermission: true, + }; + }; + + const { quotePurchasabilityPermission, quoteConvertToOrderPermission } = + getPurchasabilityAndConvertToOrderPermission(); + + setQuotePurchasabilityPermission({ + quotePurchasabilityPermission, + quoteConvertToOrderPermission, + }); + }, [isB2BUser, quoteDetail, selectCompanyHierarchyId, purchasabilityPermission]); + useEffect(() => { let oosErrorList = ''; let nonPurchasableErrorList = ''; @@ -499,7 +534,7 @@ function QuoteDetail() { const quoteGotoCheckout = async () => { try { - setQuoteCheckoutLoadding(true); + setQuoteCheckoutLoading(true); await handleQuoteCheckout({ quoteId: id, proceedingCheckoutFn, @@ -508,7 +543,7 @@ function QuoteDetail() { navigate, }); } finally { - setQuoteCheckoutLoadding(false); + setQuoteCheckoutLoading(false); } }; useEffect(() => { @@ -558,8 +593,11 @@ function QuoteDetail() { useScrollBar(false); + const { quotePurchasabilityPermission, quoteConvertToOrderPermission } = + quotePurchasabilityPermissionInfo; + return ( - + quoteInfo.draftQuoteInfo); const currency = useAppSelector(activeCurrencyInfoSelector); - const b2bPermissions = useAppSelector(rolePermissionSelector); const quoteSubmissionResponseInfo = useAppSelector( ({ global }) => global.quoteSubmissionResponse, ); + const { selectCompanyHierarchyId } = useAppSelector( + ({ company }) => company.companyHierarchyInfo, + ); const isEnableProduct = useAppSelector( ({ global }) => global.blockPendingQuoteNonPurchasableOOS.isEnableProduct, @@ -151,7 +154,16 @@ function QuoteDraft({ setOpenPage }: PageProps) { }, } = useContext(CustomStyleContext); - const quotesActionsPermission = isB2BUser ? b2bPermissions.quotesActionsPermission : true; + const quotesActionsPermission = useMemo(() => { + if (isB2BUser) { + return verifyCreatePermission( + b2bPermissionsMap.quotesCreateActionsPermission, + +selectCompanyHierarchyId, + ); + } + + return true; + }, [isB2BUser, selectCompanyHierarchyId]); const navigate = useNavigate(); @@ -196,9 +208,9 @@ function QuoteDraft({ setOpenPage }: PageProps) { dispatch(setDraftQuoteInfo(newInfo)); }; - try { - const quoteInfo = cloneDeep(quoteInfoOrigin); + const quoteInfo = cloneDeep(quoteInfoOrigin); + try { if (isB2BUser) { const companyId = companyB2BId || salesRepCompanyId; const { @@ -582,7 +594,7 @@ function QuoteDraft({ setOpenPage }: PageProps) { : (allPrice + allTaxPrice).toFixed(currency.decimal_places), grandTotal: allPrice.toFixed(currency.decimal_places), subtotal: allPrice.toFixed(currency.decimal_places), - companyId: isB2BUser ? companyB2BId || salesRepCompanyId : '', + companyId: isB2BUser ? selectCompanyHierarchyId || companyB2BId || salesRepCompanyId : '', storeHash, quoteTitle, discount: '0.00', diff --git a/apps/storefront/src/pages/ShoppingListDetails/components/ReAddToCart.tsx b/apps/storefront/src/pages/ShoppingListDetails/components/ReAddToCart.tsx index ccd4721f8..576e8c504 100644 --- a/apps/storefront/src/pages/ShoppingListDetails/components/ReAddToCart.tsx +++ b/apps/storefront/src/pages/ShoppingListDetails/components/ReAddToCart.tsx @@ -14,7 +14,7 @@ import { activeCurrencyInfoSelector, rolePermissionSelector, useAppSelector } fr import { currencyFormat, snackbar } from '@/utils'; import { setModifierQtyPrice } from '@/utils/b3Product/b3Product'; import { - addlineItems, + addLineItems, getProductOptionsFields, ProductsProps, } from '@/utils/b3Product/shared/config'; @@ -191,9 +191,9 @@ export default function ReAddToCart(props: ShoppingProductsProps) { const newProduct: ProductsProps[] = [...products]; newProduct[index].node.quantity = +value; newProduct[index].isValid = isValid; - const caculateProduct = await setModifierQtyPrice(newProduct[index].node, +value); - if (caculateProduct) { - (newProduct[index] as CustomFieldItems).node = caculateProduct; + const calculateProduct = await setModifierQtyPrice(newProduct[index].node, +value); + if (calculateProduct) { + (newProduct[index] as CustomFieldItems).node = calculateProduct; setValidateFailureProducts(newProduct); } }; @@ -220,7 +220,7 @@ export default function ReAddToCart(props: ShoppingProductsProps) { try { setLoading(true); - const lineItems = addlineItems(products); + const lineItems = addLineItems(products); const res = await callCart(lineItems); diff --git a/apps/storefront/src/pages/ShoppingListDetails/components/ShoppingDetailFooter.tsx b/apps/storefront/src/pages/ShoppingListDetails/components/ShoppingDetailFooter.tsx index 9e1d5d565..aab03ffc3 100644 --- a/apps/storefront/src/pages/ShoppingListDetails/components/ShoppingDetailFooter.tsx +++ b/apps/storefront/src/pages/ShoppingListDetails/components/ShoppingDetailFooter.tsx @@ -26,7 +26,7 @@ import { validProductQty, } from '@/utils/b3Product/b3Product'; import { - addlineItems, + addLineItems, conversionProductsList, ProductsProps, } from '@/utils/b3Product/shared/config'; @@ -83,8 +83,11 @@ function ShoppingDetailFooter(props: ShoppingDetailFooterProps) { const isAgenting = useAppSelector(({ b2bFeatures }) => b2bFeatures.masqueradeCompany.isAgenting); const companyId = useAppSelector(({ company }) => company.companyInfo.id); const customerGroupId = useAppSelector(({ company }) => company.customer.customerGroupId); - const { shoppingListActionsPermission, purchasabilityPermission, submitShoppingListPermission } = - useAppSelector(rolePermissionSelector); + const { + shoppingListCreateActionsPermission, + purchasabilityPermission, + submitShoppingListPermission, + } = useAppSelector(rolePermissionSelector); const ref = useRef(null); const [isOpen, setIsOpen] = useState(false); @@ -114,7 +117,7 @@ function ShoppingDetailFooter(props: ShoppingDetailFooterProps) { role, } = props; - const b2bShoppingListActionsPermission = isB2BUser ? shoppingListActionsPermission : true; + const b2bShoppingListActionsPermission = isB2BUser ? shoppingListCreateActionsPermission : true; const isCanAddToCart = isB2BUser ? purchasabilityPermission : true; const b2bSubmitShoppingListPermission = isB2BUser ? submitShoppingListPermission : +role === 2; @@ -244,7 +247,7 @@ function ShoppingDetailFooter(props: ShoppingDetailFooterProps) { ); if (validateSuccessArr.length !== 0) { - const lineItems = addlineItems(validateSuccessArr); + const lineItems = addLineItems(validateSuccessArr); const deleteCartObject = deleteCartData(cartEntityId); const cartInfo = await getCart(); let res = null; diff --git a/apps/storefront/src/pages/ShoppingListDetails/components/ShoppingDetailHeader.tsx b/apps/storefront/src/pages/ShoppingListDetails/components/ShoppingDetailHeader.tsx index 2846bdbf7..6c285d1ca 100644 --- a/apps/storefront/src/pages/ShoppingListDetails/components/ShoppingDetailHeader.tsx +++ b/apps/storefront/src/pages/ShoppingListDetails/components/ShoppingDetailHeader.tsx @@ -1,4 +1,4 @@ -import { useContext } from 'react'; +import { useContext, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { useB3Lang } from '@b3/lang'; import { ArrowBackIosNew } from '@mui/icons-material'; @@ -10,6 +10,8 @@ import { useMobile } from '@/hooks'; import { type SetOpenPage } from '@/pages/SetOpenPage'; import { CustomStyleContext } from '@/shared/customStyleButton'; import { rolePermissionSelector, useAppSelector } from '@/store'; +import { verifyLevelPermission, verifySubmitShoppingListSubsidiariesPermission } from '@/utils'; +import { b2bPermissionsMap } from '@/utils/b3CheckPermissions/config'; import { ShoppingStatus } from '../../ShoppingLists/ShoppingStatus'; @@ -55,10 +57,57 @@ function ShoppingDetailHeader(props: ShoppingDetailHeaderProps) { } = useContext(CustomStyleContext); const navigate = useNavigate(); + const { selectCompanyHierarchyId } = useAppSelector( + ({ company }) => company.companyHierarchyInfo, + ); + + const { + submitShoppingListPermission: submitShoppingList, + approveShoppingListPermission: approveShoppingList, + } = useAppSelector(rolePermissionSelector); + + const shoppingListPermissions = useMemo(() => { + if (isB2BUser) { + const companyInfo = shoppingListInfo?.companyInfo || {}; + + const { + submitShoppingListPermission: submitShoppingListPermissionCode, + approveShoppingListPermission: approveShoppingListPermissionCode, + } = b2bPermissionsMap; + const submitShoppingListPermissionLevel = verifySubmitShoppingListSubsidiariesPermission({ + code: submitShoppingListPermissionCode, + userId: +(customerInfo?.userId || 0), + selectId: +selectCompanyHierarchyId, + }); + + const approveShoppingListPermissionLevel = verifyLevelPermission({ + code: approveShoppingListPermissionCode, + companyId: +(companyInfo?.companyId || 0), + userId: +(customerInfo?.userId || 0), + }); + + return { + submitShoppingListPermission: submitShoppingListPermissionLevel, + approveShoppingListPermission: approveShoppingListPermissionLevel, + }; + } + + return { + submitShoppingListPermission: submitShoppingList, + approveShoppingListPermission: approveShoppingList, + }; + }, [ + customerInfo, + isB2BUser, + submitShoppingList, + approveShoppingList, + shoppingListInfo?.companyInfo, + selectCompanyHierarchyId, + ]); + const isDisabledBtn = shoppingListInfo?.products?.edges.length === 0; - const { submitShoppingListPermission, approveShoppingListPermission } = - useAppSelector(rolePermissionSelector); + const { submitShoppingListPermission, approveShoppingListPermission } = shoppingListPermissions; const gridOptions = (xs: number) => isMobile diff --git a/apps/storefront/src/pages/ShoppingListDetails/components/ShoppingDetailTable.tsx b/apps/storefront/src/pages/ShoppingListDetails/components/ShoppingDetailTable.tsx index 652c6264c..a779fd5a6 100644 --- a/apps/storefront/src/pages/ShoppingListDetails/components/ShoppingDetailTable.tsx +++ b/apps/storefront/src/pages/ShoppingListDetails/components/ShoppingDetailTable.tsx @@ -163,11 +163,11 @@ function ShoppingDetailTable(props: ShoppingDetailTableProps, ref: Ref) const showInclusiveTaxPrice = useAppSelector(({ global }) => global.showInclusiveTaxPrice); - const { shoppingListActionsPermission, submitShoppingListPermission } = + const { shoppingListCreateActionsPermission, submitShoppingListPermission } = useAppSelector(rolePermissionSelector); const canShoppingListActions = isB2BUser - ? shoppingListActionsPermission && isCanEditShoppingList + ? shoppingListCreateActionsPermission && isCanEditShoppingList : true; const b2bAndBcShoppingListActionsPermissions = isB2BUser ? canShoppingListActions : true; const b2bSubmitShoppingListPermission = isB2BUser ? submitShoppingListPermission : +role === 2; diff --git a/apps/storefront/src/pages/ShoppingListDetails/index.tsx b/apps/storefront/src/pages/ShoppingListDetails/index.tsx index 4319c7e9f..460833d73 100644 --- a/apps/storefront/src/pages/ShoppingListDetails/index.tsx +++ b/apps/storefront/src/pages/ShoppingListDetails/index.tsx @@ -1,8 +1,7 @@ -import { useContext, useEffect, useRef, useState } from 'react'; +import { useContext, useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useB3Lang } from '@b3/lang'; import { Box, Grid, useTheme } from '@mui/material'; -import isEmpty from 'lodash-es/isEmpty'; import B3Spin from '@/components/spin/B3Spin'; import { useMobile } from '@/hooks'; @@ -24,7 +23,9 @@ import { rolePermissionSelector, useAppSelector, } from '@/store'; -import { channelId, getB3PermissionsList, snackbar } from '@/utils'; +import { CustomerRole } from '@/types'; +import { channelId, snackbar, verifyLevelPermission } from '@/utils'; +import { b2bPermissionsMap } from '@/utils/b3CheckPermissions/config'; import { calculateProductListPrice, getBCPrice } from '@/utils/b3Product/b3Product'; import { conversionProductsList, @@ -60,11 +61,6 @@ interface UpdateShoppingListParamsProps { channelId?: number; } -interface PermissionLevelInfoProps { - permissionType: string; - permissionLevel?: number | string; -} - // shoppingList status: 0 -- Approved; 20 -- Rejected; 30 -- Draft; 40 -- Ready for approval // 0: Admin, 1: Senior buyer, 2: Junior buyer, 3: Super admin @@ -78,7 +74,6 @@ function ShoppingListDetails({ setOpenPage }: PageProps) { const role = useAppSelector(({ company }) => company.customer.role); const companyInfoId = useAppSelector(({ company }) => company.companyInfo.id); const customerGroupId = useAppSelector(({ company }) => company.customer.customerGroupId); - const permissions = useAppSelector(({ company }) => company.permissions); const isAgenting = useAppSelector(({ b2bFeatures }) => b2bFeatures.masqueradeCompany.isAgenting); const navigate = useNavigate(); @@ -108,14 +103,34 @@ function ShoppingListDetails({ setOpenPage }: PageProps) { const [allowJuniorPlaceOrder, setAllowJuniorPlaceOrder] = useState(false); const [isCanEditShoppingList, setIsCanEditShoppingList] = useState(true); - const { shoppingListActionsPermission, purchasabilityPermission, submitShoppingListPermission } = - useAppSelector(rolePermissionSelector); + const { + shoppingListCreateActionsPermission, + purchasabilityPermission, + submitShoppingListPermission, + } = useAppSelector(rolePermissionSelector); const b2bAndBcShoppingListActionsPermissions = isB2BUser - ? shoppingListActionsPermission && isCanEditShoppingList + ? shoppingListCreateActionsPermission && isCanEditShoppingList : true; + const submitShoppingList = useMemo(() => { + if (isB2BUser && shoppingListInfo) { + const { companyInfo, customerInfo } = shoppingListInfo; + const { submitShoppingListPermission: submitShoppingListPermissionCode } = b2bPermissionsMap; + const submitShoppingListPermissionLevel = verifyLevelPermission({ + code: submitShoppingListPermissionCode, + companyId: +(companyInfo?.companyId || 0), + userId: +(customerInfo?.userId || 0), + }); + + return submitShoppingListPermissionLevel; + } + + return submitShoppingListPermission; + }, [submitShoppingListPermission, isB2BUser, shoppingListInfo]); const isCanAddToCart = isB2BUser ? purchasabilityPermission : true; - const b2bSubmitShoppingListPermission = isB2BUser ? submitShoppingListPermission : role === 2; + const b2bSubmitShoppingListPermission = isB2BUser + ? submitShoppingList + : role === CustomerRole.JUNIOR_BUYER; const isJuniorApprove = shoppingListInfo?.status === 0 && b2bSubmitShoppingListPermission; @@ -132,7 +147,7 @@ function ShoppingListDetails({ setOpenPage }: PageProps) { id: parseInt(id, 10) || 0, }, }); - // disabling as we dont need a dispatcher here + // disabling as we don't need a dispatcher here // eslint-disable-next-line react-hooks/exhaustive-deps }, [id]); @@ -334,26 +349,17 @@ function ShoppingListDetails({ setOpenPage }: PageProps) { useEffect(() => { if (isB2BUser && shoppingListInfo) { - const editShoppingListPermission = permissions.find( - (item) => item.code === 'deplicate_shopping_list', - ); - const param: PermissionLevelInfoProps[] = []; - - if (editShoppingListPermission && !isEmpty(editShoppingListPermission)) { - const currentLevel = editShoppingListPermission.permissionLevel; - const isOwner = shoppingListInfo?.isOwner || false; - param.push({ - permissionType: 'shoppingListActionsPermission', - permissionLevel: currentLevel === 1 && isOwner ? currentLevel : 2, - }); - } + const { companyInfo, customerInfo } = shoppingListInfo; - const { shoppingListActionsPermission } = getB3PermissionsList(param); + const { shoppingListCreateActionsPermission } = b2bPermissionsMap; + const shoppingListActionsPermission = verifyLevelPermission({ + code: shoppingListCreateActionsPermission, + companyId: +(companyInfo?.companyId || 0), + userId: +(customerInfo?.userId || 0), + }); setIsCanEditShoppingList(shoppingListActionsPermission); } - - // eslint-disable-next-line react-hooks/exhaustive-deps }, [shoppingListInfo, isB2BUser]); return ( diff --git a/apps/storefront/src/pages/ShoppingLists/AddEditShoppingLists.tsx b/apps/storefront/src/pages/ShoppingLists/AddEditShoppingLists.tsx index 9a6125b17..e5fb70338 100644 --- a/apps/storefront/src/pages/ShoppingLists/AddEditShoppingLists.tsx +++ b/apps/storefront/src/pages/ShoppingLists/AddEditShoppingLists.tsx @@ -31,6 +31,9 @@ function AddEditShoppingLists( ref: Ref | undefined, ) { const b2bPermissions = useAppSelector(rolePermissionSelector); + const { selectCompanyHierarchyId } = useAppSelector( + ({ company }) => company.companyHierarchyInfo, + ); const [open, setOpen] = useState(false); const [type, setType] = useState(''); @@ -96,6 +99,9 @@ function AddEditShoppingLists( } else if (type === 'add') { if (isB2BUser) { const { submitShoppingListPermission } = b2bPermissions; + if (selectCompanyHierarchyId) { + params.companyId = +selectCompanyHierarchyId; + } params.status = submitShoppingListPermission ? 30 : 0; } else { params.channelId = channelId; diff --git a/apps/storefront/src/pages/ShoppingLists/ShoppingListsCard.tsx b/apps/storefront/src/pages/ShoppingLists/ShoppingListsCard.tsx index 2dd864abd..2543ded0b 100644 --- a/apps/storefront/src/pages/ShoppingLists/ShoppingListsCard.tsx +++ b/apps/storefront/src/pages/ShoppingLists/ShoppingListsCard.tsx @@ -10,11 +10,11 @@ import Card from '@mui/material/Card'; import CardContent from '@mui/material/CardContent'; import IconButton from '@mui/material/IconButton'; import Typography from '@mui/material/Typography'; -import isEmpty from 'lodash-es/isEmpty'; import CustomButton from '@/components/button/CustomButton'; import { rolePermissionSelector, useAppSelector } from '@/store'; -import { displayFormat, getB3PermissionsList } from '@/utils'; +import { displayFormat, verifyLevelPermission } from '@/utils'; +import { b2bPermissionsMap } from '@/utils/b3CheckPermissions/config'; import { ShoppingListsItemsProps } from './config'; import { ShoppingStatus } from './ShoppingStatus'; @@ -28,11 +28,6 @@ export interface OrderItemCardProps { isB2BUser: boolean; } -interface PermissionLevelInfoProps { - permissionType: string; - permissionLevel?: number | string; -} - const Flex = styled('div')(() => ({ display: 'flex', alignItems: 'center', @@ -55,7 +50,6 @@ function ShoppingListsCard(props: OrderItemCardProps) { const b3Lang = useB3Lang(); const [isCanEditShoppingList, setIsCanEditShoppingList] = useState(true); - const permissions = useAppSelector(({ company }) => company.permissions); const { submitShoppingListPermission, approveShoppingListPermission } = useAppSelector(rolePermissionSelector); @@ -91,24 +85,17 @@ function ShoppingListsCard(props: OrderItemCardProps) { useEffect(() => { if (isB2BUser) { - const editShoppingListPermission = permissions.find( - (item) => item.code === 'deplicate_shopping_list', - ); - const param: PermissionLevelInfoProps[] = []; - if (editShoppingListPermission && !isEmpty(editShoppingListPermission)) { - const currentLevel = editShoppingListPermission.permissionLevel; - const isOwner = shoppingList?.isOwner || false; - param.push({ - permissionType: 'shoppingListActionsPermission', - permissionLevel: currentLevel === 1 && isOwner ? currentLevel : 2, - }); - } - const { shoppingListActionsPermission } = getB3PermissionsList(param); + const { companyInfo, customerInfo } = shoppingList; + + const { shoppingListCreateActionsPermission } = b2bPermissionsMap; + const shoppingListActionsPermission = verifyLevelPermission({ + code: shoppingListCreateActionsPermission, + companyId: +(companyInfo?.companyId || 0), + userId: +customerInfo.userId, + }); setIsCanEditShoppingList(shoppingListActionsPermission); } - - // eslint-disable-next-line react-hooks/exhaustive-deps }, [shoppingList, isB2BUser]); return ( diff --git a/apps/storefront/src/pages/ShoppingLists/config.ts b/apps/storefront/src/pages/ShoppingLists/config.ts index 7439b4e6d..6e35c7f3d 100644 --- a/apps/storefront/src/pages/ShoppingLists/config.ts +++ b/apps/storefront/src/pages/ShoppingLists/config.ts @@ -1,5 +1,7 @@ import { LangFormatFunction } from '@b3/lang'; +import { CompanyInfoTypes } from '@/types'; + export interface ShoppingListSearch { search?: string; createdBy?: string; @@ -36,6 +38,8 @@ export interface ShoppingListsItemsProps { channelId: number; approvedFlag: boolean; isOwner: boolean; + companyInfo: CompanyInfoTypes | null; + companyId?: number; } export interface GetFilterMoreListProps { diff --git a/apps/storefront/src/pages/ShoppingLists/index.tsx b/apps/storefront/src/pages/ShoppingLists/index.tsx index 11a5c9577..755803a06 100644 --- a/apps/storefront/src/pages/ShoppingLists/index.tsx +++ b/apps/storefront/src/pages/ShoppingLists/index.tsx @@ -51,7 +51,7 @@ function ShoppingLists() { const isB2BUser = useAppSelector(isB2BUserSelector); const companyB2BId = useAppSelector(({ company }) => company.companyInfo.id); - const { shoppingListActionsPermission, submitShoppingListPermission } = + const { shoppingListCreateActionsPermission, submitShoppingListPermission } = useAppSelector(rolePermissionSelector); useEffect(() => { @@ -103,7 +103,7 @@ function ShoppingLists() { const isExtraLarge = useCardListColumn(); const customItem = { - isEnabled: isB2BUser ? shoppingListActionsPermission : true, + isEnabled: isB2BUser ? shoppingListCreateActionsPermission : true, customLabel: b3Lang('shoppingLists.createNew'), customButtonStyle: { fontSize: '15px', @@ -241,7 +241,7 @@ function ShoppingLists() { void; onDelete: (data: UsersList) => void; - isPermissions: boolean; } const Flex = styled('div')(() => ({ @@ -34,19 +35,33 @@ const Flex = styled('div')(() => ({ })); export function UserItemCard(props: OrderItemCardProps) { - const { item: userInfo, onEdit, onDelete, isPermissions } = props; + const { item: userInfo, onEdit, onDelete } = props; + const { companyInfo, id, companyRoleName, firstName, lastName, email } = userInfo; + + const { userUpdateActionsPermission, userDeleteActionsPermission } = b2bPermissionsMap; + + const updateActionsPermission = verifyLevelPermission({ + code: userUpdateActionsPermission, + companyId: +(companyInfo?.companyId || 0), + userId: +id, + }); + const deleteActionsPermission = verifyLevelPermission({ + code: userDeleteActionsPermission, + companyId: +(companyInfo?.companyId || 0), + userId: +id, + }); const getNewRoleList = () => { const userRole = getUserRole(); const newRoleList: Array = userRole.map((item) => { if (+item.value === 2) { - if (userInfo.companyRoleName !== 'Junior Buyer') { + if (companyRoleName !== 'Junior Buyer') { return { color: '#ce93d8', textColor: 'black', ...item, - label: userInfo.companyRoleName, - name: userInfo.companyRoleName, + label: companyRoleName, + name: companyRoleName, }; } return { @@ -85,7 +100,7 @@ export function UserItemCard(props: OrderItemCardProps) { }; return ( - + - {userInfo.firstName} {userInfo.lastName} + {firstName} {lastName} - {userInfo.email} + {email} - {statusRender(userInfo.companyRoleName)} - - { - onEdit(userInfo); - }} - > - - - { - onDelete(userInfo); - }} - > - - + {statusRender(companyRoleName)} + + {updateActionsPermission && ( + { + onEdit(userInfo); + }} + > + + + )} + {deleteActionsPermission && ( + { + onDelete(userInfo); + }} + > + + + )} diff --git a/apps/storefront/src/pages/UserManagement/config.ts b/apps/storefront/src/pages/UserManagement/config.ts index 7a1ed3113..4aed10ca9 100644 --- a/apps/storefront/src/pages/UserManagement/config.ts +++ b/apps/storefront/src/pages/UserManagement/config.ts @@ -1,5 +1,7 @@ import { LangFormatFunction } from '@b3/lang'; +import { CompanyInfoTypes } from '@/types'; + interface ExtraFieldsProps { fieldName: string; fieldValue: string | number; @@ -17,7 +19,9 @@ interface UsersListItems { companyRoleId: number | string; updatedAt: number; extraFields: ExtraFieldsProps[]; - [key: string]: string | null | number | ExtraFieldsProps[]; + masqueradingCompanyId: number | string | null; + companyInfo: CompanyInfoTypes | null; + [key: string]: string | null | number | ExtraFieldsProps[] | CompanyInfoTypes; } interface FilterProps { diff --git a/apps/storefront/src/pages/UserManagement/index.tsx b/apps/storefront/src/pages/UserManagement/index.tsx index 5b557b10c..a82352698 100644 --- a/apps/storefront/src/pages/UserManagement/index.tsx +++ b/apps/storefront/src/pages/UserManagement/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useB3Lang } from '@b3/lang'; import { Box } from '@mui/material'; @@ -11,6 +11,8 @@ import { deleteUsers, getUsers } from '@/shared/service/b2b'; import { rolePermissionSelector, useAppSelector } from '@/store'; import { CustomerRole } from '@/types'; import { snackbar } from '@/utils'; +import { verifyCreatePermission } from '@/utils/b3CheckPermissions'; +import { b2bPermissionsMap } from '@/utils/b3CheckPermissions/config'; import B3AddEditUser from './AddEditUser'; import { FilterProps, getFilterMoreList, UsersList } from './config'; @@ -41,6 +43,8 @@ function UserManagement() { extraFields: [], companyRoleName: '', companyRoleId: '', + masqueradingCompanyId: '', + companyInfo: null, }); const b3Lang = useB3Lang(); @@ -55,21 +59,36 @@ function UserManagement() { const companyId = +role === CustomerRole.SUPER_ADMIN ? salesRepCompanyId : companyInfo?.id; const b2bPermissions = useAppSelector(rolePermissionSelector); + const { selectCompanyHierarchyId } = useAppSelector( + ({ company }) => company.companyHierarchyInfo, + ); + + const isEnableBtnPermissions = b2bPermissions.userCreateActionsPermission; + + const customItem = useMemo(() => { + const { userCreateActionsPermission } = b2bPermissionsMap; + + const isCreatePermission = verifyCreatePermission( + userCreateActionsPermission, + +selectCompanyHierarchyId, + ); + return { + isEnabled: isEnableBtnPermissions && isCreatePermission, + customLabel: b3Lang('userManagement.addUser'), + }; - const isEnableBtnPermissions = b2bPermissions.userActionsPermission; + // ignore b3Lang due it's function that doesn't not depend on any reactive value + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isEnableBtnPermissions, selectCompanyHierarchyId]); const addEditUserRef = useRef(null); const [paginationTableRef] = useTableRef(); - const customItem = { - isEnabled: isEnableBtnPermissions, - customLabel: b3Lang('userManagement.addUser'), - }; - const initSearch = { search: '', companyRoleId: '', companyId, + q: '', }; const filterMoreInfo = getFilterMoreList(b3Lang); @@ -159,7 +178,7 @@ function UserManagement() { handleCancelClick(); await deleteUsers({ userId: row.id || '', - companyId, + companyId: selectCompanyHierarchyId || companyId, }); snackbar.success(b3Lang('userManagement.deleteUserSuccessfully')); } finally { @@ -168,20 +187,6 @@ function UserManagement() { } }; - const resetFilterInfo = () => { - const newTranslatedFilterInfo = translatedFilterInfo.map((element: CustomFieldItems) => { - const translatedItem = element; - - translatedItem.setValueName = setValueName; - translatedItem.defaultName = ''; - - return element; - }); - - setValueName(''); - setTranslatedFilterInfo(newTranslatedFilterInfo); - }; - useEffect(() => { handleGetTranslatedFilterInfo(); @@ -204,7 +209,6 @@ function UserManagement() { handleFilterChange={handleFilterChange} customButtonConfig={customItem} handleFilterCustomButtonClick={handleAddUserClick} - resetFilterInfo={resetFilterInfo} /> )} /> - + company.companyInfo.id); const role = useAppSelector(({ company }) => company.customer.role); const salesRepCompanyId = useAppSelector(({ b2bFeatures }) => b2bFeatures.masqueradeCompany.id); const isAgenting = useAppSelector(({ b2bFeatures }) => b2bFeatures.masqueradeCompany.isAgenting); + const { order: orderSubViewPermission } = useAppSelector( + ({ company }) => company.pagesSubsidiariesPermission, + ); + + const { selectCompanyHierarchyId, isEnabledCompanyHierarchy } = useAppSelector( + ({ company }) => company.companyHierarchyInfo, + ); + const currentCompanyId = + role === CustomerRole.SUPER_ADMIN && isAgenting ? +salesRepCompanyId : +companyB2BId; + const [isRequestLoading, setIsRequestLoading] = useState(false); const [allTotal, setAllTotal] = useState(0); @@ -71,6 +97,7 @@ function Order({ isCompanyOrder = false }: OrderProps) { const [filterInfo, setFilterInfo] = useState>([]); const [getOrderStatuses, setOrderStatuses] = useState>([]); + const [isAutoRefresh, setIsAutoRefresh] = useState(false); const [handleSetOrderBy, order, orderBy] = useSort( sortKeys, @@ -81,7 +108,11 @@ function Order({ isCompanyOrder = false }: OrderProps) { useEffect(() => { const search = getInitFilter(isCompanyOrder, isB2BUser); + if (isB2BUser) { + search.companyIds = [+selectCompanyHierarchyId || +currentCompanyId]; + } setFilterData(search); + setIsAutoRefresh(true); if (role === 100) return; const initFilter = async () => { @@ -129,7 +160,7 @@ function Order({ isCompanyOrder = false }: OrderProps) { initFilter(); // disabling as we only need to run this once and values at starting render are good enough // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [selectCompanyHierarchyId]); const fetchList = async (params: Partial) => { const { edges = [], totalCount } = isB2BUser @@ -137,7 +168,7 @@ function Order({ isCompanyOrder = false }: OrderProps) { : await getBCAllOrders(params); setAllTotal(totalCount); - + setIsAutoRefresh(false); return { edges, totalCount, @@ -166,6 +197,17 @@ function Order({ isCompanyOrder = false }: OrderProps) { width: '10%', isSortable: true, }, + { + key: 'companyName', + title: b3Lang('orders.company'), + width: '10%', + isSortable: false, + render: (item: ListItem) => { + const { companyInfo } = item; + + return {companyInfo?.companyName || '–'}; + }, + }, { key: 'poNumber', title: b3Lang('orders.poReference'), @@ -209,17 +251,12 @@ function Order({ isCompanyOrder = false }: OrderProps) { width: '10%', isSortable: true, }, - { - key: 'companyId', - title: b3Lang('orders.company'), - render: (item: ListItem) => `${item?.companyName || ''}`, - width: '10%', - }, ]; const getColumnItems = () => { const getNewColumnItems = columnAllItems.filter((item: { key: string }) => { const { key } = item; + if (!isB2BUser && key === 'companyName') return false; if ((!isB2BUser || (+role === 3 && !isAgenting)) && key === 'placedBy') return false; if (key === 'companyId' && isB2BUser && (+role !== 3 || isAgenting)) return false; if ( @@ -268,6 +305,15 @@ function Order({ isCompanyOrder = false }: OrderProps) { const columnItems = getColumnItems(); + const handleSelectCompanies = (company: number[]) => { + const newCompanyIds = company.includes(-1) ? [] : company; + + setFilterData({ + ...filterData, + companyIds: newCompanyIds, + }); + }; + return ( - div': { + width: isMobile ? '100%' : 'auto', + }, }} - filterMoreInfo={filterInfo} - handleChange={handleChange} - handleFilterChange={handleFilterChange} - /> + > + {isEnabledCompanyHierarchy && orderSubViewPermission && ( + + + + )} + + + (''); - const [loadding, setLoadding] = useState(false); + const [loading, setLoading] = useState(false); - const { quotesActionsPermission: quotesActionsPermissionRename } = - useAppSelector(rolePermissionSelector); - const quotesActionsPermission = isB2BUser ? quotesActionsPermissionRename : true; + const { quotesUpdateMessageActionsPermission } = useAppSelector(rolePermissionSelector); + const quotesUpdateMessagePermission = isB2BUser ? quotesUpdateMessageActionsPermission : true; const convertedMsgs = (msgs: MessageProps[]) => { let nextMsg: MessageProps = {}; @@ -262,7 +261,7 @@ function Message({ msgs, id, isB2BUser, email, status }: MsgsProps) { const updateMsgs = async (msg: string) => { try { const fn = isB2BUser ? updateB2BQuote : updateBCQuote; - setLoadding(true); + setLoading(true); const { quoteUpdate: { quote: { trackingHistory }, @@ -280,7 +279,7 @@ function Message({ msgs, id, isB2BUser, email, status }: MsgsProps) { setRead(0); convertedMsgs(trackingHistory); } finally { - setLoadding(false); + setLoading(false); } }; @@ -293,7 +292,7 @@ function Message({ msgs, id, isB2BUser, email, status }: MsgsProps) { const handleOnChange = useCallback( (open: boolean) => { if (open) { - if (!quotesActionsPermission && isB2BUser) return; + if (!quotesUpdateMessagePermission && isB2BUser) return; const fn = isB2BUser ? updateB2BQuote : updateBCQuote; if (changeReadRef.current === 0 && msgs.length) { fn({ @@ -312,7 +311,7 @@ function Message({ msgs, id, isB2BUser, email, status }: MsgsProps) { changeReadRef.current += 1; } }, - [email, id, isB2BUser, msgs, quotesActionsPermission], + [email, id, isB2BUser, msgs, quotesUpdateMessagePermission], ); useEffect(() => { @@ -375,9 +374,9 @@ function Message({ msgs, id, isB2BUser, email, status }: MsgsProps) { - {status !== 4 && quotesActionsPermission && ( + {status !== 4 && quotesUpdateMessagePermission && ( (field.value || status === 'Draft') && ( {`${field.fieldName}: ${field.value}`} ), )} diff --git a/apps/storefront/src/pages/quote/components/QuoteNote.tsx b/apps/storefront/src/pages/quote/components/QuoteNote.tsx index 5e713bc63..a78e0c5eb 100644 --- a/apps/storefront/src/pages/quote/components/QuoteNote.tsx +++ b/apps/storefront/src/pages/quote/components/QuoteNote.tsx @@ -26,7 +26,7 @@ export default function QuoteNote(props: QuoteNoteProps) { const isB2BUser = useAppSelector(isB2BUserSelector); const b2bPermissions = useAppSelector(rolePermissionSelector); - const quotesActionsPermission = isB2BUser ? b2bPermissions.quotesActionsPermission : true; + const quotesActionsPermission = isB2BUser ? b2bPermissions.quotesCreateActionsPermission : true; const handleNoteTextChange = (event: ChangeEvent) => { setNoteText(event?.target.value || ''); diff --git a/apps/storefront/src/shared/customStyleButton/context/config.ts b/apps/storefront/src/shared/customStyleButton/context/config.ts index 68ff728f9..2cb623319 100644 --- a/apps/storefront/src/shared/customStyleButton/context/config.ts +++ b/apps/storefront/src/shared/customStyleButton/context/config.ts @@ -2,6 +2,7 @@ import { Dispatch } from 'react'; type BtnKeys = | 'masqueradeButton' + | 'switchAccountButton' | 'floatingAction' | 'addToAllQuoteBtn' | 'shoppingListBtn' @@ -90,6 +91,14 @@ export const initState = { horizontalPadding: '', verticalPadding: '', }, + switchAccountButton: { + color: '#FFFFFF', + text: 'Switch Company', + location: 'bottomLeft', + customCss: '', + horizontalPadding: '', + verticalPadding: '', + }, addQuoteBtn: { color: '#fff', text: 'Add to Quote', diff --git a/apps/storefront/src/shared/routeList.ts b/apps/storefront/src/shared/routeList.ts index d6d27086d..f96c600d6 100644 --- a/apps/storefront/src/shared/routeList.ts +++ b/apps/storefront/src/shared/routeList.ts @@ -1,10 +1,14 @@ import { FC, LazyExoticComponent, ReactElement } from 'react'; +import { PAGES_SUBSIDIARIES_PERMISSION_KEYS } from '@/constants'; import { PageProps } from '@/pages/PageProps'; import { GlobalState, QuoteConfigProps } from '@/shared/global/context/config'; import { store } from '@/store'; import { CompanyStatus, CustomerRole, UserTypes } from '@/types'; -import { checkEveryPermissionsCode, getPermissionsInfo } from '@/utils'; +import { checkEveryPermissionsCode } from '@/utils'; +import { validatePermissionWithComparisonType } from '@/utils/b3CheckPermissions'; + +import { legacyPermissions, newPermissions } from './routes/config'; export interface BuyerPortalRoute { path: string; @@ -24,73 +28,113 @@ export interface RouteItem extends RouteItemBasic { pageTitle?: string; idLang: string; permissionCodes?: string; + subsidiariesCompanyKey?: (typeof PAGES_SUBSIDIARIES_PERMISSION_KEYS)[number]['key']; } export interface RouteFirstLevelItem extends RouteItemBasic { isProvider: boolean; } +const { + dashboardPermissions, + ordersPermissions, + companyOrdersPermissions, + invoicePermissions, + quotesPermissions, + shoppingListsPermissions, + quickOrderPermissions, + orderDetailPermissions, + invoiceDetailPermissions, + addressesPermissions, + shoppingListDetailPermissions, + userManagementPermissions, + quoteDraftPermissions, + accountSettingPermissions, + companyHierarchyPermissions, + quoteDetailPermissions, +} = legacyPermissions; + +const { + ordersPermissionCodes, + companyOrdersPermissionCodes, + invoicePermissionCodes, + quotesPermissionCodes, + shoppingListsPermissionCodes, + orderDetailPerPermissionCodes, + invoiceDetailPerPermissionCodes, + addressesPermissionCodes, + shoppingListDetailPermissionCodes, + userManagementPermissionCodes, + quoteDraftPermissionCodes, + quoteDetailPermissionCodes, + companyHierarchyPermissionCodes, +} = newPermissions; + export const routeList: (BuyerPortalRoute | RouteItem)[] = [ { path: '/dashboard', name: 'Dashboard', wsKey: 'router-orders', isMenuItem: true, - permissions: [3, 4], + permissions: dashboardPermissions, isTokenLogin: true, idLang: 'global.navMenu.dashboard', }, { path: '/orders', name: 'My orders', + subsidiariesCompanyKey: 'order', wsKey: 'router-orders', isMenuItem: true, - permissions: [0, 1, 3, 4, 99, 100], - permissionCodes: 'get_orders, get_order_detail', + permissions: ordersPermissions, + permissionCodes: ordersPermissionCodes, isTokenLogin: true, idLang: 'global.navMenu.orders', }, { path: '/company-orders', name: 'Company orders', + subsidiariesCompanyKey: 'order', wsKey: 'router-orders', isMenuItem: true, - permissions: [0, 1, 3], - permissionCodes: 'get_orders, get_order_detail', + permissions: companyOrdersPermissions, + permissionCodes: companyOrdersPermissionCodes, isTokenLogin: true, idLang: 'global.navMenu.companyOrders', }, { path: '/invoice', name: 'Invoice', + subsidiariesCompanyKey: 'invoice', wsKey: 'invoice', isMenuItem: true, configKey: 'invoice', - permissions: [0, 1, 3], - permissionCodes: - 'get_invoices, get_invoice_detail, get_invoice_pdf, export_invoices, get_invoice_payments_history', + permissions: invoicePermissions, + permissionCodes: invoicePermissionCodes, isTokenLogin: true, idLang: 'global.navMenu.invoice', }, { path: '/quotes', name: 'Quotes', + subsidiariesCompanyKey: 'quotes', wsKey: 'quotes', isMenuItem: true, configKey: 'quotes', - permissions: [0, 1, 2, 3, 99, 100], - permissionCodes: 'get_quotes, get_quote_detail, get_quote_pdf', + permissions: quotesPermissions, + permissionCodes: quotesPermissionCodes, isTokenLogin: true, idLang: 'global.navMenu.quotes', }, { path: '/shoppingLists', name: 'Shopping lists', + subsidiariesCompanyKey: 'shoppingLists', wsKey: 'shoppingLists', isMenuItem: true, configKey: 'shoppingLists', - permissions: [0, 1, 2, 3, 99], - permissionCodes: 'get_shopping_lists, get_shopping_list_detail', + permissions: shoppingListsPermissions, + permissionCodes: shoppingListsPermissionCodes, isTokenLogin: true, idLang: 'global.navMenu.shoppingLists', }, @@ -101,7 +145,7 @@ export const routeList: (BuyerPortalRoute | RouteItem)[] = [ wsKey: 'quickOrder', isMenuItem: true, configKey: 'quickOrderPad', - permissions: [0, 1, 2, 3, 99], + permissions: quickOrderPermissions, isTokenLogin: true, idLang: 'global.navMenu.quickOrder', }, @@ -109,9 +153,10 @@ export const routeList: (BuyerPortalRoute | RouteItem)[] = [ path: '/orderDetail/:id', name: 'Order details', wsKey: 'router-orders', + subsidiariesCompanyKey: 'order', isMenuItem: false, - permissions: [0, 1, 2, 3, 4, 99, 100], - permissionCodes: 'get_orders, get_order_detail', + permissions: orderDetailPermissions, + permissionCodes: orderDetailPerPermissionCodes, isTokenLogin: true, idLang: 'global.navMenu.orderDetail', }, @@ -119,21 +164,22 @@ export const routeList: (BuyerPortalRoute | RouteItem)[] = [ path: '/invoiceDetail/:id', name: 'Invoice details', wsKey: 'router-invoice', + subsidiariesCompanyKey: 'invoice', isMenuItem: false, - permissions: [0, 1, 3, 99, 100], - permissionCodes: - 'get_invoices, get_invoice_detail, get_invoice_pdf, export_invoices, get_invoice_payments_history', + permissions: invoiceDetailPermissions, + permissionCodes: invoiceDetailPerPermissionCodes, isTokenLogin: true, idLang: 'global.navMenu.invoiceDetail', }, { path: '/addresses', name: 'Addresses', + subsidiariesCompanyKey: 'addresses', wsKey: 'router-address', isMenuItem: true, configKey: 'addressBook', - permissions: [0, 1, 2, 3, 99, 100], - permissionCodes: 'get_addresses, get_address_detail, get_default_shipping, get_default_billing', + permissions: addressesPermissions, + permissionCodes: addressesPermissionCodes, isTokenLogin: true, idLang: 'global.navMenu.addresses', }, @@ -142,18 +188,19 @@ export const routeList: (BuyerPortalRoute | RouteItem)[] = [ name: 'Shopping list', wsKey: 'router-shopping-list', isMenuItem: false, - permissions: [0, 1, 2, 3, 99], - permissionCodes: 'get_shopping_lists, get_shopping_list_detail', + permissions: shoppingListDetailPermissions, + permissionCodes: shoppingListDetailPermissionCodes, isTokenLogin: true, idLang: 'global.navMenu.shoppingList', }, { path: '/user-management', name: 'User management', + subsidiariesCompanyKey: 'userManagement', wsKey: 'router-userManagement', isMenuItem: true, - permissions: [0, 1, 3], - permissionCodes: 'get_users, get_user_detail', + permissions: userManagementPermissions, + permissionCodes: userManagementPermissionCodes, isTokenLogin: true, idLang: 'global.navMenu.userManagement', }, @@ -163,9 +210,8 @@ export const routeList: (BuyerPortalRoute | RouteItem)[] = [ wsKey: 'quoteDraft', isMenuItem: false, configKey: 'quoteDraft', - permissions: [0, 1, 2, 3, 4, 99, 100], - permissionCodes: - 'get_quotes,get_quote_detail, get_quote_pdf, create_quote, update_quote_message', + permissions: quoteDraftPermissions, + permissionCodes: quoteDraftPermissionCodes, isTokenLogin: false, idLang: 'global.navMenu.quoteDraft', }, @@ -175,18 +221,30 @@ export const routeList: (BuyerPortalRoute | RouteItem)[] = [ wsKey: 'accountSetting', isMenuItem: true, configKey: 'accountSettings', - permissions: [0, 1, 2, 3, 4, 99], + permissions: accountSettingPermissions, isTokenLogin: true, idLang: 'global.navMenu.accountSettings', }, + { + path: '/company-hierarchy', + name: 'Company hierarchy', + subsidiariesCompanyKey: 'companyHierarchy', + wsKey: 'companyHierarchy', + isMenuItem: true, + configKey: 'companyHierarchy', + permissions: companyHierarchyPermissions, + permissionCodes: companyHierarchyPermissionCodes, + isTokenLogin: true, + idLang: 'global.navMenu.companyHierarchy', + }, { path: '/quoteDetail/:id', name: 'Quote detail', wsKey: 'quoteDetail', isMenuItem: false, configKey: 'quoteDetail', - permissions: [0, 1, 2, 3, 4, 99, 100], - permissionCodes: 'get_quotes, get_quote_detail, get_quote_pdf', + permissions: quoteDetailPermissions, + permissionCodes: quoteDetailPermissionCodes, isTokenLogin: false, idLang: 'global.navMenu.quoteDetail', }, @@ -257,12 +315,11 @@ export const getAllowedRoutesWithoutComponent = (globalState: GlobalState): Buye }); if (path === '/company-orders' && isHasPermission) { - const orderPermissionInfo = getPermissionsInfo('get_orders'); - return ( - orderPermissionInfo && - orderPermissionInfo?.permissionLevel && - +orderPermissionInfo.permissionLevel > 1 - ); + return validatePermissionWithComparisonType({ + code: item.permissionCodes, + level: 2, + containOrEqual: 'contain', + }); } return isHasPermission; } @@ -306,6 +363,6 @@ export const getAllowedRoutesWithoutComponent = (globalState: GlobalState): Buye return !!config.enabledStatus && !!config.value; } - return !!config.enabledStatus; + return !!config.enabledStatus && permissions.includes(+role); }); }; diff --git a/apps/storefront/src/shared/routes/config.ts b/apps/storefront/src/shared/routes/config.ts new file mode 100644 index 000000000..985992d58 --- /dev/null +++ b/apps/storefront/src/shared/routes/config.ts @@ -0,0 +1,136 @@ +import { CustomerRole } from '@/types'; +import { b2bPermissionsMap } from '@/utils/b3CheckPermissions/config'; + +const allLegacyPermission = [ + CustomerRole.SUPER_ADMIN, + CustomerRole.SUPER_ADMIN_BEFORE_AGENCY, + CustomerRole.ADMIN, + CustomerRole.SENIOR_BUYER, + CustomerRole.JUNIOR_BUYER, + CustomerRole.CUSTOM_ROLE, + CustomerRole.B2C, + CustomerRole.GUEST, +]; +const legacyPermissions = { + dashboardPermissions: [CustomerRole.SUPER_ADMIN, CustomerRole.SUPER_ADMIN_BEFORE_AGENCY], + ordersPermissions: [ + CustomerRole.SUPER_ADMIN, + CustomerRole.SUPER_ADMIN_BEFORE_AGENCY, + CustomerRole.ADMIN, + CustomerRole.SENIOR_BUYER, + CustomerRole.JUNIOR_BUYER, + CustomerRole.CUSTOM_ROLE, + CustomerRole.B2C, + CustomerRole.GUEST, + ], + companyOrdersPermissions: [ + CustomerRole.ADMIN, + CustomerRole.SENIOR_BUYER, + CustomerRole.SUPER_ADMIN, + CustomerRole.CUSTOM_ROLE, + ], + invoicePermissions: [ + CustomerRole.ADMIN, + CustomerRole.SENIOR_BUYER, + CustomerRole.SUPER_ADMIN, + CustomerRole.CUSTOM_ROLE, + ], + quotesPermissions: [ + CustomerRole.SUPER_ADMIN, + CustomerRole.ADMIN, + CustomerRole.SENIOR_BUYER, + CustomerRole.JUNIOR_BUYER, + CustomerRole.CUSTOM_ROLE, + CustomerRole.B2C, + CustomerRole.GUEST, + ], + shoppingListsPermissions: [ + CustomerRole.SUPER_ADMIN, + CustomerRole.ADMIN, + CustomerRole.SENIOR_BUYER, + CustomerRole.JUNIOR_BUYER, + CustomerRole.CUSTOM_ROLE, + CustomerRole.B2C, + ], + quickOrderPermissions: [ + CustomerRole.SUPER_ADMIN, + CustomerRole.ADMIN, + CustomerRole.SENIOR_BUYER, + CustomerRole.JUNIOR_BUYER, + CustomerRole.CUSTOM_ROLE, + CustomerRole.B2C, + ], + orderDetailPermissions: allLegacyPermission, + invoiceDetailPermissions: [ + CustomerRole.SUPER_ADMIN, + CustomerRole.ADMIN, + CustomerRole.SENIOR_BUYER, + CustomerRole.CUSTOM_ROLE, + CustomerRole.B2C, + CustomerRole.GUEST, + ], + addressesPermissions: [ + CustomerRole.SUPER_ADMIN, + CustomerRole.ADMIN, + CustomerRole.SENIOR_BUYER, + CustomerRole.JUNIOR_BUYER, + CustomerRole.CUSTOM_ROLE, + CustomerRole.B2C, + CustomerRole.GUEST, + ], + shoppingListDetailPermissions: [ + CustomerRole.SUPER_ADMIN, + CustomerRole.ADMIN, + CustomerRole.SENIOR_BUYER, + CustomerRole.JUNIOR_BUYER, + CustomerRole.CUSTOM_ROLE, + CustomerRole.B2C, + ], + userManagementPermissions: [ + CustomerRole.SUPER_ADMIN, + CustomerRole.ADMIN, + CustomerRole.SENIOR_BUYER, + CustomerRole.CUSTOM_ROLE, + ], + quoteDraftPermissions: allLegacyPermission, + accountSettingPermissions: [ + CustomerRole.SUPER_ADMIN, + CustomerRole.ADMIN, + CustomerRole.SENIOR_BUYER, + CustomerRole.JUNIOR_BUYER, + CustomerRole.CUSTOM_ROLE, + CustomerRole.B2C, + CustomerRole.SUPER_ADMIN_BEFORE_AGENCY, + ], + companyHierarchyPermissions: [ + CustomerRole.ADMIN, + CustomerRole.SENIOR_BUYER, + CustomerRole.JUNIOR_BUYER, + CustomerRole.CUSTOM_ROLE, + ], + quoteDetailPermissions: allLegacyPermission, +}; + +const denyInvoiceRoles = [ + CustomerRole.SUPER_ADMIN_BEFORE_AGENCY, + CustomerRole.B2C, + CustomerRole.GUEST, +]; + +const newPermissions = { + ordersPermissionCodes: b2bPermissionsMap.getOrderPermission, + companyOrdersPermissionCodes: b2bPermissionsMap.getOrderPermission, + invoicePermissionCodes: b2bPermissionsMap.getInvoicesPermission, + quotesPermissionCodes: b2bPermissionsMap.getQuotesPermission, + shoppingListsPermissionCodes: b2bPermissionsMap.getShoppingListPermission, + orderDetailPerPermissionCodes: b2bPermissionsMap.getOrderDetailPermission, + invoiceDetailPerPermissionCodes: b2bPermissionsMap.getInvoiceDetailPermission, + addressesPermissionCodes: b2bPermissionsMap.getAddressesPermission, + shoppingListDetailPermissionCodes: b2bPermissionsMap.getShoppingListDetailPermission, + userManagementPermissionCodes: b2bPermissionsMap.getUserPermissionCode, + quoteDraftPermissionCodes: b2bPermissionsMap.quotesCreateActionsPermission, + quoteDetailPermissionCodes: b2bPermissionsMap.getQuoteDetailPermission, + companyHierarchyPermissionCodes: b2bPermissionsMap.companyHierarchyPermission, +}; + +export { legacyPermissions, denyInvoiceRoles, allLegacyPermission, newPermissions }; diff --git a/apps/storefront/src/shared/routes/index.tsx b/apps/storefront/src/shared/routes/index.tsx index 316024e62..e62f400d0 100644 --- a/apps/storefront/src/shared/routes/index.tsx +++ b/apps/storefront/src/shared/routes/index.tsx @@ -4,7 +4,7 @@ import { matchPath } from 'react-router-dom'; import { PageProps } from '@/pages/PageProps'; import { store } from '@/store'; import { CompanyStatus, CustomerRole, UserTypes } from '@/types'; -import { getB3PermissionsList } from '@/utils'; +import { b2bJumpPath } from '@/utils'; import b2bLogger from '@/utils/b3Logger'; import { isB2bTokenPage, logoutSession } from '@/utils/b3logout'; @@ -18,6 +18,8 @@ import { routeList, } from '../routeList'; +import { allLegacyPermission, denyInvoiceRoles } from './config'; + const AccountSetting = lazy(() => import('@/pages/AccountSetting')); const AddressList = lazy(() => import('@/pages/Address')); const CompanyOrderList = lazy(() => import('@/pages/CompanyOrder')); @@ -40,6 +42,7 @@ const RegisteredBCToB2B = lazy(() => import('@/pages/RegisteredBCToB2B')); const ShippingLists = lazy(() => import('@/pages/ShoppingLists')); const ShoppingListDetails = lazy(() => import('@/pages/ShoppingListDetails')); const UserManagement = lazy(() => import('@/pages/UserManagement')); +const CompanyHierarchy = lazy(() => import('@/pages/CompanyHierarchy')); const routesMap: Record ReactElement>> = { '/dashboard': Dashboard, @@ -57,6 +60,7 @@ const routesMap: Record ReactE '/quoteDraft': QuoteDraft, '/accountSettings': AccountSetting, '/quoteDetail/:id': QuoteDetail, + '/company-hierarchy': CompanyHierarchy, }; function addComponentToRoutes(routes: BuyerPortalRoute[]): RouteItem[] { @@ -75,55 +79,53 @@ const firstLevelRouting: RouteFirstLevelItem[] = [ path: '/', name: '', component: HomePage, - permissions: [0, 1, 2, 3, 4, 99, 100], + permissions: allLegacyPermission, isProvider: false, }, { path: '/register', name: 'register', component: Registered, - permissions: [0, 1, 2, 3, 4, 99, 100], + permissions: allLegacyPermission, isProvider: true, }, { path: '/login', name: 'Login', component: Login, - permissions: [0, 1, 2, 3, 4, 99, 100], + permissions: allLegacyPermission, isProvider: false, }, { path: '/pdp', name: 'pdp', component: PDP, - permissions: [0, 1, 2, 3, 4, 99, 100], + permissions: allLegacyPermission, isProvider: false, }, { path: '/forgotPassword', name: 'forgotPassword', component: ForgotPassword, - permissions: [0, 1, 2, 3, 4, 99, 100], + permissions: allLegacyPermission, isProvider: false, }, { path: '/registeredbctob2b', name: 'registeredbctob2b', component: RegisteredBCToB2B, - permissions: [0, 1, 2, 3, 4, 99, 100], + permissions: allLegacyPermission, isProvider: true, }, { path: '/payment/:id', name: 'payment', component: InvoicePayment, - permissions: [0, 1, 2, 3, 4, 99, 100], + permissions: allLegacyPermission, isProvider: false, }, ]; -const denyInvoiceRoles = [4, 99, 100]; - const invoiceTypes = ['invoice?invoiceId', 'invoice?receiptId']; const gotoAllowedAppPage = async ( @@ -135,7 +137,6 @@ const gotoAllowedAppPage = async ( const currentState = store.getState(); const { company } = currentState; - const { companyRoleName } = company.customer; const isLoggedIn = company.customer || role !== CustomerRole.GUEST; if (!isLoggedIn) { gotoPage('/login?loginFlag=loggedOutLogin&&closeIsLogout=1'); @@ -161,9 +162,7 @@ const gotoAllowedAppPage = async ( } let url = hash.split('#')[1] || ''; - const IsRealJuniorBuyer = - +role === CustomerRole.JUNIOR_BUYER && companyRoleName === 'Junior Buyer'; - const currentRole = !IsRealJuniorBuyer && +role === CustomerRole.JUNIOR_BUYER ? 1 : role; + if ((!url && role !== CustomerRole.GUEST && pathname.includes('account.php')) || isAccountEnter) { let isB2BUser = false; if ( @@ -175,18 +174,11 @@ const gotoAllowedAppPage = async ( isB2BUser = true; } - const { getShoppingListPermission, getOrderPermission } = getB3PermissionsList(); - let currentAuthorizedPages = '/orders'; + const currentAuthorizedPages = isB2BUser ? b2bJumpPath(+role) : '/orders'; - if (isB2BUser) { - currentAuthorizedPages = getShoppingListPermission ? '/shoppingLists' : '/accountSettings'; - if (getOrderPermission) - currentAuthorizedPages = IsRealJuniorBuyer ? currentAuthorizedPages : '/orders'; - } - - switch (currentRole) { + switch (+role) { case CustomerRole.JUNIOR_BUYER: - url = currentAuthorizedPages; + url = '/shoppingLists'; break; case CustomerRole.SUPER_ADMIN: url = '/dashboard'; @@ -199,7 +191,7 @@ const gotoAllowedAppPage = async ( const flag = routes.some((item: RouteItem) => { if (matchPath(item.path, url) || isInvoicePage()) { - return item.permissions.includes(currentRole); + return item.permissions.includes(+role); } return false; }); diff --git a/apps/storefront/src/shared/service/b2b/graphql/address.ts b/apps/storefront/src/shared/service/b2b/graphql/address.ts index 6843931a9..b4f8fedba 100644 --- a/apps/storefront/src/shared/service/b2b/graphql/address.ts +++ b/apps/storefront/src/shared/service/b2b/graphql/address.ts @@ -63,6 +63,10 @@ const getAddress = ({ } isDefaultShipping isDefaultBilling + companyInfo { + companyId + companyName + } } } } diff --git a/apps/storefront/src/shared/service/b2b/graphql/global.ts b/apps/storefront/src/shared/service/b2b/graphql/global.ts index 8cd4f2789..07dcedf0b 100644 --- a/apps/storefront/src/shared/service/b2b/graphql/global.ts +++ b/apps/storefront/src/shared/service/b2b/graphql/global.ts @@ -1,3 +1,4 @@ +import { CompanyHierarchyListProps, ConfigsSwitchStatusProps } from '@/types'; import { convertArrayToGraphql, convertObjectOrArrayKeysToCamel, @@ -26,6 +27,13 @@ interface ProductPrice { customer_group_id: number; } +interface CompanySubsidiariesProps { + companySubsidiaries: CompanyHierarchyListProps[]; +} + +interface ConfigsSwitchStatus { + storeConfigSwitchStatus: ConfigsSwitchStatusProps; +} const getB2BTokenQl = (currentCustomerJWT: string, channelId: number) => `mutation { authorization(authData: { bcToken: "${currentCustomerJWT}" @@ -311,6 +319,50 @@ const priceProducts = `query priceProducts($storeHash: String, $channelId: Int, } `; +const companySubsidiaries = `query { + companySubsidiaries { + companyId + companyName + parentCompanyId + parentCompanyName + channelFlag + } +}`; + +const userMasqueradingCompanyBegin = `mutation userMasqueradingCompanyBegin($companyId: Int!) { + userMasqueradingCompanyBegin(companyId: $companyId) { + userMasqueradingCompanyBegin{ + companyId + companyName + bcId + } + } +}`; + +const userMasqueradingCompanyEnd = `mutation userMasqueradingCompanyEnd { + userMasqueradingCompanyEnd { + message + } +}`; + +const userMasqueradingCompany = `query { + userMasqueradingCompany { + companyId + companyName + bcId + } +}`; + +const storeConfigSwitchStatus = `query storeConfigSwitchStatus($key: String!){ + storeConfigSwitchStatus( + key: $key, + ) { + id, + key, + isEnabled, + } +}`; + export const getB2BToken = (currentCustomerJWT: string, channelId = 1) => B3Request.graphqlB2B({ query: getB2BTokenQl(currentCustomerJWT, channelId), @@ -385,3 +437,29 @@ export const getProductPricing = (data: Partial) => data: convertObjectOrArrayKeysToSnake(b2bPriceProducts) as CustomFieldItems[], }; }); + +export const getCompanySubsidiaries = (): Promise => + B3Request.graphqlB2B({ + query: companySubsidiaries, + }); + +export const startUserMasqueradingCompany = (companyId: number) => + B3Request.graphqlB2B({ + query: userMasqueradingCompanyBegin, + variables: { companyId }, + }); + +export const endUserMasqueradingCompany = () => + B3Request.graphqlB2B({ + query: userMasqueradingCompanyEnd, + }); +export const getUserMasqueradingCompany = () => + B3Request.graphqlB2B({ + query: userMasqueradingCompany, + }); + +export const getStoreConfigsSwitchStatus = (key: string): Promise => + B3Request.graphqlB2B({ + query: storeConfigSwitchStatus, + variables: { key }, + }); diff --git a/apps/storefront/src/shared/service/b2b/graphql/invoice.ts b/apps/storefront/src/shared/service/b2b/graphql/invoice.ts index 021a0f910..086f43d32 100644 --- a/apps/storefront/src/shared/service/b2b/graphql/invoice.ts +++ b/apps/storefront/src/shared/service/b2b/graphql/invoice.ts @@ -12,6 +12,7 @@ const invoiceList = (data: CustomFieldItems) => `{ orderBy: "${data?.orderBy}" ${data?.beginDueDateAt ? `beginDueDateAt: "${data.beginDueDateAt}"` : ''} ${data?.endDueDateAt ? `endDueDateAt: "${data.endDueDateAt}"` : ''} + ${data?.companyIds ? `companyIds: ${convertArrayToGraphql(data.companyIds || [])}` : ''} ){ totalCount, pageInfo{ @@ -43,15 +44,28 @@ const invoiceList = (data: CustomFieldItems) => `{ code, value, }, + companyInfo { + companyId, + companyName, + companyAddress, + companyCountry, + companyState, + companyCity, + companyZipCode, + phoneNumber, + bcId, + }, + orderUserId, } } } }`; -const invoiceStats = (status: number | string, decimalPlaces: number) => `{ +const invoiceStats = (status: number | string, decimalPlaces: number, companyIds: number[]) => `{ invoiceStats ( ${status === '' ? '' : `status: ${status},`} decimalPlaces: ${decimalPlaces} + ${companyIds.length ? `companyIds: ${convertArrayToGraphql(companyIds || [])}` : ''} ){ totalBalance, overDueBalance, @@ -226,7 +240,11 @@ export const exportInvoicesAsCSV = (data: CustomFieldItems) => variables: data, }); -export const getInvoiceStats = (status: number | string, decimalPlaces: number) => +export const getInvoiceStats = ( + status: number | string, + decimalPlaces: number, + comapnyIds: number[], +) => B3Request.graphqlB2B({ - query: invoiceStats(status, decimalPlaces), + query: invoiceStats(status, decimalPlaces, comapnyIds), }); diff --git a/apps/storefront/src/shared/service/b2b/graphql/orders.ts b/apps/storefront/src/shared/service/b2b/graphql/orders.ts index d0b7ff36c..1e23ac9a4 100644 --- a/apps/storefront/src/shared/service/b2b/graphql/orders.ts +++ b/apps/storefront/src/shared/service/b2b/graphql/orders.ts @@ -1,7 +1,22 @@ import { B2BOrderData, OrderStatusItem } from '@/types'; +import { convertArrayToGraphql } from '../../../../utils'; import B3Request from '../../request/b3Fetch'; +const companyInfo = ` + companyInfo { + companyId, + companyName, + companyAddress, + companyCountry, + companyState, + companyCity, + companyZipCode, + phoneNumber, + bcId, + } +`; + const allOrders = (data: CustomFieldItems, fn: string) => `{ ${fn}( search: "${data.q || ''}" @@ -15,6 +30,7 @@ const allOrders = (data: CustomFieldItems, fn: string) => `{ isShowMy: "${data?.isShowMy || 0}" orderBy: "${data.orderBy}" email: "${data?.email || ''}" + ${data?.companyIds ? `companyIds: ${convertArrayToGraphql(data.companyIds || [])}` : ''} ){ totalCount, pageInfo{ @@ -50,6 +66,7 @@ const allOrders = (data: CustomFieldItems, fn: string) => `{ firstName, lastName, companyName, + ${companyInfo} } } } @@ -152,6 +169,7 @@ const orderDetail = (id: number, fn: string) => `{ extraFields, createdAt, }, + ${companyInfo} } }`; diff --git a/apps/storefront/src/shared/service/b2b/graphql/quickorder.ts b/apps/storefront/src/shared/service/b2b/graphql/quickOrder.ts similarity index 100% rename from apps/storefront/src/shared/service/b2b/graphql/quickorder.ts rename to apps/storefront/src/shared/service/b2b/graphql/quickOrder.ts diff --git a/apps/storefront/src/shared/service/b2b/graphql/shoppingList.ts b/apps/storefront/src/shared/service/b2b/graphql/shoppingList.ts index 7fed61a29..bb3bf0b2b 100644 --- a/apps/storefront/src/shared/service/b2b/graphql/shoppingList.ts +++ b/apps/storefront/src/shared/service/b2b/graphql/shoppingList.ts @@ -9,6 +9,7 @@ interface ShoppingListParams { description: string; status: number; channelId: number; + companyId: number; } const getStatus = (status: any): string => { @@ -62,7 +63,18 @@ const getShoppingList = ({ products { totalCount, } - approvedFlag + approvedFlag, + companyInfo { + companyId, + companyName, + companyAddress, + companyCountry, + companyState, + companyCity, + companyZipCode, + phoneNumber, + bcId, + }, } } } @@ -85,6 +97,17 @@ const getShoppingListInfo = `shoppingList { totalDiscount, totalTax, isShowGrandTotal, + companyInfo { + companyId, + companyName, + companyAddress, + companyCountry, + companyState, + companyCity, + companyZipCode, + phoneNumber, + bcId, + }, }`; const updateShoppingList = ( @@ -98,7 +121,9 @@ const updateShoppingList = ( } }`; -const createShoppingList = (fn: string) => `mutation($shoppingListData: ShoppingListsInputType!){ +const createShoppingList = ( + fn: string, +) => `mutation($shoppingListData: ShoppingListsCreateInputType!){ ${fn}( shoppingListData: $shoppingListData ) { @@ -178,6 +203,17 @@ const getShoppingListDetails = (data: CustomFieldItems) => `{ channelId, channelName, approvedFlag, + companyInfo { + companyId, + companyName, + companyAddress, + companyCountry, + companyState, + companyCity, + companyZipCode, + phoneNumber, + bcId, + }, products ( offset: ${data.offset || 0} first: ${data.first || 100}, @@ -463,6 +499,7 @@ export const createB2BShoppingList = (data: Partial) => query: createShoppingList('shoppingListsCreate'), variables: { shoppingListData: { + companyId: data.companyId, name: data.name, description: data.description, status: data.status, diff --git a/apps/storefront/src/shared/service/b2b/graphql/users.ts b/apps/storefront/src/shared/service/b2b/graphql/users.ts index cadc0ae7d..85b21c435 100644 --- a/apps/storefront/src/shared/service/b2b/graphql/users.ts +++ b/apps/storefront/src/shared/service/b2b/graphql/users.ts @@ -34,6 +34,18 @@ const getUsersQl = (data: CustomFieldItems) => `{ } companyRoleId, companyRoleName, + masqueradingCompanyId, + companyInfo { + companyId, + companyName, + companyAddress, + companyCountry, + companyState, + companyCity, + companyZipCode, + phoneNumber, + bcId, + }, } } } diff --git a/apps/storefront/src/shared/service/b2b/index.ts b/apps/storefront/src/shared/service/b2b/index.ts index e7c0a084d..7cd7d5455 100644 --- a/apps/storefront/src/shared/service/b2b/index.ts +++ b/apps/storefront/src/shared/service/b2b/index.ts @@ -14,17 +14,22 @@ import { updateBcAddress, } from './graphql/address'; import { + endUserMasqueradingCompany, getAgentInfo, getB2BToken, getBcCurrencies, getCompanyCreditConfig, + getCompanySubsidiaries, getCurrencies, getProductPricing, + getStoreConfigsSwitchStatus, getStorefrontConfig, getStorefrontConfigs, getStorefrontDefaultLanguages, getTaxZoneRates, getUserCompany, + getUserMasqueradingCompany, + startUserMasqueradingCompany, superAdminBeginMasquerade, superAdminCompanies, superAdminEndMasquerade, @@ -136,7 +141,7 @@ export { getInvoiceStats, invoiceDownloadPDF, } from './graphql/invoice'; -export { getBcOrderedProducts, getOrderedProducts } from './graphql/quickorder'; +export { getBcOrderedProducts, getOrderedProducts } from './graphql/quickOrder'; export { addOrUpdateUsers, @@ -205,6 +210,10 @@ export { getBcShoppingList, getBcShoppingListDetails, getBCStoreChannelId, + getCompanySubsidiaries, + startUserMasqueradingCompany, + endUserMasqueradingCompany, + getUserMasqueradingCompany, getBcVariantInfoBySkus, getCompanyCreditConfig, getCurrencies, @@ -215,6 +224,7 @@ export { getShoppingListsCreatedByUser, getStorefrontConfig, getStorefrontConfigs, + getStoreConfigsSwitchStatus, getStorefrontDefaultLanguages, getTaxZoneRates, getUserCompany, diff --git a/apps/storefront/src/store/index.ts b/apps/storefront/src/store/index.ts index 2d2c6330a..112dd4c01 100644 --- a/apps/storefront/src/store/index.ts +++ b/apps/storefront/src/store/index.ts @@ -25,7 +25,7 @@ export const middlewareOptions = { PURGE, REGISTER, 'theme/setThemeFrame', - 'global/setGlabolCommonState', + 'global/setGlobalCommonState', 'global/setOpenPageReducer', ], ignoredPaths: ['theme.themeFrame', 'global.globalMessage', 'global.setOpenPageFn'], @@ -49,4 +49,7 @@ export type AppDispatch = AppStore['dispatch']; export const useAppDispatch: () => AppDispatch = useDispatch; export const useAppSelector: TypedUseSelectorHook = useSelector; export const useAppStore: () => AppStore = useStore; + +// cspell:disable export const persistor = persistStore(store); +// cspell:enable diff --git a/apps/storefront/src/store/selectors.ts b/apps/storefront/src/store/selectors.ts index d778c1201..18f300fa1 100644 --- a/apps/storefront/src/store/selectors.ts +++ b/apps/storefront/src/store/selectors.ts @@ -2,8 +2,8 @@ import { createSelector } from '@reduxjs/toolkit'; import { RootState } from '@/store'; import { CompanyStatus, Currency, CustomerRole, UserTypes } from '@/types'; -import { checkEveryPermissionsCode } from '@/utils/b3CheckPermissions/permission'; -import { B2BPermissionParams, b2bPermissionsList } from '@/utils/b3RolePermissions/config'; +import { getCorrespondsConfigurationPermission } from '@/utils/b3CheckPermissions/base'; +import { B2BPermissionsMapParams } from '@/utils/b3CheckPermissions/config'; import { defaultCurrenciesState } from './slices/storeConfigs'; @@ -59,24 +59,11 @@ interface OptionList { export const rolePermissionSelector = createSelector( companySelector, - ({ permissions }): B2BPermissionParams => { - const keys = Object.keys(b2bPermissionsList); - - const newB3PermissionsList: Record = b2bPermissionsList; - - return keys.reduce((acc, cur: string) => { - const param = { - code: newB3PermissionsList[cur], - }; - - const item = checkEveryPermissionsCode(param, permissions); - - return { - ...acc, - [cur]: item, - }; - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - }, {} as B2BPermissionParams); + ({ + permissions, + companyHierarchyInfo: { selectCompanyHierarchyId }, + }): B2BPermissionsMapParams => { + return getCorrespondsConfigurationPermission(permissions, +selectCompanyHierarchyId); }, ); diff --git a/apps/storefront/src/store/slices/company.ts b/apps/storefront/src/store/slices/company.ts index 10d4ef77e..9ac0d3562 100644 --- a/apps/storefront/src/store/slices/company.ts +++ b/apps/storefront/src/store/slices/company.ts @@ -3,7 +3,17 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import persistReducer from 'redux-persist/es/persistReducer'; import storageSession from 'redux-persist/lib/storage/session'; -import { CompanyInfo, CompanyStatus, Customer, CustomerRole, LoginTypes, UserTypes } from '@/types'; +import { + CompanyHierarchyInfoProps, + CompanyHierarchyListProps, + CompanyInfo, + CompanyStatus, + Customer, + CustomerRole, + LoginTypes, + PagesSubsidiariesPermissionProps, + UserTypes, +} from '@/types'; interface Tokens { B2BToken: string; @@ -21,6 +31,8 @@ export interface CompanyState { customer: Customer; tokens: Tokens; permissions: PermissionsCodesProps[]; + companyHierarchyInfo: CompanyHierarchyInfoProps; + pagesSubsidiariesPermission: PagesSubsidiariesPermissionProps; } const initialState: CompanyState = { @@ -48,6 +60,23 @@ const initialState: CompanyState = { currentCustomerJWT: '', }, permissions: [], + companyHierarchyInfo: { + isEnabledCompanyHierarchy: true, + isHasCurrentPagePermission: true, + selectCompanyHierarchyId: '', + companyHierarchyList: [], + companyHierarchyAllList: [], + companyHierarchySelectSubsidiariesList: [], + }, + pagesSubsidiariesPermission: { + order: false, + invoice: false, + addresses: false, + userManagement: false, + shoppingLists: false, + quotes: false, + companyHierarchy: false, + }, }; const companySlice = createSlice({ @@ -88,6 +117,52 @@ const companySlice = createSlice({ setPermissionModules: (state, { payload }: PayloadAction) => { state.permissions = payload; }, + setPagesSubsidiariesPermission: ( + state, + { payload }: PayloadAction, + ) => { + state.pagesSubsidiariesPermission = payload; + }, + setCompanyHierarchyIsEnabled: ( + state, + { payload }: PayloadAction>, + ) => { + const { companyHierarchyInfo } = state; + + state.companyHierarchyInfo = { + ...companyHierarchyInfo, + ...payload, + }; + }, + setCompanyHierarchyListModules: ( + state, + { payload }: PayloadAction, + ) => { + const companyHierarchyList = payload.filter((item) => item.channelFlag); + const { companyHierarchyInfo } = state; + + state.companyHierarchyInfo = { + ...companyHierarchyInfo, + companyHierarchyList, + companyHierarchyAllList: payload, + }; + }, + setCompanyHierarchyInfoModules: ( + state, + { payload }: PayloadAction>, + ) => { + let companyHierarchyList = state.companyHierarchyInfo.companyHierarchyList; + + if (payload.companyHierarchyAllList?.length) { + companyHierarchyList = payload.companyHierarchyAllList.filter((item) => item.channelFlag); + } + + state.companyHierarchyInfo = { + ...state.companyHierarchyInfo, + ...payload, + companyHierarchyList, + }; + }, }, }); @@ -104,6 +179,9 @@ export const { setCurrentCustomerJWT, setLoginType, setPermissionModules, + setCompanyHierarchyListModules, + setCompanyHierarchyInfoModules, + setPagesSubsidiariesPermission, } = companySlice.actions; export default persistReducer({ key: 'company', storage: storageSession }, companySlice.reducer); diff --git a/apps/storefront/src/store/slices/global.ts b/apps/storefront/src/store/slices/global.ts index 7552e2d30..28598d623 100644 --- a/apps/storefront/src/store/slices/global.ts +++ b/apps/storefront/src/store/slices/global.ts @@ -75,6 +75,7 @@ export interface GlobalState { recordOpenHash: string; blockPendingQuoteNonPurchasableOOS: GlobalBlockPendingQuoteNonPurchasableOOS; quoteSubmissionResponse: QuoteSubmissionResponseProps; + isOpenCompanyHierarchyDropDown: boolean; } const initialState: GlobalState = { @@ -117,6 +118,7 @@ const initialState: GlobalState = { message: '', title: '', }, + isOpenCompanyHierarchyDropDown: false, }; export const globalSlice = createSlice({ @@ -167,6 +169,9 @@ export const globalSlice = createSlice({ ) => { state.quoteSubmissionResponse = payload; }, + setOpenCompanyHierarchyDropDown: (state, { payload }: PayloadAction) => { + state.isOpenCompanyHierarchyDropDown = payload; + }, }, }); @@ -182,6 +187,7 @@ export const { setStoreInfo, setLoginLandingLocation, setQuoteSubmissionResponse, + setOpenCompanyHierarchyDropDown, } = globalSlice.actions; export default globalSlice.reducer; diff --git a/apps/storefront/src/types/company.ts b/apps/storefront/src/types/company.ts index 6989bf797..2068c1171 100644 --- a/apps/storefront/src/types/company.ts +++ b/apps/storefront/src/types/company.ts @@ -1,3 +1,5 @@ +import { PAGES_SUBSIDIARIES_PERMISSION_KEYS } from '@/constants'; + export interface CompanyInfo { id: string; companyName: string; @@ -35,11 +37,20 @@ export enum CompanyStatus { DEFAULT = 99, } +export enum CustomerRoleName { + ADMIN_NAME = 'Admin', + SENIOR_BUYER_NAME = 'Senior Buyer', + JUNIOR_BUYER_NAME = 'Junior Buyer', +} + +/** CUSTOM_ROLE(role === 2 && roleName !== 'Junior Buyer') * */ export enum CustomerRole { ADMIN = 0, SENIOR_BUYER = 1, JUNIOR_BUYER = 2, SUPER_ADMIN = 3, + SUPER_ADMIN_BEFORE_AGENCY = 4, + CUSTOM_ROLE = 5, B2C = 99, GUEST = 100, } @@ -65,3 +76,35 @@ export enum FeatureEnabled { DISABLED = '0', ENABLED = '1', } + +export enum B2BPermissionsLevel { + USER = 1, + COMPANY = 2, + COMPANY_AND_SUBSIDIARIES = 3, +} + +export interface CompanyHierarchyListProps { + companyId: number; + companyName: string; + parentCompanyName?: string; + parentCompanyId?: number | null; + channelFlag: boolean; +} + +export interface CompanyHierarchyInfoProps { + isEnabledCompanyHierarchy: boolean; + isHasCurrentPagePermission: boolean; + selectCompanyHierarchyId: string | number; + companyHierarchyList: CompanyHierarchyListProps[]; + companyHierarchyAllList: CompanyHierarchyListProps[]; + companyHierarchySelectSubsidiariesList: CompanyHierarchyListProps[]; +} + +export interface CompanyHierarchyProps extends CompanyHierarchyListProps { + children?: CompanyHierarchyProps[]; +} + +export type PagesSubsidiariesPermissionProps = Record< + (typeof PAGES_SUBSIDIARIES_PERMISSION_KEYS)[number]['key'], + boolean +>; diff --git a/apps/storefront/src/types/global.ts b/apps/storefront/src/types/global.ts index 9d47238c4..fd13c5642 100644 --- a/apps/storefront/src/types/global.ts +++ b/apps/storefront/src/types/global.ts @@ -27,3 +27,9 @@ export interface Address { street_2: string; zip: string; } + +export interface ConfigsSwitchStatusProps { + key: string; + id: string; + isEnabled: string; +} diff --git a/apps/storefront/src/types/index.ts b/apps/storefront/src/types/index.ts index 684c7399e..799bc5cba 100644 --- a/apps/storefront/src/types/index.ts +++ b/apps/storefront/src/types/index.ts @@ -7,3 +7,4 @@ export * from './shoppingList'; export * from './company'; export * from './currency'; export * from './common'; +export * from './invoice'; diff --git a/apps/storefront/src/types/invoice.ts b/apps/storefront/src/types/invoice.ts index 7e560fc69..086bbe8e6 100644 --- a/apps/storefront/src/types/invoice.ts +++ b/apps/storefront/src/types/invoice.ts @@ -1,3 +1,15 @@ +export interface CompanyInfoTypes { + companyId: string; + companyName: string; + companyAddress: string; + companyCountry: string; + companyState: string; + companyCity: string; + companyZipCode: string; + phoneNumber: string; + bcId: string; +} + export interface InvoiceList { id: string; createdAt: number; @@ -15,9 +27,11 @@ export interface InvoiceList { pendingPaymentCount: number; openBalance: OpenBalance; originalBalance: OpenBalance; + orderUserId: number; + companyInfo: CompanyInfoTypes; isCollapse?: boolean; disableCurrentCheckbox?: boolean; - sortDirection?: any; + sortDirection?: string; } export interface InvoiceListNode { diff --git a/apps/storefront/src/types/orderDetail.ts b/apps/storefront/src/types/orderDetail.ts index f0f6183f7..968009a50 100644 --- a/apps/storefront/src/types/orderDetail.ts +++ b/apps/storefront/src/types/orderDetail.ts @@ -1,4 +1,5 @@ import { Address } from './global'; +import { CompanyInfoTypes } from './invoice'; export interface OrderProductOption { display_name: string; @@ -251,6 +252,7 @@ export interface B2BOrderData { wrappingCostIncTax: string; wrappingCostTax: string; wrappingCostTaxClassId: number; + companyInfo: CompanyInfoTypes; } export interface OrderDetailsResponse { diff --git a/apps/storefront/src/utils/b3AccountItem.ts b/apps/storefront/src/utils/b3AccountItem.ts index 8ba94270c..2dd9dcc47 100644 --- a/apps/storefront/src/utils/b3AccountItem.ts +++ b/apps/storefront/src/utils/b3AccountItem.ts @@ -5,7 +5,6 @@ interface OpenPageByClickProps { role: number | string; isRegisterAndLogin: boolean; isAgenting: boolean; - IsRealJuniorBuyer: boolean; authorizedPages: string; } @@ -51,8 +50,6 @@ const redirectBcMenus = ( // Supermarket theme if (key.includes('/account.php') && !key.includes('?')) { switch (role) { - case CustomerRole.JUNIOR_BUYER: - return authorizedPages; case CustomerRole.SUPER_ADMIN: return '/dashboard'; default: @@ -73,7 +70,10 @@ const redirectBcMenus = ( : '/dashboard'; } - if (+role === CustomerRole.JUNIOR_BUYER && currentItem?.newTargetUrl?.includes('order_status')) { + if ( + (+role === CustomerRole.JUNIOR_BUYER || +role === CustomerRole.CUSTOM_ROLE) && + currentItem?.newTargetUrl?.includes('order_status') + ) { return authorizedPages; } @@ -99,14 +99,11 @@ const getCurrentLoginUrl = (href: string): string => { const openPageByClick = ({ href, - role, + role: currentRole, isRegisterAndLogin, isAgenting, - IsRealJuniorBuyer, authorizedPages, }: OpenPageByClickProps) => { - const currentRole = !IsRealJuniorBuyer && +role === CustomerRole.JUNIOR_BUYER ? 1 : role; - if (href?.includes('register')) { return '/register'; } diff --git a/apps/storefront/src/utils/b3CheckPermissions/b2bPermissionPath.ts b/apps/storefront/src/utils/b3CheckPermissions/b2bPermissionPath.ts new file mode 100644 index 000000000..20db9eea6 --- /dev/null +++ b/apps/storefront/src/utils/b3CheckPermissions/b2bPermissionPath.ts @@ -0,0 +1,41 @@ +import { PATH_ROUTES } from '@/constants'; +import { CustomerRole } from '@/types'; + +import { checkEveryPermissionsCode } from './check'; +import { b2bPermissionsMap, B2BPermissionsMapParams } from './config'; + +const getEnabledB2BPermissionsMap = (): B2BPermissionsMapParams => { + const keys = Object.keys(b2bPermissionsMap); + return keys.reduce((acc, cur: string) => { + const param: { + code: string; + permissionLevel?: number | string; + } = { + code: (b2bPermissionsMap as Record)[cur], + }; + + const item = checkEveryPermissionsCode(param); + + return { + ...acc, + [cur]: item, + }; + }, {} as B2BPermissionsMapParams); +}; + +const currentB2BExitsPath = (): string => { + const { getShoppingListPermission, getOrderPermission } = getEnabledB2BPermissionsMap(); + + if (getOrderPermission) return PATH_ROUTES.ORDERS; + + if (getShoppingListPermission) return PATH_ROUTES.SHOPPING_LISTS; + + return PATH_ROUTES.ACCOUNT_SETTINGS; +}; + +export const b2bJumpPath = (role: number): string => { + const path = + role === CustomerRole.JUNIOR_BUYER ? PATH_ROUTES.SHOPPING_LISTS : currentB2BExitsPath(); + + return path; +}; diff --git a/apps/storefront/src/utils/b3CheckPermissions/base.ts b/apps/storefront/src/utils/b3CheckPermissions/base.ts index 9d01544d6..e2a9a8ae3 100644 --- a/apps/storefront/src/utils/b3CheckPermissions/base.ts +++ b/apps/storefront/src/utils/b3CheckPermissions/base.ts @@ -1,4 +1,9 @@ -interface PermissionCodesProps { +import { permissionLevels } from '@/constants'; +import { CompanyInfo, Customer } from '@/types'; + +import { b2bPermissionsMap, B2BPermissionsMapParams } from './config'; + +interface PermissionsCodesProp { code: string; permissionLevel?: number | string; } @@ -6,9 +11,40 @@ interface PermissionCodesProps { interface HandleVerifyPermissionCode { permission: string; permissionLevel?: number | string; - permissions: PermissionCodesProps[]; + permissions: PermissionsCodesProp[]; +} + +interface ValidateBasePermissionWithComparisonTypeProps { + level: number; + code?: string; + containOrEqual?: 'contain' | 'equal'; + permissions: PermissionsCodesProp[]; } +interface LevelComparisonProps { + permissionLevel: number; + customer: Customer; + companyInfo: CompanyInfo; + params: { + companyId: number; + userEmail: string; + userId: number; + }; +} + +const pdpButtonAndOthersPermission = [ + 'purchasabilityPermission', + 'quotesCreateActionsPermission', + 'quotesUpdateMessageActionsPermission', + 'shoppingListCreateActionsPermission', + 'shoppingListDuplicateActionsPermission', + 'shoppingListUpdateActionsPermission', + 'shoppingListDeleteActionsPermission', + 'shoppingListCreateItemActionsPermission', + 'shoppingListUpdateItemActionsPermission', + 'shoppingListDeleteItemActionsPermission', +]; + const handleVerifyPermissionCode = ({ permission, permissionLevel, @@ -24,9 +60,9 @@ const handleVerifyPermissionCode = ({ }; export const checkPermissionCode = ( - permissionCodes: PermissionCodesProps, + permissionCodes: PermissionsCodesProp, type: string, - permissions: PermissionCodesProps[], + permissions: PermissionsCodesProp[], ) => { const { code, permissionLevel = '' } = permissionCodes; @@ -49,4 +85,83 @@ export const checkPermissionCode = ( ); }; +export const validateBasePermissionWithComparisonType = ({ + level = 0, + code = '', + containOrEqual = 'equal', + permissions = [], +}: ValidateBasePermissionWithComparisonTypeProps) => { + if (!code) return false; + const info = permissions.find((permission) => permission.code.includes(code)); + + if (!info) return !!info; + + const { permissionLevel = 0 } = info; + + if (containOrEqual === 'equal') return permissionLevel === level; + + return +permissionLevel >= +level; +}; + +export const getCorrespondsConfigurationPermission = ( + permissions: PermissionsCodesProp[], + selectCompanyHierarchyId: number, +) => { + const keys = Object.keys(b2bPermissionsMap); + + const newB3PermissionsList: Record = b2bPermissionsMap; + + return keys.reduce((acc, cur: string) => { + const param = { + code: newB3PermissionsList[cur], + }; + + const item = checkPermissionCode(param, 'every', permissions || []); + + if (pdpButtonAndOthersPermission.includes(cur)) { + const isPdpButtonAndOthersPermission = validateBasePermissionWithComparisonType({ + code: newB3PermissionsList[cur], + containOrEqual: 'contain', + level: selectCompanyHierarchyId + ? permissionLevels.COMPANY_SUBSIDIARIES + : permissionLevels.USER, + permissions, + }); + + return { + ...acc, + [cur]: isPdpButtonAndOthersPermission, + }; + } + + return { + ...acc, + [cur]: item, + }; + }, {} as B2BPermissionsMapParams); +}; + +export const levelComparison = ({ + permissionLevel, + customer, + companyInfo, + params: { companyId, userEmail, userId }, +}: LevelComparisonProps) => { + const currentCompanyId = companyInfo?.id; + const customerId = customer?.id; + const customerB2BId = customer?.b2bId || 0; + const customerEmail = customer?.emailAddress; + + switch (permissionLevel) { + case permissionLevels.COMPANY_SUBSIDIARIES: + return true; + case permissionLevels.COMPANY: + return +companyId === +currentCompanyId; + case permissionLevels.USER: + return userId === +customerId || userId === +customerB2BId || userEmail === customerEmail; + default: + return false; + } +}; + export default checkPermissionCode; diff --git a/apps/storefront/src/utils/b3CheckPermissions/check.ts b/apps/storefront/src/utils/b3CheckPermissions/check.ts new file mode 100644 index 000000000..d1a42cb6a --- /dev/null +++ b/apps/storefront/src/utils/b3CheckPermissions/check.ts @@ -0,0 +1,137 @@ +import { permissionLevels } from '@/constants'; +import { store } from '@/store'; + +import { + checkPermissionCode, + levelComparison, + validateBasePermissionWithComparisonType, +} from './base'; + +export interface PermissionCodesProps { + code: string; + permissionLevel?: number | string; +} + +export interface VerifyLevelPermissionProps { + code: string; + companyId?: number; + userEmail?: string; + userId?: number; +} + +export interface ValidatePermissionWithComparisonTypeProps { + level?: number; + code?: string; + containOrEqual?: 'contain' | 'equal'; + permissions?: PermissionCodesProps[]; +} + +interface VerifyPermissionProps { + code: string; + userId: number; + selectId: number; +} + +export const checkEveryPermissionsCode = (permission: PermissionCodesProps) => { + const newPermissions = store.getState().company.permissions || []; + + return checkPermissionCode(permission, 'every', newPermissions); +}; + +export const checkOneOfPermissionsCode = (permission: PermissionCodesProps) => { + const newPermissions = store.getState().company.permissions || []; + + return checkPermissionCode(permission, 'some', newPermissions); +}; + +export const getPermissionsInfo = (code: string): PermissionCodesProps | undefined => { + const permissions = store.getState().company.permissions || []; + + return permissions.find((permission) => permission.code.includes(code)); +}; + +export const validatePermissionWithComparisonType = ({ + level = 0, + code = '', + containOrEqual = 'equal', + permissions = [], +}: ValidatePermissionWithComparisonTypeProps): boolean => { + const newPermissions = + permissions && permissions.length ? permissions : store.getState().company.permissions || []; + + return validateBasePermissionWithComparisonType({ + level, + code, + containOrEqual, + permissions: newPermissions, + }); +}; + +export const verifyCreatePermission = ( + code: string, + selectCompanyHierarchyId?: number, + permissions?: PermissionCodesProps[], +): boolean => { + return validatePermissionWithComparisonType({ + code, + containOrEqual: 'contain', + level: selectCompanyHierarchyId ? permissionLevels.COMPANY_SUBSIDIARIES : permissionLevels.USER, + permissions, + }); +}; + +/** + * Verifies the user's permission level based on the provided criteria. + * + * @param {Object} params - The function parameters. + * @param {string} params.code - The permission code to check. + * @param {number} params.companyId - The ID of the company to compare, default is 0. + * @param {string} params.userEmail - The email of the user to compare. Either `userEmail` or `userId` is required for user-level validation. + * @param {number} params.userId - The ID of the user to compare. Either `userEmail` or `userId` is required for user-level validation. + * @returns {boolean} - Returns `true` if permission is granted, `false` otherwise. + */ +export const verifyLevelPermission = ({ + code, + companyId = 0, + userEmail = '', + userId = 0, +}: VerifyLevelPermissionProps): boolean => { + const info = getPermissionsInfo(code); + + if (!info) return !!info; + + const { permissionLevel } = info; + + if (!permissionLevel) return false; + const { companyInfo, customer } = store.getState().company || {}; + + return levelComparison({ + permissionLevel: +permissionLevel, + customer, + companyInfo, + params: { + companyId, + userEmail, + userId, + }, + }); +}; + +export const verifySubmitShoppingListSubsidiariesPermission = ({ + code, + userId = 0, + selectId, +}: VerifyPermissionProps): boolean => { + const info = getPermissionsInfo(code); + + if (!info) return !!info; + + const submitShoppingListPermission = verifyLevelPermission({ + code, + userId, + }); + + return info.permissionLevel === permissionLevels.USER && selectId + ? false + : submitShoppingListPermission; +}; diff --git a/apps/storefront/src/utils/b3CheckPermissions/config.ts b/apps/storefront/src/utils/b3CheckPermissions/config.ts new file mode 100644 index 000000000..2cad04c9d --- /dev/null +++ b/apps/storefront/src/utils/b3CheckPermissions/config.ts @@ -0,0 +1,56 @@ +export const b2bPermissionsMap = { + getUserPermissionCode: 'get_users', + getUserDetailPermissionCode: 'get_user_detail', + userCreateActionsPermission: 'create_user', + userUpdateActionsPermission: 'update_user', + userDeleteActionsPermission: 'delete_user', + + getShoppingListPermission: 'get_shopping_lists', + getShoppingListDetailPermission: 'get_shopping_list_detail', + shoppingListCreateActionsPermission: 'create_shopping_list', + /* cspell:disable */ + shoppingListDuplicateActionsPermission: 'deplicate_shopping_list', + shoppingListUpdateActionsPermission: 'update_shopping_list', + shoppingListDeleteActionsPermission: 'delete_shopping_list', + shoppingListCreateItemActionsPermission: 'create_shopping_list_item', + shoppingListUpdateItemActionsPermission: 'update_shopping_list_item', + shoppingListDeleteItemActionsPermission: 'delete_shopping_list_item', + submitShoppingListPermission: 'submit_shopping_list_for_approval', + approveShoppingListPermission: 'approve_draft_shopping_list', + + getAddressesPermission: 'get_addresses', + getAddressDetailPermission: 'get_address_detail', + getDefaultShippingPermission: 'get_default_shipping', + getDefaultBillingPermission: 'get_default_billing', + addressesCreateActionsPermission: 'create_address', + addressesUpdateActionsPermission: 'update_address', + addressesDeleteActionsPermission: 'delete_address', + + getQuotesPermission: 'get_quotes', + getQuoteDetailPermission: 'get_quote_detail', + getQuotePDFPermission: 'get_quote_pdf', + quotesCreateActionsPermission: 'create_quote', + quotesUpdateMessageActionsPermission: 'update_quote_message', + + quoteConvertToOrderPermission: 'checkout_with_quote', + + getOrderPermission: 'get_orders', + getOrderDetailPermission: 'get_order_detail', + + getInvoicesPermission: 'get_invoices', + getInvoiceDetailPermission: 'get_invoice_detail', + getInvoicePDFPermission: 'get_invoice_pdf', + exportInvoicesPermission: 'export_invoices', + getInvoicePaymentsHistoryPermission: 'get_invoice_payments_history', + invoicePayPermission: 'pay_invoice', + + purchasabilityPermission: 'purchase_enable', + + companyHierarchyPermission: 'get_company_subsidiaries', +}; + +type B2BPermissionsMap = typeof b2bPermissionsMap; + +export type B2BPermissionsMapParams = { + [Key in keyof B2BPermissionsMap]: boolean; +}; diff --git a/apps/storefront/src/utils/b3CheckPermissions/index.ts b/apps/storefront/src/utils/b3CheckPermissions/index.ts index 54bb03063..5f6eac436 100644 --- a/apps/storefront/src/utils/b3CheckPermissions/index.ts +++ b/apps/storefront/src/utils/b3CheckPermissions/index.ts @@ -1,26 +1,5 @@ -import { store } from '@/store'; - -import { checkPermissionCode } from './base'; - -interface PermissionCodesProps { - code: string; - permissionLevel?: number | string; -} - -export const checkEveryPermissionsCode = (permission: PermissionCodesProps) => { - const newPermissions = store.getState().company.permissions || []; - - return checkPermissionCode(permission, 'every', newPermissions); -}; - -export const checkOneOfPermissionsCode = (permission: PermissionCodesProps) => { - const newPermissions = store.getState().company.permissions || []; - - return checkPermissionCode(permission, 'some', newPermissions); -}; - -export const getPermissionsInfo = (code: string): PermissionCodesProps | undefined => { - const permissions = store.getState().company.permissions || []; - - return permissions.find((permission) => permission.code.includes(code)); -}; +export * from './check'; +export * from './base'; +export * from './juniorRolePermissions'; +export * from './config'; +export * from './b2bPermissionPath'; diff --git a/apps/storefront/src/utils/b3CheckPermissions/juniorRolePermissions.ts b/apps/storefront/src/utils/b3CheckPermissions/juniorRolePermissions.ts new file mode 100644 index 000000000..89d0fc3ae --- /dev/null +++ b/apps/storefront/src/utils/b3CheckPermissions/juniorRolePermissions.ts @@ -0,0 +1,21 @@ +import { store } from '@/store'; + +import { getCorrespondsConfigurationPermission } from './base'; + +export const setCartPermissions = (isLoggedInAndB2BAccount: boolean) => { + const permissions = store.getState()?.company?.permissions || []; + + const selectCompanyHierarchyId = + store.getState()?.company?.companyHierarchyInfo?.selectCompanyHierarchyId || 0; + + const { purchasabilityPermission } = getCorrespondsConfigurationPermission( + permissions, + +selectCompanyHierarchyId, + ); + + if (!purchasabilityPermission && isLoggedInAndB2BAccount) return; + const style = document.getElementById('b2bPermissions-cartElement-id'); + if (style) { + style.remove(); + } +}; diff --git a/apps/storefront/src/utils/b3CheckPermissions/permission.ts b/apps/storefront/src/utils/b3CheckPermissions/permission.ts deleted file mode 100644 index 109fa06cf..000000000 --- a/apps/storefront/src/utils/b3CheckPermissions/permission.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { checkPermissionCode } from './base'; - -interface PermissionCodesProps { - code: string; - permissionLevel?: number | string; -} - -export const checkEveryPermissionsCode = ( - permission: PermissionCodesProps, - permissions: PermissionCodesProps[], -) => checkPermissionCode(permission, 'every', permissions); - -export const checkOneOfPermissionsCode = ( - permission: PermissionCodesProps, - permissions: PermissionCodesProps[], -) => { - return checkPermissionCode(permission, 'some', permissions); -}; diff --git a/apps/storefront/src/utils/b3Company.ts b/apps/storefront/src/utils/b3Company.ts new file mode 100644 index 000000000..42fb72dd5 --- /dev/null +++ b/apps/storefront/src/utils/b3Company.ts @@ -0,0 +1,52 @@ +import { CompanyHierarchyListProps, CompanyHierarchyProps } from '@/types'; + +type BuildHierarchyProps = { + data: CompanyHierarchyListProps[]; + companyId?: number | null; + parentId?: number | null; +}; + +export const buildHierarchy = ({ + data, + companyId, + parentId, +}: BuildHierarchyProps): CompanyHierarchyProps[] => { + return data + .filter((company) => { + if (companyId) { + return company.companyId === companyId; + } + if (!parentId) { + return company.parentCompanyId === null || company.parentCompanyId === 0; + } + + return company.parentCompanyId === parentId; + }) + .map((company) => ({ + ...company, + children: buildHierarchy({ + data, + parentId: company.companyId, + }), + })); +}; + +export const flattenBuildHierarchyCompanies = (company: CompanyHierarchyProps) => { + let result: CompanyHierarchyProps[] = []; + + result.push({ + companyId: company.companyId, + companyName: company.companyName, + parentCompanyId: company.parentCompanyId, + parentCompanyName: company.parentCompanyName, + channelFlag: company.channelFlag, + }); + + if (company.children && company.children.length > 0) { + company.children.forEach((child) => { + result = result.concat(flattenBuildHierarchyCompanies(child)); + }); + } + + return result; +}; diff --git a/apps/storefront/src/utils/b3Init.ts b/apps/storefront/src/utils/b3Init.ts index f74e9d400..1413ac847 100644 --- a/apps/storefront/src/utils/b3Init.ts +++ b/apps/storefront/src/utils/b3Init.ts @@ -1,7 +1,5 @@ import { CustomerRole, FeatureEnabled } from '@/types'; -import { getB3PermissionsList } from './b3RolePermissions'; - export interface QuoteConfigItem { [key: string]: string; } @@ -44,8 +42,6 @@ export const getQuoteEnabled = ( const shoppingListEnabled = storefrontConfig.shoppingLists; const registerEnabled = storefrontConfig.tradeProfessionalApplication; - const { shoppingListActionsPermission, quotesActionsPermission } = getB3PermissionsList(); - quoteConfig.forEach((config) => { switch (config.key) { case 'quote_customer': @@ -101,12 +97,9 @@ export const getQuoteEnabled = ( cartQuoteEnabled = cartQuoteEnabled && guestEnabled === FeatureEnabled.ENABLED; productShoppingListEnabled = shoppingListEnabled && slGuestEnabled; } else if (isB2BUser) { - productQuoteEnabled = - productQuoteEnabled && b2bUserEnabled === FeatureEnabled.ENABLED && quotesActionsPermission; - cartQuoteEnabled = - cartQuoteEnabled && b2bUserEnabled === FeatureEnabled.ENABLED && quotesActionsPermission; - productShoppingListEnabled = - shoppingListEnabled && slB2bUserEnabled && shoppingListActionsPermission; + productQuoteEnabled = productQuoteEnabled && b2bUserEnabled === FeatureEnabled.ENABLED; + cartQuoteEnabled = cartQuoteEnabled && b2bUserEnabled === FeatureEnabled.ENABLED; + productShoppingListEnabled = shoppingListEnabled && slB2bUserEnabled; if (role === CustomerRole.SUPER_ADMIN && !isAgenting) { productQuoteEnabled = false; diff --git a/apps/storefront/src/utils/b3Product/shared/config.ts b/apps/storefront/src/utils/b3Product/shared/config.ts index a21334149..b4818f2ba 100644 --- a/apps/storefront/src/utils/b3Product/shared/config.ts +++ b/apps/storefront/src/utils/b3Product/shared/config.ts @@ -2,16 +2,16 @@ import { LangFormatFunction } from '@b3/lang'; import format from 'date-fns/format'; import isEmpty from 'lodash-es/isEmpty'; -import { AllOptionProps, ALlOptionValue, Product } from '@/types/products'; -import b2bLogger from '@/utils/b3Logger'; - import { BcCalculatedPrice, + CompanyInfoTypes, OptionValueProps, ShoppingListProductItem, ShoppingListSelectProductOption, SimpleObject, -} from '../../../types'; +} from '@/types'; +import { AllOptionProps, ALlOptionValue, Product } from '@/types/products'; +import b2bLogger from '@/utils/b3Logger'; export interface ShoppingListInfoProps { name: string; @@ -24,6 +24,7 @@ export interface ShoppingListInfoProps { }; customerInfo: CustomerInfoProps; isOwner: boolean; + companyInfo: CompanyInfoTypes; } export interface CustomerInfoProps { @@ -57,10 +58,10 @@ export interface ProductInfoProps { variantId: number; variantSku: string; productsSearch: CustomFieldItems; - picklistIds?: number[]; + pickListIds?: number[]; modifierPrices?: ModifierPrices[]; baseAllPrice?: number | string; - baseAllPricetax?: number | string; + baseAllPriceTax?: number | string; currentProductPrices?: BcCalculatedPrice; extraProductPrices?: BcCalculatedPrice[]; [key: string]: any; @@ -540,7 +541,7 @@ interface AllOptionsProps { type: string; } -export const addlineItems = (products: ProductsProps[]) => { +export const addLineItems = (products: ProductsProps[]) => { const lineItems = products.map((item: ProductsProps) => { const { node } = item; diff --git a/apps/storefront/src/utils/b3RolePermissions/b3PermissionsList.ts b/apps/storefront/src/utils/b3RolePermissions/b3PermissionsList.ts deleted file mode 100644 index 03d69c866..000000000 --- a/apps/storefront/src/utils/b3RolePermissions/b3PermissionsList.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { checkEveryPermissionsCode } from '../b3CheckPermissions'; - -import { B2BPermissionParams, b2bPermissionsList } from './config'; - -interface PermissionLevelInfoProps { - permissionType: string; - permissionLevel?: number | string; -} - -const getB3PermissionsList = ( - permissionLevelInfo?: PermissionLevelInfoProps[], -): B2BPermissionParams => { - const keys = Object.keys(b2bPermissionsList); - return keys.reduce((acc, cur: string) => { - const param: { - code: string; - permissionLevel?: number | string; - } = { - code: (b2bPermissionsList as Record)[cur], - }; - - if (permissionLevelInfo && permissionLevelInfo.length > 0) { - const currentPermission = permissionLevelInfo.find( - (item: PermissionLevelInfoProps) => item.permissionType === cur, - ); - - if (currentPermission) { - param.permissionLevel = currentPermission.permissionLevel; - } - } - - const item = checkEveryPermissionsCode(param); - - return { - ...acc, - [cur]: item, - }; - }, {} as B2BPermissionParams); -}; - -export default getB3PermissionsList; diff --git a/apps/storefront/src/utils/b3RolePermissions/config.ts b/apps/storefront/src/utils/b3RolePermissions/config.ts deleted file mode 100644 index 5e140851a..000000000 --- a/apps/storefront/src/utils/b3RolePermissions/config.ts +++ /dev/null @@ -1,30 +0,0 @@ -export const b2bPermissionsList = { - getUserPermissionCode: 'get_users, get_user_detail', - userActionsPermission: 'create_user, update_user, delete_user', - getShoppingListPermission: 'get_shopping_lists, get_shopping_list_detail', - shoppingListActionsPermission: - /* cspell:disable-next-line */ - 'create_shopping_list, deplicate_shopping_list, update_shopping_list, delete_shopping_list, create_shopping_list_item, update_shopping_list_item, delete_shopping_list_item', - submitShoppingListPermission: 'submit_shopping_list_for_approval', - approveShoppingListPermission: 'approve_draft_shopping_list', - getAddressesPermission: - 'get_addresses, get_address_detail, get_default_shipping, get_default_billing', - addressesActionsPermission: 'create_address, update_address, delete_address', - getQuotesPermission: 'get_quotes, get_quote_detail, get_quote_pdf', - quotesActionsPermission: 'create_quote, update_quote_message', - quoteConvertToOrderPermission: 'checkout_with_quote', - getOrderPermission: 'get_orders, get_order_detail', - getInvoicesPermission: - 'get_invoices, get_invoice_detail, get_invoice_pdf, export_invoices, get_invoice_payments_history', - invoicePayPermission: 'pay_invoice', - /* cspell:disable-next-line */ - purchasabilityPermission: 'purchase_enable', -}; - -type B3PermissionsList = typeof b2bPermissionsList; - -export type B2BPermissionParams = { - [Key in keyof B3PermissionsList]: boolean; -}; - -export default b2bPermissionsList; diff --git a/apps/storefront/src/utils/b3RolePermissions/index.ts b/apps/storefront/src/utils/b3RolePermissions/index.ts deleted file mode 100644 index da3395baf..000000000 --- a/apps/storefront/src/utils/b3RolePermissions/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -// eslint-disable-next-line -export { default as setCartPermissions } from './juniorRolePermissions' -export { default as getB3PermissionsList } from './b3PermissionsList'; diff --git a/apps/storefront/src/utils/b3RolePermissions/juniorRolePermissions.ts b/apps/storefront/src/utils/b3RolePermissions/juniorRolePermissions.ts deleted file mode 100644 index fc4a5a8f7..000000000 --- a/apps/storefront/src/utils/b3RolePermissions/juniorRolePermissions.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { checkEveryPermissionsCode } from '../b3CheckPermissions'; - -const setCartPermissions = (isLoggedInAndB2BAccount: boolean) => { - const purchasbility = checkEveryPermissionsCode({ code: 'purchase_enable' }); - - if (!purchasbility && isLoggedInAndB2BAccount) return; - const style = document.getElementById('b2bPermissions-cartElement-id'); - if (style) { - style.remove(); - } -}; - -export default setCartPermissions; diff --git a/apps/storefront/src/utils/b3ShoppingList/b3ShoppingList.ts b/apps/storefront/src/utils/b3ShoppingList/b3ShoppingList.ts index 97f05feab..2a3bd6692 100644 --- a/apps/storefront/src/utils/b3ShoppingList/b3ShoppingList.ts +++ b/apps/storefront/src/utils/b3ShoppingList/b3ShoppingList.ts @@ -1,6 +1,7 @@ import { createB2BShoppingList, createBcShoppingList } from '@/shared/service/b2b'; +import { store } from '@/store'; -import { getB3PermissionsList } from '../b3RolePermissions'; +import { b2bPermissionsMap, validatePermissionWithComparisonType } from '../b3CheckPermissions'; import { channelId } from '../basicConfig'; interface CreateShoppingListParams { @@ -18,8 +19,15 @@ CreateShoppingListParams) => { const createSL = isB2BUser ? createB2BShoppingList : createBcShoppingList; if (isB2BUser) { - const { submitShoppingListPermission } = getB3PermissionsList(); + const submitShoppingListPermission = validatePermissionWithComparisonType({ + code: b2bPermissionsMap.submitShoppingListPermission, + }); + const selectCompanyHierarchyId = + store.getState()?.company?.companyHierarchyInfo?.selectCompanyHierarchyId || 0; createShoppingData.status = submitShoppingListPermission ? 30 : 0; + if (selectCompanyHierarchyId) { + createShoppingData.companyId = selectCompanyHierarchyId; + } } else { createShoppingData.channelId = channelId; } diff --git a/apps/storefront/src/utils/index.ts b/apps/storefront/src/utils/index.ts index c5909def7..7d3d32643 100644 --- a/apps/storefront/src/utils/index.ts +++ b/apps/storefront/src/utils/index.ts @@ -1,9 +1,3 @@ -import { checkPermissionCode } from './b3CheckPermissions/base'; -import { - checkEveryPermissionsCode, - checkOneOfPermissionsCode, - getPermissionsInfo, -} from './b3CheckPermissions/index'; import b2bGetVariantImageByVariantInfo from './b2bGetVariantImageByVariantInfo'; import { openPageByClick, redirectBcMenus, removeBCMenus } from './b3AccountItem'; import currencyFormat, { @@ -20,7 +14,6 @@ import { showPageMask } from './b3PageMask'; import distanceDay from './b3Picker'; import { getProductPriceIncTax, getProductPriceIncTaxOrExTaxBySetting } from './b3Price'; import b2bPrintInvoice from './b3PrintInvoice'; -import { getB3PermissionsList, setCartPermissions } from './b3RolePermissions'; import { serialize } from './b3Serialize'; import { B3LStorage, B3SStorage } from './b3Storage'; import { globalSnackbar, snackbar } from './b3Tip'; @@ -44,14 +37,14 @@ export { loginJump } from './b3Login'; // TODO: Clean this up export { default as hideStorefrontElement } from './b3HideStorefrontElement'; +export * from './b3Company'; +export * from './b3CheckPermissions'; + export { b2bPrintInvoice, b2bGetVariantImageByVariantInfo, B3LStorage, B3SStorage, - checkEveryPermissionsCode, - checkOneOfPermissionsCode, - checkPermissionCode, convertArrayToGraphql, convertObjectToGraphql, convertObjectOrArrayKeysToCamel, @@ -79,9 +72,6 @@ export { redirectBcMenus, removeBCMenus, serialize, - setCartPermissions, - getB3PermissionsList, - getPermissionsInfo, showPageMask, snackbar, validatorRules, diff --git a/apps/storefront/src/utils/loginInfo.ts b/apps/storefront/src/utils/loginInfo.ts index bfb6acfba..fdd82ee54 100644 --- a/apps/storefront/src/utils/loginInfo.ts +++ b/apps/storefront/src/utils/loginInfo.ts @@ -1,9 +1,12 @@ import { + endUserMasqueradingCompany, getAgentInfo, getB2BCompanyUserInfo, getB2BToken, getBCGraphqlToken, + getCompanySubsidiaries, getUserCompany, + getUserMasqueradingCompany, } from '@/shared/service/b2b'; import { getCurrentCustomerJWT, getCustomerInfo } from '@/shared/service/bc'; import { getAppClientId } from '@/shared/service/request/base'; @@ -18,6 +21,7 @@ import { clearCompanySlice, setB2BToken, setBcGraphQLToken, + setCompanyHierarchyInfoModules, setCompanyInfo, setCompanyStatus, setCurrentCustomerJWT, @@ -26,7 +30,8 @@ import { setPermissionModules, } from '@/store/slices/company'; import { resetDraftQuoteInfo, resetDraftQuoteList } from '@/store/slices/quoteInfo'; -import { CompanyStatus, CustomerRole, LoginTypes, UserTypes } from '@/types'; +import { CompanyStatus, CustomerRole, CustomerRoleName, LoginTypes, UserTypes } from '@/types'; +import { getAccountHierarchyIsEnabled } from '@/utils/storefrontConfig'; import b2bLogger from './b3Logger'; import { B3LStorage, B3SStorage } from './b3Storage'; @@ -141,7 +146,12 @@ export const clearCurrentCustomerInfo = async () => { // 3: inactive // 4: deleted -const VALID_ROLES = [CustomerRole.ADMIN, CustomerRole.SENIOR_BUYER, CustomerRole.JUNIOR_BUYER]; +const VALID_ROLES = [ + CustomerRole.ADMIN, + CustomerRole.SENIOR_BUYER, + CustomerRole.JUNIOR_BUYER, + CustomerRole.CUSTOM_ROLE, +]; export const getCompanyInfo = async ( role: number | string, @@ -291,7 +301,14 @@ export const getCurrentCustomerInfo: (b2bToken?: string) => Promise< const companyUserInfo = await getCompanyUserInfo(); if (companyUserInfo && customerId) { - const { userType, role, id, companyRoleName, permissions } = companyUserInfo; + const { userType, id, companyRoleName, permissions } = companyUserInfo; + + let { role } = companyUserInfo; + + role = + role === CustomerRole.JUNIOR_BUYER && companyRoleName !== CustomerRoleName.JUNIOR_BUYER_NAME + ? CustomerRole.CUSTOM_ROLE + : role; const [companyInfo] = await Promise.all([ getCompanyInfo(role, id, userType), @@ -323,6 +340,40 @@ export const getCurrentCustomerInfo: (b2bToken?: string) => Promise< companyName: companyInfo.companyName, }; + if ( + role === CustomerRole.ADMIN || + role === CustomerRole.SENIOR_BUYER || + role === CustomerRole.JUNIOR_BUYER || + role === CustomerRole.CUSTOM_ROLE + ) { + const isEnabledAccountHierarchy = await getAccountHierarchyIsEnabled(); + + if (isEnabledAccountHierarchy) { + const [{ companySubsidiaries }, { userMasqueradingCompany }] = await Promise.all([ + getCompanySubsidiaries(), + getUserMasqueradingCompany(), + ]); + + if (userMasqueradingCompany?.companyId) { + await endUserMasqueradingCompany(); + } + + store.dispatch( + setCompanyHierarchyInfoModules({ + companyHierarchyAllList: companySubsidiaries, + isEnabledCompanyHierarchy: isEnabledAccountHierarchy, + }), + ); + } else { + store.dispatch( + setCompanyHierarchyInfoModules({ + isEnabledCompanyHierarchy: false, + companyHierarchyAllList: [], + }), + ); + } + } + store.dispatch(resetDraftQuoteList()); store.dispatch(resetDraftQuoteInfo()); store.dispatch(clearMasqueradeCompany()); diff --git a/apps/storefront/src/utils/storefrontConfig.ts b/apps/storefront/src/utils/storefrontConfig.ts index 836eaf5f0..38680ac55 100644 --- a/apps/storefront/src/utils/storefrontConfig.ts +++ b/apps/storefront/src/utils/storefrontConfig.ts @@ -6,6 +6,7 @@ import { getB2BRegisterLogo, getBCStoreChannelId, getCurrencies, + getStoreConfigsSwitchStatus, getStorefrontConfig, getStorefrontConfigs, getStorefrontDefaultLanguages, @@ -91,6 +92,10 @@ const storeforntKeys: StoreforntKeysProps[] = [ key: 'masquerade_button', name: 'masqueradeButton', }, + { + key: 'switch_account_button', + name: 'switchAccountButton', + }, { key: 'quote_floating_action_button', name: 'floatingAction', @@ -205,6 +210,16 @@ const getTemPlateConfig = async (dispatch: any, dispatchGlobal: any) => { }; } + if (storeforntKey.key === 'switch_account_button') { + storefrontConfig.extraFields = { + ...item.extraFields, + color: item.extraFields?.color || '#ED6C02', + location: item.extraFields?.location || ' bottomLeft', + horizontalPadding: item.extraFields?.horizontalPadding || '', + verticalPadding: item.extraFields?.verticalPadding || '', + }; + } + if (storeforntKey.key === 'quote_floating_action_button') { storefrontConfig.extraFields = { ...item.extraFields, @@ -309,6 +324,14 @@ const getQuoteConfig = async (dispatch: DispatchProps) => { }); }; +export const getAccountHierarchyIsEnabled = async () => { + const { storeConfigSwitchStatus } = await getStoreConfigsSwitchStatus('account_hierarchy'); + if (!storeConfigSwitchStatus) return false; + const { isEnabled } = storeConfigSwitchStatus; + + return isEnabled === '1'; +}; + const setStorefrontConfig = async (dispatch: DispatchProps) => { const { storefrontConfig: { config: storefrontConfig }, diff --git a/packages/lang/locales/en.json b/packages/lang/locales/en.json index 1f9cb3145..4eb91eb5c 100644 --- a/packages/lang/locales/en.json +++ b/packages/lang/locales/en.json @@ -69,13 +69,14 @@ "global.navMenu.dashboard": "Dashboard", "global.navMenu.orders": "My orders", "global.navMenu.companyOrders": "Company orders", - "global.navMenu.invoice": "Invoice", + "global.navMenu.invoice": "Invoices", "global.navMenu.quotes": "Quotes", "global.navMenu.shoppingLists": "Shopping lists", "global.navMenu.quickOrder": "Quick order", "global.navMenu.addresses": "Addresses", "global.navMenu.userManagement": "User management", "global.navMenu.accountSettings": "Account settings", + "global.navMenu.companyHierarchy": "Company hierarchy", "global.B3MainHeader.superAdmin": "Super admin", "global.B3MainHeader.signIn": "Sign in", "global.B3MainHeader.home": "Home", @@ -113,6 +114,7 @@ "global.customStyles.addToAllQuoteBtn": "Add All To Quote", "global.customStyles.shoppingListBtn": "Add to Shopping List", "global.masquerade.youAreMasqueradeAs": "You are masquerade as", + "global.companyHierarchy.externalBtn": "You are representing", "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.", "global.B3Upload.downloadErrorResults": "Download error results", @@ -122,6 +124,9 @@ "global.statusNotifications.willGainAccessToBusinessFeatProductsAndPricingAfterApproval": "Your business account is pending approval. You will gain access to business account features, products, and pricing after account approval.", "global.statusNotifications.checkEmailLetterWithDetails": "Your business account has been approved. Check your email inbox, we sent you a letter with all details.", "global.statusNotifications.checkEmailLetterWithDetailsResubmitApplication": "Your business account has been rejected. Check your email inbox, we sent you a letter with all details. You can resubmit your application.", + "global.B2BAutoCompleteCheckbox.input.label": "Company", + "global.B2BSwitchCompanyModal.title": "Switch company", + "global.B2BSwitchCompanyModal.confirm.button": "Switch company", "dashboard.company": "Company", "dashboard.admin": "Admin", @@ -160,7 +165,8 @@ "invoice.exportSelectedAsCsv": "Export selected as CSV", "invoice.exportFilteredAsCsv": "Export filtered as CSV", "invoice.pdfUrlResolutionError": "pdf url resolution error", - "invoice.headers.invoice": "Invoice", + "invoice.headers.invoice": "Invoices", + "invoice.headers.companyName": "Company", "invoice.headers.order": "Order", "invoice.headers.invoiceDate": "Invoice date", "invoice.headers.dueDate": "Due date", @@ -367,6 +373,9 @@ "orderDetail.summary.grandTotal": "Grand total", "orderDetail.addToShoppingList.productsAdded": "Products were added to your shopping list", "orderDetail.viewShoppingList": "View shopping list", + "orderDetail.anotherCompany.tips": "This order is related to another company. To reorder, add to a shopping list, or perform other actions, you need to switch to that company.", + "orderDetail.switchCompany.title": "Switch company", + "orderDetail.switchCompany.content.tipsText": "To continue you have to switch company. Switching to a different company will refresh your shopping cart.", "addresses.noPermissionToAdd": "You do not have permission to add new address, please contact store owner", "addresses.noPermissionToEdit": "You do not have permission to edit address, please contact store owner", @@ -710,5 +719,11 @@ "payment.paymentTowardsInvoices": "You made payments towards the invoices shown below", "payment.invoiceNumber": "Invoice#", "payment.amountPaid": "Amount paid", - "payment.okButton": "Ok" + "payment.okButton": "Ok", + + "companyHierarchy.table.name": "Name", + "companyHierarchy.dialog.title": "Switch company", + "companyHierarchy.dialog.content": "Switching to a different company will refresh your shopping cart. Do you want to continue?", + "companyHierarchy.chip.currentCompany": "Your company", + "companyHierarchy.chip.selectCompany": "Representing" }