diff --git a/apps/storefront/src/components/filter/B3Filter.tsx b/apps/storefront/src/components/filter/B3Filter.tsx index dd2fee48..db48a9c2 100644 --- a/apps/storefront/src/components/filter/B3Filter.tsx +++ b/apps/storefront/src/components/filter/B3Filter.tsx @@ -1,6 +1,7 @@ import { useState, ReactElement, + ReactNode, } from 'react' import { Box, @@ -209,11 +210,10 @@ const B3Filter: (props: B3FilterProps) => ReactElement = (props) => fiterMoreInfo={fiterMoreInfo} onChange={handleFilterChange} /> - - { + { customButtomConfig?.isEnabled && ( ) } - + + { + customButtomConfig?.isEnabled && ( + + ) + } ) } diff --git a/apps/storefront/src/components/table/B3Table.tsx b/apps/storefront/src/components/table/B3Table.tsx index 0a80a0e6..66af3934 100644 --- a/apps/storefront/src/components/table/B3Table.tsx +++ b/apps/storefront/src/components/table/B3Table.tsx @@ -146,16 +146,20 @@ export const B3Table:(props: TableProps) => ReactElement = ({ spacing={itemSpacing} > { - listItems.map((row, index) => ( - - <> - {row?.node && renderItem(row.node, index)} - - - )) + listItems.map((row, index) => { + const node = row.node || row || {} + return ( + + <> + {row?.node && renderItem(row.node, index)} + + + ) + }) } { diff --git a/apps/storefront/src/pages/address/Address.tsx b/apps/storefront/src/pages/address/Address.tsx new file mode 100644 index 00000000..f63ae899 --- /dev/null +++ b/apps/storefront/src/pages/address/Address.tsx @@ -0,0 +1,232 @@ +import { + Box, Button, +} from '@mui/material' + +import { + useEffect, + useContext, + useState, +} from 'react' + +import { + useMobile, +} from '@/hooks' + +import B3Filter from '../../components/filter/B3Filter' +import { + AddressItemCard, +} from './components/AddressItemCard' +import { + Pagination, + B3Table, +} from '@/components/B3Table' +import { + B3Sping, +} from '@/components/spin/B3Sping' + +import { + GlobaledContext, +} from '@/shared/global' + +import { + getB2BCustomerAddress, +} from '@/shared/service/b2b' + +import { + filterFormConfig, + filterSortConfig, + filterPickerConfig, +} from './shared/config' + +import { + AddressItemType, +} from '../../types/address' + +const Address = () => { + const { + state: { + role, + isB2BUser, + companyInfo: { + id: companyId, + }, + }, + } = useContext(GlobaledContext) + + const [isRequestLoading, setIsRequestLoading] = useState(false) + const [addressList, setAddressList] = useState([]) + const [searchParams, setSearchParams] = useState({}) + const [pagination, setPagination] = useState({ + offset: 0, + count: 0, + first: 1, + }) + + const [isMobile] = useMobile() + + const getAddressList = async (pagination: Pagination, params = {}) => { + setIsRequestLoading(true) + + try { + const addressKey = isB2BUser ? 'addresses' : 'customerAddresses' + const { + [addressKey]: { + edges: list = [], + totalCount, + }, + }: CustomFieldItems = await getB2BCustomerAddress({ + companyId, + ...params, + ...pagination, + }) + + if (isMobile) { + const newList = pagination.offset > 0 ? [...addressList, ...list] : list + setAddressList(newList) + } else { + setAddressList(list) + } + + setPagination({ + ...pagination, + count: totalCount, + }) + } finally { + setIsRequestLoading(false) + } + } + + const handleChange = (key: string, value: string) => { + if (key === 'search') { + const params = { + ...searchParams, + search: value, + } + + setSearchParams(params) + getAddressList({ + ...pagination, + offset: 0, + }, params) + } + } + const handleFilterChange = (values: {[key: string]: string | number | Date}) => { + const params = { + ...searchParams, + country: values.country || '', + state: values.state || '', + city: values.city || '', + } + setSearchParams(params) + getAddressList({ + ...pagination, + offset: 0, + }, params) + } + + const handlePaginationChange = (pagination: Pagination) => { + setPagination(pagination) + getAddressList(pagination, searchParams) + } + + const [editPermission, setEditPermission] = useState(false) + const [isOpenPermission, setIsOpenPermission] = useState(false) + const isAdmin = !role || role === 3 + + const getEditPermission = () => { + if (isAdmin) { + // TODO get config + } + } + + useEffect(() => { + getEditPermission() + }, []) + + const checkPermission = () => { + if (!isAdmin) { + return false + } + if (!editPermission) { + setIsOpenPermission(true) + return false + } + return true + } + + const handleCreate = () => { + if (!checkPermission()) { + return + } + // TODO show create modal + console.log('create') + } + + const handleEdit = () => { + if (!checkPermission()) { + return + } + // TODO show edit modal + console.log('edit') + } + + const handleDelete = () => { + if (!checkPermission()) { + return + } + // TODO show delete modal + console.log('delete') + } + + const handleSetDefault = () => { + // TODO show delete modal + console.log('setDefault') + } + + const AddButtonConfig = { + isEnabled: isAdmin, + customLabel: 'Add new address', + } + + return ( + + + + ( + + )} + /> + + + ) +} + +export default Address diff --git a/apps/storefront/src/pages/address/components/AddressItemCard.tsx b/apps/storefront/src/pages/address/components/AddressItemCard.tsx new file mode 100644 index 00000000..3eefe394 --- /dev/null +++ b/apps/storefront/src/pages/address/components/AddressItemCard.tsx @@ -0,0 +1,174 @@ +import { + useContext, +} from 'react' + +import Card from '@mui/material/Card' +import CardContent from '@mui/material/CardContent' +import Box from '@mui/material/Box' +import Typography from '@mui/material/Typography' +import Button from '@mui/material/Button' +import IconButton from '@mui/material/IconButton' + +import DeleteIcon from '@mui/icons-material/Delete' +import EditIcon from '@mui/icons-material/Edit' + +import styled from '@emotion/styled' + +import { + useTheme, + Theme, +} from '@mui/material' + +import { + GlobaledContext, +} from '@/shared/global' + +import { + AddressItemType, +} from '../../../types/address' + +import { + B3Tag, +} from '@/components/B3Tag' + +export interface OrderItemCardProps { + item: AddressItemType, + onEdit: (data: AddressItemType) => void + onDelete: (data: AddressItemType) => void + onSetDefault: (data: AddressItemType) => void +} + +interface TagBoxProps { + marginBottom: number | string +} + +const TagBox = styled('div')(({ + marginBottom, +}: TagBoxProps) => ({ + marginBottom, + '& > span:not(:last-child)': { + marginRight: '4px', + }, +})) + +interface FlexProps { + theme?: Theme +} + +const Flex = styled('div')(({ + theme, +}: FlexProps) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + marginTop: theme!.spacing(3), +})) + +export const AddressItemCard = (props: OrderItemCardProps) => { + const { + item: addressInfo, + onEdit, + onDelete, + onSetDefault, + } = props + + const theme = useTheme() + + const { + state: { + role, + isB2BUser, + }, + } = useContext(GlobaledContext) + + const hasPermission = !role || role === 3 + + return ( + + + {addressInfo.label && ( + + {addressInfo.label} + + )} + + + { addressInfo.isDefaultShipping === 1 && ( + + Default shipping + + )} + { addressInfo.isDefaultBilling === 1 && ( + + Default billing + + )} + + + + {`${addressInfo.firstName} ${addressInfo.lastName}`} + + company name + {addressInfo.addressLine1} + {addressInfo.addressLine2} + + {`${addressInfo.city}, ${addressInfo.state} ${addressInfo.zipCode}, ${addressInfo.country}`} + + {addressInfo.phoneNumber} + + { + hasPermission && ( + + + + { onEdit(addressInfo) }} + > + + + { onDelete(addressInfo) }} + > + + + + + ) + } + + + ) +} diff --git a/apps/storefront/src/pages/address/shared/config.ts b/apps/storefront/src/pages/address/shared/config.ts new file mode 100644 index 00000000..d8472efb --- /dev/null +++ b/apps/storefront/src/pages/address/shared/config.ts @@ -0,0 +1,43 @@ +export const filterFormConfig = [ + { + name: 'city', + label: 'City', + required: false, + default: '', + fieldType: 'text', + xs: 12, + variant: 'filled', + size: 'small', + }, + { + name: 'state', + label: 'State', + required: false, + default: '', + fieldType: 'text', + xs: 12, + variant: 'filled', + size: 'small', + }, + { + name: 'country', + label: 'Country', + required: false, + default: '', + fieldType: 'text', + xs: 12, + variant: 'filled', + size: 'small', + }, +] + +export const filterSortConfig = { + isEnabled: false, + sortByList: [], + sortByLabel: '', +} + +export const filterPickerConfig = { + isEnabled: false, + label: '', +} diff --git a/apps/storefront/src/pages/orderDetail/components/OrderAction.tsx b/apps/storefront/src/pages/orderDetail/components/OrderAction.tsx index 3a8fce9b..afd7c207 100644 --- a/apps/storefront/src/pages/orderDetail/components/OrderAction.tsx +++ b/apps/storefront/src/pages/orderDetail/components/OrderAction.tsx @@ -335,7 +335,7 @@ export const OrderAction = (props: OrderActionProps) => { zip, country, city, - } = billingAddress + } = billingAddress || {} const paymentAddress = { paymentMethod: `Payment by ${paymentMethod}`, name: `${firstName} ${lastName}`, @@ -395,7 +395,7 @@ export const OrderAction = (props: OrderActionProps) => { { header: 'Order summary', key: 'order-summary', - subtitle: `Purchased by ${name} on ${format(+(updatedAt || 0) * 1000, 'dd MMM yy')}.`, + subtitle: (updatedAt && name ? `Purchased by ${name} on ${format(+updatedAt * 1000, 'dd MMM yy')}.` : ''), buttons, infos: { money, @@ -405,7 +405,7 @@ export const OrderAction = (props: OrderActionProps) => { { header: 'Payment', key: 'payment', - subtitle: `Paid in full on ${format(Date.parse(createAt || ''), 'dd MMM yy')}.`, + subtitle: (createAt ? `Paid in full on ${format(Date.parse(createAt), 'dd MMM yy')}.` : ''), buttons: [ { value: isB2BUser ? 'view invoice' : 'print invoice', diff --git a/apps/storefront/src/shared/routes/routes.ts b/apps/storefront/src/shared/routes/routes.ts index 655992ab..f376d213 100644 --- a/apps/storefront/src/shared/routes/routes.ts +++ b/apps/storefront/src/shared/routes/routes.ts @@ -14,6 +14,8 @@ const InvoiceDetail = lazy(() => import('../../pages/invoiceDetail/InvoiceDetail const Usermanagement = lazy(() => import('../../pages/usermanagement/Usermanagement')) +const AddressList = lazy(() => import('../../pages/address/Address')) + type OrderItem = typeof OrderList export interface RouteItem { @@ -54,11 +56,11 @@ const routes: RouteItem[] = [ component: InvoiceDetail, }, { - path: '/addresss', - name: 'Addresss', - wsKey: 'router-orders', + path: '/addresses', + name: 'Addresses', + wsKey: 'router-address', isMenuItem: true, - component: Dashboard, + component: AddressList, }, { path: '/user-management', diff --git a/apps/storefront/src/shared/service/b2b/graphql/address.ts b/apps/storefront/src/shared/service/b2b/graphql/address.ts new file mode 100644 index 00000000..298a8f47 --- /dev/null +++ b/apps/storefront/src/shared/service/b2b/graphql/address.ts @@ -0,0 +1,63 @@ +import { + B3Request, +} from '../../request/b3Fetch' + +const getCustomerAddress = ({ + companyId = 0, + offset = 0, + first = 50, + search = '', + country = '', + state = '', + city = '', +}) => `{ + addresses ( + companyId: ${companyId} + offset: ${offset} + first: ${first} + search: "${search}" + country: "${country}" + state: "${state}" + city: "${city}" + ){ + totalCount, + pageInfo{ + hasNextPage, + hasPreviousPage, + }, + edges{ + node{ + id + createdAt + updatedAt + firstName + lastName + isShipping + isBilling + addressLine1 + addressLine2 + address + city + state + stateCode + country + countryCode + zipCode + phoneNumber + isActive + label + uuid + extraFields { + fieldName + fieldValue + } + isDefaultShipping + isDefaultBilling + } + } + } +}` + +export const getB2BCustomerAddress = (data: CustomFieldItems = {}): CustomFieldItems => B3Request.graphqlB2B({ + query: getCustomerAddress(data), +}) diff --git a/apps/storefront/src/shared/service/b2b/index.ts b/apps/storefront/src/shared/service/b2b/index.ts index 67e0ccd3..5eaac230 100644 --- a/apps/storefront/src/shared/service/b2b/index.ts +++ b/apps/storefront/src/shared/service/b2b/index.ts @@ -61,6 +61,10 @@ import { deleteUsers, } from './graphql/users' +import { + getB2BCustomerAddress, +} from './graphql/address' + export { getB2BRegisterCustomFields, getB2BRegisterLogo, @@ -95,4 +99,5 @@ export { getUsers, addOrUpdateUsers, deleteUsers, + getB2BCustomerAddress, } diff --git a/apps/storefront/src/types/address.ts b/apps/storefront/src/types/address.ts new file mode 100644 index 00000000..d170416f --- /dev/null +++ b/apps/storefront/src/types/address.ts @@ -0,0 +1,30 @@ +export interface AddressExtraFieldType { + fieldName: string, + fieldValue: string, +} + +export interface AddressItemType { + id: number, + createdAt: number, + updatedAt: number, + firstName: string + lastName: string + isShipping: number + isBilling: number + addressLine1: string + addressLine2: string + address: string + city: string + state: string + stateCode: string + country: string + countryCode: string + zipCode: string + phoneNumber: string + isActive: number, + label: string + uuid: string + extraFields: AddressExtraFieldType[] + isDefaultShipping: number, + isDefaultBilling: number, +}