From 26132d206c80e4c7b85f1e45940eca68efc98220 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sat, 29 Aug 2026 00:47:46 +0100 Subject: [PATCH] Add Payment and Fulfillment Policy Components with SVG Icons - Introduced new components for managing Payment Policies and Fulfillment Policies, enhancing the dashboard's functionality for financial management. - Added corresponding SVG icons for Payment Policy, Fulfillment Policy, and Return Policy to improve visual representation. - Updated App.css with new styles for file gallery previews, ensuring a cohesive layout and user experience across the dashboard. - Implemented new object forms and tables for Payment Policies, allowing for better data handling and user interaction. --- assets/icons/fulfillmentpolicyicon.svg | 14 + assets/icons/paymentpolicyicon.svg | 10 + assets/icons/returnpolicyicon.svg | 16 + assets/stylesheets/App.css | 157 ++++++++ .../Dashboard/Finance/PaymentPolicies.jsx | 79 ++++ .../PaymentPolicies/NewPaymentPolicy.jsx | 90 +++++ .../PaymentPolicies/PaymentPolicyInfo.jsx | 264 +++++++++++++ .../Inventory/PartStocks/NewPartStock.jsx | 5 +- .../Inventory/PartStocks/PostPartStock.jsx | 42 ++ .../ProductCategories/NewProductCategory.jsx | 24 +- .../ProductCategories/ProductCategoryInfo.jsx | 81 ++-- .../Management/TaxRates/TaxRateInfo.jsx | 81 ++-- .../Dashboard/Sales/FulfillmentPolicies.jsx | 79 ++++ .../FulfillmentPolicyInfo.jsx | 270 +++++++++++++ .../NewFulfillmentPolicy.jsx | 90 +++++ .../Dashboard/Sales/ListingVarients.jsx | 86 +++++ .../ListingVarients/ListingVarientInfo.jsx | 28 +- .../ListingVarients/NewListingVarient.jsx | 4 +- .../ListingVarients/PublishListingVarient.jsx | 2 +- .../UnpublishListingVarient.jsx | 4 +- .../Dashboard/Sales/Listings/ListingInfo.jsx | 70 +++- .../Dashboard/Sales/Listings/NewListing.jsx | 4 +- .../Sales/Listings/PublishListing.jsx | 8 +- .../Sales/Listings/UnpublishListing.jsx | 8 +- .../Sales/Marketplaces/MarketplaceInfo.jsx | 12 +- .../Marketplaces/SyncAccountPolicies.jsx | 57 +++ .../Marketplaces/SyncFulfillmentPolicies.jsx | 20 + .../Sales/Marketplaces/SyncMarketplace.jsx | 2 +- .../Marketplaces/SyncPaymentPolicies.jsx | 20 + .../Sales/Marketplaces/SyncReturnPolicies.jsx | 20 + .../Sales/Marketplaces/SyncTaxRates.jsx | 20 + .../Dashboard/Sales/ReturnPolicies.jsx | 78 ++++ .../Sales/ReturnPolicies/NewReturnPolicy.jsx | 96 +++++ .../Sales/ReturnPolicies/ReturnPolicyInfo.jsx | 264 +++++++++++++ .../Dashboard/common/CustomTreeSelect.jsx | 74 ++++ .../Dashboard/common/DashboardBreadcrumb.jsx | 12 +- .../Dashboard/common/FileGalleryList.jsx | 362 ++++++++++++++++++ src/components/Dashboard/common/FileList.jsx | 54 ++- .../Dashboard/common/FilePreview.jsx | 17 +- .../Dashboard/common/FileUpload.jsx | 89 +++++ .../Dashboard/common/ObjectForm.jsx | 17 +- .../Dashboard/common/ObjectProperty.jsx | 48 ++- .../Dashboard/common/StateDisplay.jsx | 13 +- src/components/Dashboard/common/StateTag.jsx | 12 + src/components/Dashboard/utils/Utils.js | 73 ++++ .../Icons/FulfillmentPolicyIcon.jsx | 6 + src/components/Icons/PaymentPolicyIcon.jsx | 6 + src/components/Icons/ReturnPolicyIcon.jsx | 6 + src/components/Icons/sidebarIconMap.jsx | 8 + src/database/ObjectModels.js | 34 +- src/database/Sidebars.js | 5 +- src/database/models/File.js | 18 +- src/database/models/FulfillmentPolicy.js | 231 +++++++++++ src/database/models/Listing.js | 101 ++++- src/database/models/ListingVarient.js | 46 ++- src/database/models/Marketplace.js | 194 +++++++--- src/database/models/PartStock.js | 141 +++++-- src/database/models/PaymentPolicy.js | 187 +++++++++ src/database/models/ProductCategory.js | 76 +++- src/database/models/ProductStock.js | 3 +- src/database/models/ReturnPolicy.js | 294 ++++++++++++++ src/database/models/TaxRate.js | 27 +- src/database/models/marketplaceMappings.js | 49 +++ src/database/sidebars/finance.js | 6 + src/database/sidebars/sales.js | 19 + src/routes/FinanceRoutes.jsx | 8 + src/routes/SalesRoutes.jsx | 26 +- 67 files changed, 4116 insertions(+), 251 deletions(-) create mode 100644 assets/icons/fulfillmentpolicyicon.svg create mode 100644 assets/icons/paymentpolicyicon.svg create mode 100644 assets/icons/returnpolicyicon.svg create mode 100644 src/components/Dashboard/Finance/PaymentPolicies.jsx create mode 100644 src/components/Dashboard/Finance/PaymentPolicies/NewPaymentPolicy.jsx create mode 100644 src/components/Dashboard/Finance/PaymentPolicies/PaymentPolicyInfo.jsx create mode 100644 src/components/Dashboard/Inventory/PartStocks/PostPartStock.jsx create mode 100644 src/components/Dashboard/Sales/FulfillmentPolicies.jsx create mode 100644 src/components/Dashboard/Sales/FulfillmentPolicies/FulfillmentPolicyInfo.jsx create mode 100644 src/components/Dashboard/Sales/FulfillmentPolicies/NewFulfillmentPolicy.jsx create mode 100644 src/components/Dashboard/Sales/ListingVarients.jsx create mode 100644 src/components/Dashboard/Sales/Marketplaces/SyncAccountPolicies.jsx create mode 100644 src/components/Dashboard/Sales/Marketplaces/SyncFulfillmentPolicies.jsx create mode 100644 src/components/Dashboard/Sales/Marketplaces/SyncPaymentPolicies.jsx create mode 100644 src/components/Dashboard/Sales/Marketplaces/SyncReturnPolicies.jsx create mode 100644 src/components/Dashboard/Sales/Marketplaces/SyncTaxRates.jsx create mode 100644 src/components/Dashboard/Sales/ReturnPolicies.jsx create mode 100644 src/components/Dashboard/Sales/ReturnPolicies/NewReturnPolicy.jsx create mode 100644 src/components/Dashboard/Sales/ReturnPolicies/ReturnPolicyInfo.jsx create mode 100644 src/components/Dashboard/common/CustomTreeSelect.jsx create mode 100644 src/components/Dashboard/common/FileGalleryList.jsx create mode 100644 src/components/Icons/FulfillmentPolicyIcon.jsx create mode 100644 src/components/Icons/PaymentPolicyIcon.jsx create mode 100644 src/components/Icons/ReturnPolicyIcon.jsx create mode 100644 src/database/models/FulfillmentPolicy.js create mode 100644 src/database/models/PaymentPolicy.js create mode 100644 src/database/models/ReturnPolicy.js create mode 100644 src/database/models/marketplaceMappings.js diff --git a/assets/icons/fulfillmentpolicyicon.svg b/assets/icons/fulfillmentpolicyicon.svg new file mode 100644 index 00000000..70f5993c --- /dev/null +++ b/assets/icons/fulfillmentpolicyicon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/icons/paymentpolicyicon.svg b/assets/icons/paymentpolicyicon.svg new file mode 100644 index 00000000..f1837f1e --- /dev/null +++ b/assets/icons/paymentpolicyicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/returnpolicyicon.svg b/assets/icons/returnpolicyicon.svg new file mode 100644 index 00000000..8fad746a --- /dev/null +++ b/assets/icons/returnpolicyicon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/stylesheets/App.css b/assets/stylesheets/App.css index d00ff1b0..c7e8be02 100644 --- a/assets/stylesheets/App.css +++ b/assets/stylesheets/App.css @@ -2143,3 +2143,160 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton { border-radius: 1px; background: color-mix(in srgb, var(--color-text) 50%, transparent); } + +.file-gallery-list-preview { + position: relative; + width: 100%; + max-height: 600px; + overflow: hidden; +} + +.file-gallery-list-scroll { + width: 100%; +} + +.file-gallery-list-preview .simplebar-track.simplebar-vertical { + display: none; +} + +.file-gallery-list-preview .simplebar-track.simplebar-horizontal { + display: none; +} + +.file-gallery-list-preview .simplebar-content-wrapper { + scroll-snap-type: x mandatory; + scroll-padding-inline: 0; +} + +.file-gallery-list-preview:not(.is-mobile) .simplebar-content-wrapper { + mask-image: linear-gradient( + to right, + transparent 0, + #000 var(--file-gallery-side-padding, 66px), + #000 calc(100% - var(--file-gallery-side-padding, 66px)), + transparent 100% + ); + -webkit-mask-image: linear-gradient( + to right, + transparent 0, + #000 var(--file-gallery-side-padding, 66px), + #000 calc(100% - var(--file-gallery-side-padding, 66px)), + transparent 100% + ); +} + +.file-gallery-list-track { + display: flex; + align-items: center; +} + +.file-gallery-list-slide { + box-sizing: border-box; + scroll-snap-align: center; + height: 100%; + min-width: 0; + display: flex; + justify-content: center; + align-items: center; +} + +.file-gallery-list-preview-frame { + flex: 0 0 auto; + max-width: 100%; + max-height: 600px; + overflow: hidden; + border-radius: 8px; +} + +.file-gallery-list-nav { + position: absolute; + top: 50%; + z-index: 2; + transform: translateY(-50%); +} + +.file-gallery-list-nav-left { + left: 16px; +} + +.file-gallery-list-nav-right { + right: 16px; +} + +.file-gallery-footer { + padding: 0 24px; +} + +.file-gallery-list-thumb-container { + height: 64px; + width: 64px; +} +.file-gallery-list-thumb { + position: relative; + flex: 0 0 auto; + cursor: pointer; + border-radius: 5px; + line-height: 0; +} + +.file-gallery-list-thumb.is-active { + outline: 2px solid var(--color-primary); + outline-offset: 0; +} + +.file-gallery-list-thumb-delete.ant-card { + position: absolute; + top: 0; + right: 0; + transform: translate(30%, -60%); + z-index: 4; + width: max-content; +} + +.file-gallery-list-thumb-delete.ant-card .ant-card-body { + line-height: 1; +} + +.file-gallery-list-thumb-delete .ant-btn { + height: auto; + width: auto; + padding: 0 2px; +} + +.file-gallery-list-upload { + flex: 0 0 auto; +} + +.file-gallery-list-dropzone.ant-upload.ant-upload-drag { + width: 64px; + height: 64px; + padding: 0; + border-radius: 5px; + border: 1px dashed color-mix(in srgb, var(--color-primary) 50%, transparent); + background: repeating-linear-gradient( + -45deg, + color-mix(in srgb, var(--color-primary) 12%, transparent), + color-mix(in srgb, var(--color-primary) 12%, transparent) 6px, + transparent 6px, + transparent 12px + ); +} + +.file-gallery-list-dropzone.ant-upload.ant-upload-drag + .ant-upload-drag-container { + padding: 0; + display: flex; + align-items: center; + justify-content: center; + height: 64px; +} + +.file-gallery-list-dropzone.ant-upload.ant-upload-drag:hover, +.file-gallery-list-dropzone.ant-upload.ant-upload-drag.ant-upload-drag-hover { + border-color: var(--color-primary); +} + +.file-gallery-list-dropzone .anticon { + font-size: 18px; + color: var(--color-primary); +} diff --git a/src/components/Dashboard/Finance/PaymentPolicies.jsx b/src/components/Dashboard/Finance/PaymentPolicies.jsx new file mode 100644 index 00000000..8276751a --- /dev/null +++ b/src/components/Dashboard/Finance/PaymentPolicies.jsx @@ -0,0 +1,79 @@ +import { useRef } from 'react' +import { Flex, Space } from 'antd' +import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import useColumnVisibility from '../hooks/useColumnVisibility' +import ObjectTableViewButton from '../common/ObjectTableViewButton' +import FilterSidebarButton from '../common/FilterSidebarButton' +import SortSidebarButton from '../common/SortSidebarButton' +import useViewMode from '../hooks/useViewMode' +import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' +import ColumnViewButton from '../common/ColumnViewButton' +import ExportListButton from '../common/ExportListButton' + +const PaymentPolicies = () => { + const tableRef = useRef() + const [viewMode, setViewMode] = useViewMode('paymentPolicy') + const [columnVisibility, setColumnVisibility] = + useColumnVisibility('paymentPolicy') + const [showFilterSidebar, setShowFilterSidebar] = + useFilterSidebarVisibility('PaymentPolicies') + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('PaymentPolicies') + + return ( + + + + tableRef.current?.reload()} + /> + + + + + setShowSortSidebar(!showSortSidebar)} + /> + setShowFilterSidebar(!showFilterSidebar)} + /> + + + + + + ) +} + +export default PaymentPolicies diff --git a/src/components/Dashboard/Finance/PaymentPolicies/NewPaymentPolicy.jsx b/src/components/Dashboard/Finance/PaymentPolicies/NewPaymentPolicy.jsx new file mode 100644 index 00000000..86627d92 --- /dev/null +++ b/src/components/Dashboard/Finance/PaymentPolicies/NewPaymentPolicy.jsx @@ -0,0 +1,90 @@ +import PropTypes from 'prop-types' +import ObjectInfo from '../../common/ObjectInfo' +import NewObjectForm from '../../common/NewObjectForm' +import WizardView from '../../common/WizardView' + +const NewPaymentPolicy = ({ onOk, defaultValues }) => { + return ( + + {({ handleSubmit, submitLoading, objectData, formValid }) => { + const steps = [ + { + title: 'Required', + key: 'required', + content: ( + + ) + }, + { + title: 'Optional', + key: 'optional', + content: ( + + ) + }, + { + title: 'Summary', + key: 'summary', + content: ( + + ) + } + ] + return ( + { + const result = await handleSubmit() + if (result) { + onOk() + } + }} + /> + ) + }} + + ) +} + +NewPaymentPolicy.propTypes = { + onOk: PropTypes.func.isRequired, + reset: PropTypes.bool, + defaultValues: PropTypes.object +} + +export default NewPaymentPolicy diff --git a/src/components/Dashboard/Finance/PaymentPolicies/PaymentPolicyInfo.jsx b/src/components/Dashboard/Finance/PaymentPolicies/PaymentPolicyInfo.jsx new file mode 100644 index 00000000..52dc484e --- /dev/null +++ b/src/components/Dashboard/Finance/PaymentPolicies/PaymentPolicyInfo.jsx @@ -0,0 +1,264 @@ +import { useRef, useState, useMemo } from 'react' +import { useLocation } from 'react-router-dom' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' +import loglevel from 'loglevel' +import config from '../../../../config' +import useCollapseState from '../../hooks/useCollapseState' +import NotesPanel from '../../common/NotesPanel' +import InfoCollapse from '../../common/InfoCollapse' +import ObjectInfo from '../../common/ObjectInfo' +import ObjectProperty from '../../common/ObjectProperty.jsx' +import ViewButton from '../../common/ViewButton' +import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' +import NoteIcon from '../../../Icons/NoteIcon.jsx' +import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' +import MarketplaceIcon from '../../../Icons/MarketplaceIcon.jsx' +import ObjectForm from '../../common/ObjectForm' +import EditButtons from '../../common/EditButtons' +import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' +import ActivityIndicator from '../../common/ActivityIndicator.jsx' +import ActionHandler from '../../common/ActionHandler.jsx' +import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' +import ObjectTable from '../../common/ObjectTable.jsx' +import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' +import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { + getModelProperty, + getModelByName +} from '../../../../database/ObjectModels.js' +import ScrollBox from '../../common/ScrollBox.jsx' + +const log = loglevel.getLogger('PaymentPolicyInfo') +log.setLevel(config.logLevel) + +const PaymentPolicyInfo = () => { + const location = useLocation() + const objectFormRef = useRef(null) + const actionHandlerRef = useRef(null) + const paymentPolicyId = new URLSearchParams(location.search).get( + 'paymentPolicyId' + ) + const [collapseState, updateCollapseState] = useCollapseState( + 'PaymentPolicyInfo', + { + info: true, + marketplaces: true, + notes: true, + auditLogs: false + } + ) + const [objectFormState, setEditFormState] = useState({ + isEditing: false, + editLoading: false, + formValid: false, + lock: null, + loading: false, + editDisabled: false, + objectData: {} + }) + + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, paymentPolicyId] + ) + useDashboardObjectTools(currentObjectTools) + + const actions = { + edit: () => { + objectFormRef?.current?.startEditing?.() + return false + }, + cancelEdit: () => { + objectFormRef?.current?.cancelEditing?.() + return true + }, + finishEdit: () => { + objectFormRef?.current?.handleUpdate?.() + return true + } + } + + return ( + <> + + + + + + + + + + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + + + + + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} + > + {({ loading, isEditing, objectData }) => ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > + + + } + active={collapseState.marketplaces} + onToggle={(expanded) => + updateCollapseState('marketplaces', expanded) + } + collapseKey='marketplaces' + > + + + + )} + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } + > + + + + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) + } + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + + + ) +} + +export default PaymentPolicyInfo diff --git a/src/components/Dashboard/Inventory/PartStocks/NewPartStock.jsx b/src/components/Dashboard/Inventory/PartStocks/NewPartStock.jsx index 4a94467b..a40dc583 100644 --- a/src/components/Dashboard/Inventory/PartStocks/NewPartStock.jsx +++ b/src/components/Dashboard/Inventory/PartStocks/NewPartStock.jsx @@ -8,7 +8,7 @@ const NewPartStock = ({ onOk, reset, defaultValues }) => { {({ handleSubmit, submitLoading, objectData, formValid }) => { const steps = [ @@ -38,7 +38,8 @@ const NewPartStock = ({ onOk, reset, defaultValues }) => { _id: false, _reference: false, createdAt: false, - updatedAt: false + updatedAt: false, + postedAt: false }} isEditing={false} objectData={objectData} diff --git a/src/components/Dashboard/Inventory/PartStocks/PostPartStock.jsx b/src/components/Dashboard/Inventory/PartStocks/PostPartStock.jsx new file mode 100644 index 00000000..92bf9fde --- /dev/null +++ b/src/components/Dashboard/Inventory/PartStocks/PostPartStock.jsx @@ -0,0 +1,42 @@ +import { useState, useContext } from 'react' +import PropTypes from 'prop-types' +import { ApiServerContext } from '../../context/ApiServerContext' +import { message } from 'antd' +import MessageDialogView from '../../common/MessageDialogView.jsx' + +const PostPartStock = ({ onOk, objectData }) => { + const [postLoading, setPostLoading] = useState(false) + const { sendObjectFunction } = useContext(ApiServerContext) + + const handlePost = async () => { + setPostLoading(true) + try { + const result = await sendObjectFunction(objectData._id, 'PartStock', 'post') + if (result) { + message.success('Part stock posted successfully') + onOk(result) + } + } catch (error) { + console.error('Error posting part stock:', error) + } finally { + setPostLoading(false) + } + } + + return ( + + ) +} + +PostPartStock.propTypes = { + onOk: PropTypes.func.isRequired, + objectData: PropTypes.object +} + +export default PostPartStock diff --git a/src/components/Dashboard/Management/ProductCategories/NewProductCategory.jsx b/src/components/Dashboard/Management/ProductCategories/NewProductCategory.jsx index 7edaf30f..1904db1c 100644 --- a/src/components/Dashboard/Management/ProductCategories/NewProductCategory.jsx +++ b/src/components/Dashboard/Management/ProductCategories/NewProductCategory.jsx @@ -3,9 +3,9 @@ import ObjectInfo from '../../common/ObjectInfo' import NewObjectForm from '../../common/NewObjectForm' import WizardView from '../../common/WizardView' -const NewProductCategory = ({ onOk }) => { +const NewProductCategory = ({ onOk, defaultValues }) => { return ( - + {({ handleSubmit, submitLoading, objectData, formValid }) => { const steps = [ { @@ -23,6 +23,21 @@ const NewProductCategory = ({ onOk }) => { /> ) }, + { + title: 'Optional', + key: 'optional', + content: ( + + ) + }, { title: 'Summary', key: 'summary', @@ -39,7 +54,7 @@ const NewProductCategory = ({ onOk }) => { }} isEditing={false} objectData={objectData} - labelWidth={70} + labelWidth={120} /> ) } @@ -65,7 +80,8 @@ const NewProductCategory = ({ onOk }) => { NewProductCategory.propTypes = { onOk: PropTypes.func.isRequired, - reset: PropTypes.bool + reset: PropTypes.bool, + defaultValues: PropTypes.object } export default NewProductCategory diff --git a/src/components/Dashboard/Management/ProductCategories/ProductCategoryInfo.jsx b/src/components/Dashboard/Management/ProductCategories/ProductCategoryInfo.jsx index af2a9b2a..a5625f56 100644 --- a/src/components/Dashboard/Management/ProductCategories/ProductCategoryInfo.jsx +++ b/src/components/Dashboard/Management/ProductCategories/ProductCategoryInfo.jsx @@ -6,10 +6,12 @@ import useCollapseState from '../../hooks/useCollapseState' import NotesPanel from '../../common/NotesPanel' import InfoCollapse from '../../common/InfoCollapse' import ObjectInfo from '../../common/ObjectInfo' +import ObjectProperty from '../../common/ObjectProperty.jsx' import ViewButton from '../../common/ViewButton' import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' import NoteIcon from '../../../Icons/NoteIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' +import MarketplaceIcon from '../../../Icons/MarketplaceIcon.jsx' import ObjectForm from '../../common/ObjectForm' import EditButtons from '../../common/EditButtons' import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' @@ -21,7 +23,7 @@ import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' -import { getModelByName } from '../../../../database/ObjectModels.js' +import { getModelProperty, getModelByName } from '../../../../database/ObjectModels.js' import ScrollBox from '../../common/ScrollBox.jsx' const ProductCategoryInfo = () => { @@ -35,6 +37,7 @@ const ProductCategoryInfo = () => { 'ProductCategoryInfo', { info: true, + marketplaces: true, notes: true, auditLogs: false } @@ -98,6 +101,7 @@ const ProductCategoryInfo = () => { disabled={objectFormState.loading} items={[ { key: 'info', label: 'Product Category Information' }, + { key: 'marketplaces', label: 'Marketplaces' }, { key: 'notes', label: 'Notes' }, { key: 'auditLogs', label: 'Audit Logs' } ]} @@ -150,33 +154,56 @@ const ProductCategoryInfo = () => { loading={objectFormState.loading} ref={actionHandlerRef} > - } - active={collapseState.info} - onToggle={(expanded) => updateCollapseState('info', expanded)} - collapseKey='info' + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} > - { - setEditFormState((prev) => ({ ...prev, ...state })) - }} - > - {({ loading, isEditing, objectData }) => ( - - )} - - + {({ loading, isEditing, objectData }) => ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > + + + } + active={collapseState.marketplaces} + onToggle={(expanded) => + updateCollapseState('marketplaces', expanded) + } + collapseKey='marketplaces' + > + + + + )} + { const taxRateId = new URLSearchParams(location.search).get('taxRateId') const [collapseState, updateCollapseState] = useCollapseState('TaxRateInfo', { info: true, + marketplaces: true, notes: true, auditLogs: false }) @@ -98,6 +101,7 @@ const TaxRateInfo = () => { disabled={objectFormState.loading} items={[ { key: 'info', label: 'Tax Rate Information' }, + { key: 'marketplaces', label: 'Marketplaces' }, { key: 'notes', label: 'Notes' }, { key: 'auditLogs', label: 'Audit Logs' } ]} @@ -150,33 +154,56 @@ const TaxRateInfo = () => { loading={objectFormState.loading} ref={actionHandlerRef} > - } - active={collapseState.info} - onToggle={(expanded) => updateCollapseState('info', expanded)} - collapseKey='info' + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} > - { - setEditFormState((prev) => ({ ...prev, ...state })) - }} - > - {({ loading, isEditing, objectData }) => ( - - )} - - + {({ loading, isEditing, objectData }) => ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > + + + } + active={collapseState.marketplaces} + onToggle={(expanded) => + updateCollapseState('marketplaces', expanded) + } + collapseKey='marketplaces' + > + + + + )} + { + const tableRef = useRef() + const [viewMode, setViewMode] = useViewMode('fulfillmentPolicy') + const [columnVisibility, setColumnVisibility] = + useColumnVisibility('fulfillmentPolicy') + const [showFilterSidebar, setShowFilterSidebar] = + useFilterSidebarVisibility('FulfillmentPolicies') + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('FulfillmentPolicies') + + return ( + + + + tableRef.current?.reload()} + /> + + + + + setShowSortSidebar(!showSortSidebar)} + /> + setShowFilterSidebar(!showFilterSidebar)} + /> + + + + + + ) +} + +export default FulfillmentPolicies diff --git a/src/components/Dashboard/Sales/FulfillmentPolicies/FulfillmentPolicyInfo.jsx b/src/components/Dashboard/Sales/FulfillmentPolicies/FulfillmentPolicyInfo.jsx new file mode 100644 index 00000000..36469c5f --- /dev/null +++ b/src/components/Dashboard/Sales/FulfillmentPolicies/FulfillmentPolicyInfo.jsx @@ -0,0 +1,270 @@ +import { useRef, useState, useMemo } from 'react' +import { useLocation } from 'react-router-dom' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' +import loglevel from 'loglevel' +import config from '../../../../config' +import useCollapseState from '../../hooks/useCollapseState' +import NotesPanel from '../../common/NotesPanel' +import InfoCollapse from '../../common/InfoCollapse' +import ObjectInfo from '../../common/ObjectInfo' +import ObjectProperty from '../../common/ObjectProperty.jsx' +import ViewButton from '../../common/ViewButton' +import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' +import NoteIcon from '../../../Icons/NoteIcon.jsx' +import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' +import MarketplaceIcon from '../../../Icons/MarketplaceIcon.jsx' +import ObjectForm from '../../common/ObjectForm' +import EditButtons from '../../common/EditButtons' +import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' +import ActivityIndicator from '../../common/ActivityIndicator.jsx' +import ActionHandler from '../../common/ActionHandler.jsx' +import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' +import ObjectTable from '../../common/ObjectTable.jsx' +import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' +import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { + getModelProperty, + getModelByName +} from '../../../../database/ObjectModels.js' +import ScrollBox from '../../common/ScrollBox.jsx' + +const log = loglevel.getLogger('FulfillmentPolicyInfo') +log.setLevel(config.logLevel) + +const FulfillmentPolicyInfo = () => { + const location = useLocation() + const objectFormRef = useRef(null) + const actionHandlerRef = useRef(null) + const fulfillmentPolicyId = new URLSearchParams(location.search).get( + 'fulfillmentPolicyId' + ) + const [collapseState, updateCollapseState] = useCollapseState( + 'FulfillmentPolicyInfo', + { + info: true, + marketplaces: true, + notes: true, + auditLogs: false + } + ) + const [objectFormState, setEditFormState] = useState({ + isEditing: false, + editLoading: false, + formValid: false, + lock: null, + loading: false, + editDisabled: false, + objectData: {} + }) + + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, fulfillmentPolicyId] + ) + useDashboardObjectTools(currentObjectTools) + + const actions = { + edit: () => { + objectFormRef?.current?.startEditing?.() + return false + }, + cancelEdit: () => { + objectFormRef?.current?.cancelEditing?.() + return true + }, + finishEdit: () => { + objectFormRef?.current?.handleUpdate?.() + return true + } + } + + return ( + <> + + + + + + + + + + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + + + + + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} + > + {({ loading, isEditing, objectData }) => ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > + + + } + active={collapseState.marketplaces} + onToggle={(expanded) => + updateCollapseState('marketplaces', expanded) + } + collapseKey='marketplaces' + > + + + + )} + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } + > + + + + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) + } + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + + + ) +} + +export default FulfillmentPolicyInfo diff --git a/src/components/Dashboard/Sales/FulfillmentPolicies/NewFulfillmentPolicy.jsx b/src/components/Dashboard/Sales/FulfillmentPolicies/NewFulfillmentPolicy.jsx new file mode 100644 index 00000000..d8070927 --- /dev/null +++ b/src/components/Dashboard/Sales/FulfillmentPolicies/NewFulfillmentPolicy.jsx @@ -0,0 +1,90 @@ +import PropTypes from 'prop-types' +import ObjectInfo from '../../common/ObjectInfo' +import NewObjectForm from '../../common/NewObjectForm' +import WizardView from '../../common/WizardView' + +const NewFulfillmentPolicy = ({ onOk, defaultValues }) => { + return ( + + {({ handleSubmit, submitLoading, objectData, formValid }) => { + const steps = [ + { + title: 'Required', + key: 'required', + content: ( + + ) + }, + { + title: 'Optional', + key: 'optional', + content: ( + + ) + }, + { + title: 'Summary', + key: 'summary', + content: ( + + ) + } + ] + return ( + { + const result = await handleSubmit() + if (result) { + onOk() + } + }} + /> + ) + }} + + ) +} + +NewFulfillmentPolicy.propTypes = { + onOk: PropTypes.func.isRequired, + reset: PropTypes.bool, + defaultValues: PropTypes.object +} + +export default NewFulfillmentPolicy diff --git a/src/components/Dashboard/Sales/ListingVarients.jsx b/src/components/Dashboard/Sales/ListingVarients.jsx new file mode 100644 index 00000000..e1e8a82e --- /dev/null +++ b/src/components/Dashboard/Sales/ListingVarients.jsx @@ -0,0 +1,86 @@ +import { useRef } from 'react' +import { Flex, Space } from 'antd' +import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import useColumnVisibility from '../hooks/useColumnVisibility' +import ObjectTableViewButton from '../common/ObjectTableViewButton' +import FilterSidebarButton from '../common/FilterSidebarButton' +import SortSidebarButton from '../common/SortSidebarButton' +import useViewMode from '../hooks/useViewMode' +import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' +import ColumnViewButton from '../common/ColumnViewButton' +import ExportListButton from '../common/ExportListButton' + +const ListingVarients = () => { + const tableRef = useRef() + + const [viewMode, setViewMode] = useViewMode('listingVarients') + + const [columnVisibility, setColumnVisibility] = + useColumnVisibility('listingVarients') + + const [showFilterSidebar, setShowFilterSidebar] = + useFilterSidebarVisibility('ListingVarients') + + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('ListingVarients') + + + return ( + <> + + + + tableRef.current?.reload()} + /> + + + + + setShowSortSidebar(!showSortSidebar)} + /> + setShowFilterSidebar(!showFilterSidebar)} + /> + + + + + + + ) +} + +export default ListingVarients diff --git a/src/components/Dashboard/Sales/ListingVarients/ListingVarientInfo.jsx b/src/components/Dashboard/Sales/ListingVarients/ListingVarientInfo.jsx index 236edd8c..f5517e46 100644 --- a/src/components/Dashboard/Sales/ListingVarients/ListingVarientInfo.jsx +++ b/src/components/Dashboard/Sales/ListingVarients/ListingVarientInfo.jsx @@ -8,8 +8,10 @@ import useCollapseState from '../../hooks/useCollapseState' import NotesPanel from '../../common/NotesPanel' import InfoCollapse from '../../common/InfoCollapse' import ObjectInfo from '../../common/ObjectInfo' +import ObjectProperty from '../../common/ObjectProperty.jsx' import ViewButton from '../../common/ViewButton' import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' +import PictureIcon from '../../../Icons/PictureIcon.jsx' import NoteIcon from '../../../Icons/NoteIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' import ObjectForm from '../../common/ObjectForm' @@ -25,7 +27,10 @@ import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' import ScrollBox from '../../common/ScrollBox.jsx' -import { getModelByName } from '../../../../database/ObjectModels.js' +import { + getModelByName, + getModelProperty +} from '../../../../database/ObjectModels.js' const log = loglevel.getLogger('ListingVarientInfo') log.setLevel(config.logLevel) @@ -49,6 +54,7 @@ const ListingVarientInfo = () => { 'ListingVarientInfo', { info: true, + listingImages: true, notes: true, auditLogs: false } @@ -111,6 +117,7 @@ const ListingVarientInfo = () => { disabled={objectFormState.loading} items={[ { key: 'info', label: 'Listing Varient Information' }, + { key: 'listingImages', label: 'Listing Images' }, { key: 'notes', label: 'Notes' }, { key: 'auditLogs', label: 'Audit Logs' } ]} @@ -189,6 +196,25 @@ const ListingVarientInfo = () => { isEditing={isEditing} type='listingVarient' objectData={objectData} + visibleProperties={{ listingImages: false }} + /> + + } + active={collapseState.listingImages} + onToggle={(expanded) => + updateCollapseState('listingImages', expanded) + } + collapseKey='listingImages' + > + diff --git a/src/components/Dashboard/Sales/ListingVarients/NewListingVarient.jsx b/src/components/Dashboard/Sales/ListingVarients/NewListingVarient.jsx index ac600e58..4d7eb768 100644 --- a/src/components/Dashboard/Sales/ListingVarients/NewListingVarient.jsx +++ b/src/components/Dashboard/Sales/ListingVarients/NewListingVarient.jsx @@ -37,6 +37,7 @@ const NewListingVarient = ({ onOk, defaultValues }) => { isEditing={true} required={false} objectData={objectData} + visibleProperties={{ listingImages: false }} /> ) }, @@ -54,7 +55,8 @@ const NewListingVarient = ({ onOk, defaultValues }) => { createdAt: false, updatedAt: false, lastSyncedAt: false, - stockQuantity: false + stockQuantity: false, + listingImages: false }} isEditing={false} objectData={objectData} diff --git a/src/components/Dashboard/Sales/ListingVarients/PublishListingVarient.jsx b/src/components/Dashboard/Sales/ListingVarients/PublishListingVarient.jsx index e6c09100..131084d3 100644 --- a/src/components/Dashboard/Sales/ListingVarients/PublishListingVarient.jsx +++ b/src/components/Dashboard/Sales/ListingVarients/PublishListingVarient.jsx @@ -17,7 +17,7 @@ const PublishListingVarient = ({ onOk, objectData }) => { 'publish' ) if (result) { - message.success('Published successfully') + message.success('Publishing started...') onOk(result) } } catch (error) { diff --git a/src/components/Dashboard/Sales/ListingVarients/UnpublishListingVarient.jsx b/src/components/Dashboard/Sales/ListingVarients/UnpublishListingVarient.jsx index ae390618..f46f63f5 100644 --- a/src/components/Dashboard/Sales/ListingVarients/UnpublishListingVarient.jsx +++ b/src/components/Dashboard/Sales/ListingVarients/UnpublishListingVarient.jsx @@ -17,7 +17,7 @@ const UnpublishListingVarient = ({ onOk, objectData }) => { 'unpublish' ) if (result) { - message.success('Unpublished successfully') + message.success('Unpublishing started...') onOk(result) } } catch (error) { @@ -35,7 +35,7 @@ const UnpublishListingVarient = ({ onOk, objectData }) => { return ( { const listingId = new URLSearchParams(location.search).get('listingId') const [collapseState, updateCollapseState] = useCollapseState('ListingInfo', { info: true, + listingImages: true, description: true, listingVarients: true, notes: true, @@ -89,7 +94,7 @@ const ListingInfo = () => { finishEdit: () => { objectFormRef?.current?.handleUpdate?.() return true - }, + } } return ( @@ -112,6 +117,7 @@ const ListingInfo = () => { disabled={objectFormState.loading} items={[ { key: 'info', label: 'Listing Information' }, + { key: 'listingImages', label: 'Listing Images' }, { key: 'description', label: 'Listing Description' }, { key: 'listingVarients', label: 'Listing Varients' }, { key: 'notes', label: 'Notes' }, @@ -191,11 +197,35 @@ const ListingInfo = () => { loading={loading} isEditing={isEditing} type='listing' - labelWidth={165} + labelWidth={180} objectData={objectData} - visibleProperties={{ description: false }} + visibleProperties={{ + description: false, + listingImages: false + }} /> + } + active={collapseState.listingImages} + onToggle={(expanded) => + updateCollapseState('listingImages', expanded) + } + collapseKey='listingImages' + > + }> + + + + } @@ -205,16 +235,18 @@ const ListingInfo = () => { } collapseKey='description' > - - - + }> + + + + { { indicator={} > - + @@ -267,10 +300,7 @@ const ListingInfo = () => { diff --git a/src/components/Dashboard/Sales/Listings/NewListing.jsx b/src/components/Dashboard/Sales/Listings/NewListing.jsx index 1e6d36c9..ea36025e 100644 --- a/src/components/Dashboard/Sales/Listings/NewListing.jsx +++ b/src/components/Dashboard/Sales/Listings/NewListing.jsx @@ -37,6 +37,7 @@ const NewListing = ({ onOk, defaultValues }) => { isEditing={true} required={false} objectData={objectData} + visibleProperties={{ listingImages: false }} /> ) }, @@ -54,7 +55,8 @@ const NewListing = ({ onOk, defaultValues }) => { createdAt: false, updatedAt: false, lastSyncedAt: false, - stockQuantity: false + stockQuantity: false, + listingImages: false }} isEditing={false} objectData={objectData} diff --git a/src/components/Dashboard/Sales/Listings/PublishListing.jsx b/src/components/Dashboard/Sales/Listings/PublishListing.jsx index 6e35655b..034ba819 100644 --- a/src/components/Dashboard/Sales/Listings/PublishListing.jsx +++ b/src/components/Dashboard/Sales/Listings/PublishListing.jsx @@ -11,9 +11,13 @@ const PublishListing = ({ onOk, objectData }) => { const handlePublish = async () => { setLoading(true) try { - const result = await sendObjectFunction(objectData._id, 'Listing', 'publish') + const result = await sendObjectFunction( + objectData._id, + 'Listing', + 'publish' + ) if (result) { - message.success('Published successfully') + message.success('Publishing started...') onOk(result) } } catch (error) { diff --git a/src/components/Dashboard/Sales/Listings/UnpublishListing.jsx b/src/components/Dashboard/Sales/Listings/UnpublishListing.jsx index 732be523..15979105 100644 --- a/src/components/Dashboard/Sales/Listings/UnpublishListing.jsx +++ b/src/components/Dashboard/Sales/Listings/UnpublishListing.jsx @@ -11,9 +11,13 @@ const UnpublishListing = ({ onOk, objectData }) => { const handleUnpublish = async () => { setLoading(true) try { - const result = await sendObjectFunction(objectData._id, 'Listing', 'unpublish') + const result = await sendObjectFunction( + objectData._id, + 'Listing', + 'unpublish' + ) if (result) { - message.success('Unpublished successfully') + message.success('Unpublishing started...') onOk(result) } } catch (error) { diff --git a/src/components/Dashboard/Sales/Marketplaces/MarketplaceInfo.jsx b/src/components/Dashboard/Sales/Marketplaces/MarketplaceInfo.jsx index 7c6fb37a..314ed533 100644 --- a/src/components/Dashboard/Sales/Marketplaces/MarketplaceInfo.jsx +++ b/src/components/Dashboard/Sales/Marketplaces/MarketplaceInfo.jsx @@ -253,6 +253,10 @@ const MarketplaceInfo = () => { isEditing={isEditing} type='marketplace' labelWidth={215} + visibleProperties={{ + 'eBay.categoryReferences': false, + 'eBay.availableShippingServices': false + }} objectData={objectData} /> )} @@ -271,7 +275,7 @@ const MarketplaceInfo = () => { indicator={} > - + @@ -288,10 +292,8 @@ const MarketplaceInfo = () => { diff --git a/src/components/Dashboard/Sales/Marketplaces/SyncAccountPolicies.jsx b/src/components/Dashboard/Sales/Marketplaces/SyncAccountPolicies.jsx new file mode 100644 index 00000000..c76fb367 --- /dev/null +++ b/src/components/Dashboard/Sales/Marketplaces/SyncAccountPolicies.jsx @@ -0,0 +1,57 @@ +import { useState, useContext } from 'react' +import PropTypes from 'prop-types' +import { ApiServerContext } from '../../context/ApiServerContext' +import { message } from 'antd' +import MessageDialogView from '../../common/MessageDialogView.jsx' + +const SyncAccountPolicies = ({ + onOk, + objectData, + functionName, + title, + description, + successMessage +}) => { + const [syncLoading, setSyncLoading] = useState(false) + const { sendObjectFunction } = useContext(ApiServerContext) + + const handleSync = async () => { + setSyncLoading(true) + try { + const result = await sendObjectFunction( + objectData._id, + 'Marketplace', + functionName + ) + if (result) { + message.success(successMessage) + onOk(result) + } + } catch (error) { + console.error(`Error syncing ${functionName}:`, error) + } finally { + setSyncLoading(false) + } + } + + return ( + + ) +} + +SyncAccountPolicies.propTypes = { + onOk: PropTypes.func.isRequired, + objectData: PropTypes.object, + functionName: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + successMessage: PropTypes.string.isRequired +} + +export default SyncAccountPolicies diff --git a/src/components/Dashboard/Sales/Marketplaces/SyncFulfillmentPolicies.jsx b/src/components/Dashboard/Sales/Marketplaces/SyncFulfillmentPolicies.jsx new file mode 100644 index 00000000..acfdb5c7 --- /dev/null +++ b/src/components/Dashboard/Sales/Marketplaces/SyncFulfillmentPolicies.jsx @@ -0,0 +1,20 @@ +import PropTypes from 'prop-types' +import SyncAccountPolicies from './SyncAccountPolicies.jsx' + +const SyncFulfillmentPolicies = ({ onOk, objectData }) => ( + +) + +SyncFulfillmentPolicies.propTypes = { + onOk: PropTypes.func.isRequired, + objectData: PropTypes.object +} + +export default SyncFulfillmentPolicies diff --git a/src/components/Dashboard/Sales/Marketplaces/SyncMarketplace.jsx b/src/components/Dashboard/Sales/Marketplaces/SyncMarketplace.jsx index cdba2b4d..378d5672 100644 --- a/src/components/Dashboard/Sales/Marketplaces/SyncMarketplace.jsx +++ b/src/components/Dashboard/Sales/Marketplaces/SyncMarketplace.jsx @@ -30,7 +30,7 @@ const SyncMarketplace = ({ onOk, objectData }) => { return ( ( + +) + +SyncPaymentPolicies.propTypes = { + onOk: PropTypes.func.isRequired, + objectData: PropTypes.object +} + +export default SyncPaymentPolicies diff --git a/src/components/Dashboard/Sales/Marketplaces/SyncReturnPolicies.jsx b/src/components/Dashboard/Sales/Marketplaces/SyncReturnPolicies.jsx new file mode 100644 index 00000000..a5768db8 --- /dev/null +++ b/src/components/Dashboard/Sales/Marketplaces/SyncReturnPolicies.jsx @@ -0,0 +1,20 @@ +import PropTypes from 'prop-types' +import SyncAccountPolicies from './SyncAccountPolicies.jsx' + +const SyncReturnPolicies = ({ onOk, objectData }) => ( + +) + +SyncReturnPolicies.propTypes = { + onOk: PropTypes.func.isRequired, + objectData: PropTypes.object +} + +export default SyncReturnPolicies diff --git a/src/components/Dashboard/Sales/Marketplaces/SyncTaxRates.jsx b/src/components/Dashboard/Sales/Marketplaces/SyncTaxRates.jsx new file mode 100644 index 00000000..8589dd46 --- /dev/null +++ b/src/components/Dashboard/Sales/Marketplaces/SyncTaxRates.jsx @@ -0,0 +1,20 @@ +import PropTypes from 'prop-types' +import SyncAccountPolicies from './SyncAccountPolicies.jsx' + +const SyncTaxRates = ({ onOk, objectData }) => ( + +) + +SyncTaxRates.propTypes = { + onOk: PropTypes.func.isRequired, + objectData: PropTypes.object +} + +export default SyncTaxRates diff --git a/src/components/Dashboard/Sales/ReturnPolicies.jsx b/src/components/Dashboard/Sales/ReturnPolicies.jsx new file mode 100644 index 00000000..954c4508 --- /dev/null +++ b/src/components/Dashboard/Sales/ReturnPolicies.jsx @@ -0,0 +1,78 @@ +import { useRef } from 'react' +import { Flex, Space } from 'antd' +import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import useColumnVisibility from '../hooks/useColumnVisibility' +import ObjectTableViewButton from '../common/ObjectTableViewButton' +import FilterSidebarButton from '../common/FilterSidebarButton' +import SortSidebarButton from '../common/SortSidebarButton' +import useViewMode from '../hooks/useViewMode' +import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' +import ColumnViewButton from '../common/ColumnViewButton' +import ExportListButton from '../common/ExportListButton' + +const ReturnPolicies = () => { + const tableRef = useRef() + const [viewMode, setViewMode] = useViewMode('returnPolicy') + const [columnVisibility, setColumnVisibility] = useColumnVisibility('returnPolicy') + const [showFilterSidebar, setShowFilterSidebar] = + useFilterSidebarVisibility('ReturnPolicies') + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('ReturnPolicies') + + return ( + + + + tableRef.current?.reload()} + /> + + + + + setShowSortSidebar(!showSortSidebar)} + /> + setShowFilterSidebar(!showFilterSidebar)} + /> + + + + + + ) +} + +export default ReturnPolicies diff --git a/src/components/Dashboard/Sales/ReturnPolicies/NewReturnPolicy.jsx b/src/components/Dashboard/Sales/ReturnPolicies/NewReturnPolicy.jsx new file mode 100644 index 00000000..92c4e1e7 --- /dev/null +++ b/src/components/Dashboard/Sales/ReturnPolicies/NewReturnPolicy.jsx @@ -0,0 +1,96 @@ +import PropTypes from 'prop-types' +import ObjectInfo from '../../common/ObjectInfo' +import NewObjectForm from '../../common/NewObjectForm' +import WizardView from '../../common/WizardView' + +const NewReturnPolicy = ({ onOk, defaultValues }) => { + return ( + + {({ handleSubmit, submitLoading, objectData, formValid }) => { + const steps = [ + { + title: 'Required', + key: 'required', + content: ( + + ) + }, + { + title: 'Optional', + key: 'optional', + content: ( + + ) + }, + { + title: 'Summary', + key: 'summary', + content: ( + + ) + } + ] + return ( + { + const result = await handleSubmit() + if (result) { + onOk() + } + }} + /> + ) + }} + + ) +} + +NewReturnPolicy.propTypes = { + onOk: PropTypes.func.isRequired, + reset: PropTypes.bool, + defaultValues: PropTypes.object +} + +export default NewReturnPolicy diff --git a/src/components/Dashboard/Sales/ReturnPolicies/ReturnPolicyInfo.jsx b/src/components/Dashboard/Sales/ReturnPolicies/ReturnPolicyInfo.jsx new file mode 100644 index 00000000..2368d5c0 --- /dev/null +++ b/src/components/Dashboard/Sales/ReturnPolicies/ReturnPolicyInfo.jsx @@ -0,0 +1,264 @@ +import { useRef, useState, useMemo } from 'react' +import { useLocation } from 'react-router-dom' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' +import loglevel from 'loglevel' +import config from '../../../../config' +import useCollapseState from '../../hooks/useCollapseState' +import NotesPanel from '../../common/NotesPanel' +import InfoCollapse from '../../common/InfoCollapse' +import ObjectInfo from '../../common/ObjectInfo' +import ObjectProperty from '../../common/ObjectProperty.jsx' +import ViewButton from '../../common/ViewButton' +import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' +import NoteIcon from '../../../Icons/NoteIcon.jsx' +import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' +import MarketplaceIcon from '../../../Icons/MarketplaceIcon.jsx' +import ObjectForm from '../../common/ObjectForm' +import EditButtons from '../../common/EditButtons' +import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' +import ActivityIndicator from '../../common/ActivityIndicator.jsx' +import ActionHandler from '../../common/ActionHandler.jsx' +import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' +import ObjectTable from '../../common/ObjectTable.jsx' +import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' +import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { + getModelProperty, + getModelByName +} from '../../../../database/ObjectModels.js' +import ScrollBox from '../../common/ScrollBox.jsx' + +const log = loglevel.getLogger('ReturnPolicyInfo') +log.setLevel(config.logLevel) + +const ReturnPolicyInfo = () => { + const location = useLocation() + const objectFormRef = useRef(null) + const actionHandlerRef = useRef(null) + const returnPolicyId = new URLSearchParams(location.search).get( + 'returnPolicyId' + ) + const [collapseState, updateCollapseState] = useCollapseState( + 'ReturnPolicyInfo', + { + info: true, + marketplaces: true, + notes: true, + auditLogs: false + } + ) + const [objectFormState, setEditFormState] = useState({ + isEditing: false, + editLoading: false, + formValid: false, + lock: null, + loading: false, + editDisabled: false, + objectData: {} + }) + + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, returnPolicyId] + ) + useDashboardObjectTools(currentObjectTools) + + const actions = { + edit: () => { + objectFormRef?.current?.startEditing?.() + return false + }, + cancelEdit: () => { + objectFormRef?.current?.cancelEditing?.() + return true + }, + finishEdit: () => { + objectFormRef?.current?.handleUpdate?.() + return true + } + } + + return ( + <> + + + + + + + + + + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + + + + + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} + > + {({ loading, isEditing, objectData }) => ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > + + + } + active={collapseState.marketplaces} + onToggle={(expanded) => + updateCollapseState('marketplaces', expanded) + } + collapseKey='marketplaces' + > + + + + )} + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } + > + + + + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) + } + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + + + ) +} + +export default ReturnPolicyInfo diff --git a/src/components/Dashboard/common/CustomTreeSelect.jsx b/src/components/Dashboard/common/CustomTreeSelect.jsx new file mode 100644 index 00000000..0cedef85 --- /dev/null +++ b/src/components/Dashboard/common/CustomTreeSelect.jsx @@ -0,0 +1,74 @@ +import { useEffect, useRef } from 'react' +import { TreeSelect } from 'antd' +import PropTypes from 'prop-types' + +export const findTreeNode = (nodes, value) => { + if (!Array.isArray(nodes) || value == null || value === '') return null + const match = String(value) + for (const node of nodes) { + if (node?.value != null && String(node.value) === match) { + return node + } + const child = findTreeNode(node?.children, value) + if (child) return child + } + return null +} + +export const findTreeLabel = (nodes, value) => { + const node = findTreeNode(nodes, value) + return node?.title || node?.label || null +} + +const CustomTreeSelect = ({ + placeholder, + disabled, + options = [], + value, + onChange, + ...rest +}) => { + const prevOptionsRef = useRef(options) + const treeData = Array.isArray(options) ? options : [] + + useEffect(() => { + if (prevOptionsRef.current === options) return + + if (value !== undefined && value !== null) { + const valueExists = findTreeNode(options, value) + if (!valueExists && onChange) { + onChange(undefined) + } + } + + prevOptionsRef.current = options + }, [options, value, onChange]) + + return ( + + ) +} + +CustomTreeSelect.propTypes = { + placeholder: PropTypes.string, + disabled: PropTypes.bool, + options: PropTypes.array, + value: PropTypes.any, + onChange: PropTypes.func +} + +export default CustomTreeSelect diff --git a/src/components/Dashboard/common/DashboardBreadcrumb.jsx b/src/components/Dashboard/common/DashboardBreadcrumb.jsx index d4e4a2dd..ae39fa72 100644 --- a/src/components/Dashboard/common/DashboardBreadcrumb.jsx +++ b/src/components/Dashboard/common/DashboardBreadcrumb.jsx @@ -4,7 +4,7 @@ import { Link, useLocation, useNavigate } from 'react-router-dom' import ArrowLeftIcon from '../../Icons/ArrowLeftIcon' import ArrowRightIcon from '../../Icons/ArrowRightIcon' import FilterIcon from '../../Icons/FilterIcon' -import { getModelByName } from '../../../database/ObjectModels' +import { getModelByPluralName } from '../../../database/ObjectModels' import { useTableState } from '../context/TableStateContext' const breadcrumbNameMap = { @@ -19,6 +19,7 @@ const breadcrumbNameMap = { design: 'Design', control: 'Control', preview: 'Preview', + settings: 'Settings', about: 'About' } @@ -32,13 +33,8 @@ const DashboardBreadcrumb = () => { function segmentToModel(segment) { if (segment) { - // If segment ends with 's', remove it and get the model - if (segment.endsWith('s')) { - const singularSegment = segment.slice(0, -1) - return getModelByName(singularSegment, true) - } // Otherwise, get the model as is - return getModelByName(segment, true) + return getModelByPluralName(segment, true) } return null } @@ -48,7 +44,7 @@ const DashboardBreadcrumb = () => { if (segment !== 'dashboard') { const isMainSection = mainSections.includes(segment) const model = segmentToModel(segment) - const modelLabelPlural = model?.label ? `${model.label}s` : null + const modelLabelPlural = model?.labelPlural ? model.labelPlural : null const name = breadcrumbNameMap[segment] || modelLabelPlural || segment const showFilterIcon = hasPageFilter(url) || diff --git a/src/components/Dashboard/common/FileGalleryList.jsx b/src/components/Dashboard/common/FileGalleryList.jsx new file mode 100644 index 00000000..514e12fb --- /dev/null +++ b/src/components/Dashboard/common/FileGalleryList.jsx @@ -0,0 +1,362 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { Button, Card, Flex, Divider } from 'antd' +import PropTypes from 'prop-types' +import { useMediaQuery } from 'react-responsive' +import FilePreview from './FilePreview' +import ScrollBox from './ScrollBox' +import Thumbnail from './Thumbnail' +import ChevronLeftIcon from '../../Icons/ChevronLeftIcon' +import ChevronRightIcon from '../../Icons/ChevronRightIcon' +import BinIcon from '../../Icons/BinIcon' + +const GALLERY_PADDING = 24 +const MIN_SIDE_PADDING = 66 +const MAX_SLIDE_HEIGHT = 600 + +const getFileKey = (file, index) => file?._id || file?.id || index + +const getSlideLayout = (containerWidth, naturalWidth, naturalHeight) => { + const availableWidth = Math.max(0, containerWidth - MIN_SIDE_PADDING * 2) + if (!containerWidth || availableWidth <= 0) { + return { + padding: MIN_SIDE_PADDING, + width: 0, + height: 0 + } + } + + const aspect = + naturalWidth > 0 && naturalHeight > 0 ? naturalWidth / naturalHeight : 1 + + let width = availableWidth + let height = width / aspect + + if (height > MAX_SLIDE_HEIGHT) { + height = MAX_SLIDE_HEIGHT + width = height * aspect + } + + return { + padding: Math.max(MIN_SIDE_PADDING, (containerWidth - width) / 2), + width, + height + } +} + +const FileGalleryList = ({ + files = [], + editing = false, + onRemove, + uploadSlot = null +}) => { + const filesToRender = useMemo( + () => (Array.isArray(files) ? files : files ? [files] : []), + [files] + ) + const isMobile = useMediaQuery({ maxWidth: 768 }) + const previewRef = useRef(null) + const scrollElRef = useRef(null) + const slideRefs = useRef([]) + const [containerWidth, setContainerWidth] = useState(0) + const [imageSizes, setImageSizes] = useState({}) + const [activeIndex, setActiveIndex] = useState(0) + + const slideLayouts = useMemo( + () => + filesToRender.map((file, index) => { + const size = imageSizes[getFileKey(file, index)] + return getSlideLayout(containerWidth, size?.width, size?.height) + }), + [containerWidth, filesToRender, imageSizes] + ) + + const slideHeight = useMemo(() => { + const heights = slideLayouts.map((layout) => layout.height) + return Math.max(0, ...heights) + }, [slideLayouts]) + + useEffect(() => { + const el = previewRef.current + if (!el) return undefined + + const updateWidth = () => { + setContainerWidth(el.clientWidth) + } + + updateWidth() + const observer = new ResizeObserver(updateWidth) + observer.observe(el) + return () => observer.disconnect() + }, [filesToRender.length]) + + useEffect(() => { + const root = previewRef.current + if (!root) return undefined + + const collectSizes = () => { + const next = {} + filesToRender.forEach((file, index) => { + const img = slideRefs.current[index]?.querySelector('img') + if (img?.naturalWidth && img.naturalHeight) { + next[getFileKey(file, index)] = { + width: img.naturalWidth, + height: img.naturalHeight + } + } + }) + setImageSizes((prev) => { + const nextKeys = Object.keys(next) + if ( + nextKeys.length === Object.keys(prev).length && + nextKeys.every( + (key) => + prev[key]?.width === next[key].width && + prev[key]?.height === next[key].height + ) + ) { + return prev + } + return next + }) + } + + const onLoadCapture = (event) => { + if (event.target?.tagName === 'IMG') { + collectSizes() + } + } + + root.addEventListener('load', onLoadCapture, true) + collectSizes() + const mutationObserver = new MutationObserver(collectSizes) + mutationObserver.observe(root, { childList: true, subtree: true }) + + return () => { + root.removeEventListener('load', onLoadCapture, true) + mutationObserver.disconnect() + } + }, [filesToRender]) + + useEffect(() => { + if (activeIndex >= filesToRender.length) { + setActiveIndex(Math.max(0, filesToRender.length - 1)) + } + }, [activeIndex, filesToRender.length]) + + useEffect(() => { + const root = scrollElRef.current + if (!root || filesToRender.length === 0) return undefined + + const observer = new IntersectionObserver( + (entries) => { + const visible = entries + .filter((entry) => entry.isIntersecting) + .sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0] + if (!visible) return + const index = slideRefs.current.findIndex( + (node) => node === visible.target + ) + if (index >= 0) { + setActiveIndex(index) + } + }, + { root, threshold: 0.6 } + ) + + slideRefs.current.forEach((node) => { + if (node) observer.observe(node) + }) + + return () => observer.disconnect() + }, [filesToRender.length, containerWidth, slideHeight]) + + const scrollToIndex = useCallback((index) => { + const nextIndex = Math.max(0, Math.min(index, slideRefs.current.length - 1)) + const node = slideRefs.current[nextIndex] + if (!node) return + node.scrollIntoView({ + behavior: 'smooth', + inline: 'center', + block: 'nearest' + }) + setActiveIndex(nextIndex) + }, []) + + const handleRemove = (fileToRemove) => { + onRemove?.(fileToRemove) + } + + const activePadding = slideLayouts[activeIndex]?.padding || MIN_SIDE_PADDING + + return ( + + {filesToRender.length > 0 ? ( +
+
+ ) : null} +
+ {filesToRender.length > 0 ? ( + + ) : null} + + {filesToRender.map((file, index) => ( +
scrollToIndex(index)} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault() + scrollToIndex(index) + } + }} + role='button' + tabIndex={0} + aria-label={`Show image ${index + 1}`} + aria-current={index === activeIndex ? 'true' : undefined} + > + + {editing ? ( + +
+ ))} + {editing && uploadSlot ? ( +
{uploadSlot}
+ ) : null} +
+
+
+ ) +} + +FileGalleryList.propTypes = { + files: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), + editing: PropTypes.bool, + onRemove: PropTypes.func, + uploadSlot: PropTypes.node +} + +export default FileGalleryList diff --git a/src/components/Dashboard/common/FileList.jsx b/src/components/Dashboard/common/FileList.jsx index c8e133f4..136b5c29 100644 --- a/src/components/Dashboard/common/FileList.jsx +++ b/src/components/Dashboard/common/FileList.jsx @@ -7,11 +7,13 @@ import DownloadIcon from '../../Icons/DownloadIcon' import { useContext, useState } from 'react' import { ApiServerContext } from '../context/ApiServerContext' import FilePreview from './FilePreview' +import FileGalleryList from './FileGalleryList' import EyeSlashIcon from '../../Icons/EyeSlashIcon' import { getModelByName } from '../../../database/ObjectModels' import InfoCircleIcon from '../../Icons/InfoCircleIcon' import { useNavigate } from 'react-router-dom' import ElipsisText from './ElipsisText' +import MissingPlaceholder from './MissingPlaceholder' const FileList = ({ files, @@ -24,6 +26,8 @@ const FileList = ({ defaultPreviewOpen = false, minimal = false, card = true, + gallery = false, + uploadSlot = null, maxWidth = '100%' }) => { const { fetchFileContent, flushFile } = useContext(ApiServerContext) @@ -37,10 +41,6 @@ const FileList = ({ ? !files || !Array.isArray(files) || files.length === 0 : !files - if (hasNoItems) { - return null - } - const handleRemove = (fileToRemove) => { flushFile(fileToRemove._id) if (multiple) { @@ -60,7 +60,41 @@ const FileList = ({ await fetchFileContent(file, true) } - const filesToRender = multiple ? files : [files] + const filesToRender = multiple + ? Array.isArray(files) + ? files + : [] + : files + ? [files] + : [] + + const rootStyle = { + minWidth: 0, + maxWidth, + ...(card + ? { width: '100%' } + : { width: 'fit-content', alignSelf: 'flex-start' }) + } + + if (gallery && !minimal) { + if (hasNoItems && !(gallery && (editing || uploadSlot))) { + return + } + return ( +
+ +
+ ) + } + + if (hasNoItems && !(gallery && (editing || uploadSlot))) { + return null + } const renderFileContent = (file) => ( ) - const rootStyle = { - minWidth: 0, - maxWidth, - ...(card - ? { width: '100%' } - : { width: 'fit-content', alignSelf: 'flex-start' }) - } - return (
{filesToRender.map((file, index) => { @@ -205,6 +231,8 @@ FileList.propTypes = { showDownload: PropTypes.bool, defaultPreviewOpen: PropTypes.bool, card: PropTypes.bool, + gallery: PropTypes.bool, + uploadSlot: PropTypes.node, minimal: PropTypes.bool, maxWidth: PropTypes.string } diff --git a/src/components/Dashboard/common/FilePreview.jsx b/src/components/Dashboard/common/FilePreview.jsx index bc33a53f..73f5e3b6 100644 --- a/src/components/Dashboard/common/FilePreview.jsx +++ b/src/components/Dashboard/common/FilePreview.jsx @@ -1,12 +1,22 @@ import PropTypes from 'prop-types' import { ApiServerContext } from '../context/ApiServerContext' -import { useCallback, useContext, useEffect, useState, memo } from 'react' +import { + useCallback, + useContext, + useEffect, + useState, + memo, + useRef +} from 'react' import LoadingPlaceholder from './LoadingPlaceholder' import GCodePreview from './GCodePreview' import ThreeDPreview from './ThreeDPreview' import { AuthContext } from '../context/AuthContext' const FilePreview = ({ file, style = {} }) => { + useEffect(() => { + console.log('FILEPREVIEWFILE', file) + }, [file]) const { token } = useContext(AuthContext) const { fetchFileContent } = useContext(ApiServerContext) @@ -14,6 +24,8 @@ const FilePreview = ({ file, style = {} }) => { const [loading, setLoading] = useState(true) const [error, setError] = useState(null) + const currentId = useRef(null) + const fetchPreview = useCallback(async () => { if (error != null) { return @@ -31,7 +43,8 @@ const FilePreview = ({ file, style = {} }) => { }, [file, fetchFileContent, error]) useEffect(() => { - if (file?.type && token != null) { + if (file?.type && token != null && file._id !== currentId.current) { + currentId.current = file._id fetchPreview() } }, [file._id, file?.type, fetchPreview, token]) diff --git a/src/components/Dashboard/common/FileUpload.jsx b/src/components/Dashboard/common/FileUpload.jsx index 6fb07185..94dc1c4b 100644 --- a/src/components/Dashboard/common/FileUpload.jsx +++ b/src/components/Dashboard/common/FileUpload.jsx @@ -28,6 +28,62 @@ const getFileIdentity = (value, multiple) => { return value?._id || value || '' } +const getMasterFilterAccept = (masterFilter) => + Array.isArray(masterFilter) && masterFilter.length > 0 + ? masterFilter.join(',') + : undefined + +const FileUploadDropzone = ({ onUploaded, multiple = true, masterFilter = [] }) => { + const { uploadFile } = useContext(ApiServerContext) + const [uploading, setUploading] = useState(false) + const [uploadProgress, setUploadProgress] = useState(0) + + const handleFileUpload = async (file) => { + try { + setUploading(true) + const uploadedFile = await uploadFile(file, {}, (progress) => { + setUploadProgress(progress) + }) + if (uploadedFile) { + onUploaded?.(uploadedFile) + } + } catch (error) { + console.error('File upload failed:', error) + } finally { + setUploading(false) + setUploadProgress(0) + } + return false + } + + return ( + + {uploading ? ( + uploadProgress > 0 && uploadProgress < 100 ? ( + {uploadProgress}% + ) : ( + + ) + ) : ( + + )} + + ) +} + +FileUploadDropzone.propTypes = { + onUploaded: PropTypes.func, + multiple: PropTypes.bool, + masterFilter: PropTypes.array +} + const FileUpload = ({ value, onChange, @@ -36,6 +92,7 @@ const FileUpload = ({ defaultPreviewOpen = false, showPreview = true, showInfo, + gallery = false, masterFilter = [] }) => { const { uploadFile } = useContext(ApiServerContext) @@ -135,6 +192,37 @@ const FileUpload = ({ ) } + if (gallery) { + return ( + { + if (multiple) { + const existing = Array.isArray(currentFiles) ? currentFiles : [] + updateCurrentFiles([...existing, uploadedFile]) + } else { + updateCurrentFiles(uploadedFile) + } + }} + /> + } + onChange={(updatedFiles) => { + updateCurrentFiles(updatedFiles) + }} + /> + ) + } + return ( {hasNoItems && uploading == false ? ( @@ -220,6 +308,7 @@ FileUpload.propTypes = { showInfo: PropTypes.bool, defaultPreviewOpen: PropTypes.bool, minimal: PropTypes.bool, + gallery: PropTypes.bool, masterFilter: PropTypes.array } diff --git a/src/components/Dashboard/common/ObjectForm.jsx b/src/components/Dashboard/common/ObjectForm.jsx index 45f0f715..3e1e67cb 100644 --- a/src/components/Dashboard/common/ObjectForm.jsx +++ b/src/components/Dashboard/common/ObjectForm.jsx @@ -783,19 +783,16 @@ const ObjectForm = forwardRef( isEditingRef.current = false onStateChangeRef.current({ isEditing: isEditingRef.current }) - serverObjectData.current = updatedObject - const computedEntries = calculateComputedValues( - updatedObject, - model, - { - skipObjectChildrenValue: true - } - ) + const mergedObject = mergeFormData(updatedObject, currentFormData) + serverObjectData.current = mergedObject + const computedEntries = calculateComputedValues(mergedObject, model, { + skipObjectChildrenValue: true + }) const nextObjectData = applyComputedEntries( - updatedObject, + mergedObject, computedEntries ) - copyObjectChildren(nextObjectData, updatedObject, model) + copyObjectChildren(nextObjectData, mergedObject, model) setObjectData({ ...nextObjectData, _isEditing: false diff --git a/src/components/Dashboard/common/ObjectProperty.jsx b/src/components/Dashboard/common/ObjectProperty.jsx index b7a86675..97fa719f 100644 --- a/src/components/Dashboard/common/ObjectProperty.jsx +++ b/src/components/Dashboard/common/ObjectProperty.jsx @@ -6,7 +6,8 @@ import { InputNumber, Form, Select, - Switch + Switch, + Tree } from 'antd' import IdDisplay from './IdDisplay' import TimeDisplay from './TimeDisplay' @@ -41,6 +42,7 @@ import ObjectTypeSelect from './ObjectTypeSelect' import ObjectTypeDisplay from './ObjectTypeDisplay' import CodeBlockEditor from './CodeBlockEditor' import CustomSelect from './CustomSelect' +import CustomTreeSelect, { findTreeLabel } from './CustomTreeSelect' import StateDisplay from './StateDisplay' import AlertsDisplay from './AlertsDisplay' import FileUpload from './FileUpload' @@ -136,6 +138,7 @@ const ObjectProperty = ({ hiddenPropertyWidth = '100px', inherit = true, scrollHeight, + gallery = false, ...rest }) => { if (typeof value === 'function') { @@ -270,6 +273,28 @@ const ObjectProperty = ({ ) } } + case 'treeSelect': { + if (Array.isArray(value) && value.length) { + return ( + + ) + } + const treeLabel = findTreeLabel(options, value) + if (treeLabel || (value != null && value !== '')) { + return {treeLabel || String(value)} + } + return ( + + n/a + + ) + } case 'priceMode': switch (value) { case 'margin': @@ -655,6 +680,8 @@ const ObjectProperty = ({ showPreview={showPreview} showInfo={showHyperlink} showDownload={showDownload} + gallery={gallery} + editing={false} masterFilter={masterFilter} /> ) @@ -670,6 +697,8 @@ const ObjectProperty = ({ defaultPreviewOpen={previewOpen} showPreview={showPreview} showInfo={showHyperlink} + gallery={gallery} + editing={false} masterFilter={masterFilter} /> ) @@ -838,6 +867,14 @@ const ObjectProperty = ({ {...inputProps} /> ) + case 'treeSelect': + return ( + + ) case 'state': if (options?.length && !readOnly) { return ( @@ -1002,6 +1039,7 @@ const ObjectProperty = ({ defaultPreviewOpen={previewOpen} showPreview={showPreview} showInfo={showHyperlink} + gallery={gallery} masterFilter={masterFilter} {...inputProps} /> @@ -1014,6 +1052,7 @@ const ObjectProperty = ({ defaultPreviewOpen={previewOpen} showPreview={showPreview} showInfo={showHyperlink} + gallery={gallery} masterFilter={masterFilter} {...inputProps} /> @@ -1073,7 +1112,7 @@ const ObjectProperty = ({ } ObjectProperty.propTypes = { - type: PropTypes.string.isRequired, + type: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired, modelType: PropTypes.string, value: PropTypes.oneOfType([PropTypes.any, PropTypes.func]), isEditing: PropTypes.bool, @@ -1101,7 +1140,7 @@ ObjectProperty.propTypes = { showPreview: PropTypes.bool, useFormItem: PropTypes.bool, showHyperlink: PropTypes.bool, - options: PropTypes.array, + options: PropTypes.oneOfType([PropTypes.array, PropTypes.func]), showSince: PropTypes.bool, inTable: PropTypes.bool, loading: PropTypes.bool, @@ -1112,7 +1151,8 @@ ObjectProperty.propTypes = { style: PropTypes.object, hiddenPropertyWidth: PropTypes.string, inherit: PropTypes.bool, - scrollHeight: PropTypes.string + scrollHeight: PropTypes.string, + gallery: PropTypes.bool } export default ObjectProperty diff --git a/src/components/Dashboard/common/StateDisplay.jsx b/src/components/Dashboard/common/StateDisplay.jsx index 881e25a9..5b400b99 100644 --- a/src/components/Dashboard/common/StateDisplay.jsx +++ b/src/components/Dashboard/common/StateDisplay.jsx @@ -20,10 +20,19 @@ const StateDisplay = ({ 'queued', 'printing', 'used', - 'deploying' + 'deploying', + 'syncing', + 'publishing', + 'unpublishing' ] const orangeProgressTypes = ['used', 'deploying', 'queued'] - const activeProgressTypes = ['printing', 'deploying'] + const activeProgressTypes = [ + 'printing', + 'deploying', + 'publishing', + 'unpublishing', + 'syncing' + ] const currentState = state || { type: 'unknown', diff --git a/src/components/Dashboard/common/StateTag.jsx b/src/components/Dashboard/common/StateTag.jsx index 156b3b94..b7635737 100644 --- a/src/components/Dashboard/common/StateTag.jsx +++ b/src/components/Dashboard/common/StateTag.jsx @@ -100,6 +100,14 @@ const StateTag = ({ state, showBadge = true, showTag = true, style = {} }) => { status = 'processing' text = 'Syncing' break + case 'publishing': + status = 'processing' + text = 'Publishing' + break + case 'unpublishing': + status = 'processing' + text = 'Unpublishing' + break case 'disconnected': status = 'default' text = 'Disconnected' @@ -112,6 +120,10 @@ const StateTag = ({ state, showBadge = true, showTag = true, style = {} }) => { status = 'warning' text = 'Queued' break + case 'pending': + status = 'default' + text = 'Pending' + break case 'used': status = 'warning' text = 'Used' diff --git a/src/components/Dashboard/utils/Utils.js b/src/components/Dashboard/utils/Utils.js index 3d1ba514..77525603 100644 --- a/src/components/Dashboard/utils/Utils.js +++ b/src/components/Dashboard/utils/Utils.js @@ -1,4 +1,8 @@ +import get from 'lodash/get' import mergeWith from 'lodash/mergeWith' +import set from 'lodash/set' + +const NESTED_OBJECT_KEYS = ['_id', '_reference', 'name', 'state'] export function capitalizeFirstLetter(string) { try { @@ -71,3 +75,72 @@ export function mergeFormData(...sources) { mergeFormCustomizer ) } + +const pickNestedObjectFields = (value) => { + if (value == null || typeof value !== 'object') { + return value + } + if (Array.isArray(value)) { + return value.map(pickNestedObjectFields) + } + + const picked = {} + NESTED_OBJECT_KEYS.forEach((key) => { + if (value[key] !== undefined) { + picked[key] = value[key] + } + }) + return picked +} + +const stripProperties = (data, properties) => { + if (data == null || typeof data !== 'object' || !Array.isArray(properties)) { + return data + } + + const next = Array.isArray(data) ? [...data] : { ...data } + + properties.forEach((property) => { + if (!property?.name) return + + const current = get(next, property.name) + + if (property.type === 'object') { + if (current === undefined) return + set(next, property.name, pickNestedObjectFields(current)) + return + } + + if (property.type === 'objectList') { + if (!Array.isArray(current)) return + set(next, property.name, current.map(pickNestedObjectFields)) + return + } + + if (!Array.isArray(property.properties) || property.properties.length === 0) { + return + } + + if (property.type === 'objectChildren' || Array.isArray(current)) { + if (!Array.isArray(current)) return + set( + next, + property.name, + current.map((child) => stripProperties(child, property.properties)) + ) + return + } + + if (current != null && typeof current === 'object') { + set(next, property.name, stripProperties(current, property.properties)) + } + }) + + return next +} + +// Before create/update, keep nested object / objectList refs as stubs so we +// do not post populated fields (e.g. marketplace category trees). +export function stripNestedObjectProperties(data, modelDefinition) { + return stripProperties(data, modelDefinition?.properties) +} diff --git a/src/components/Icons/FulfillmentPolicyIcon.jsx b/src/components/Icons/FulfillmentPolicyIcon.jsx new file mode 100644 index 00000000..d6445a48 --- /dev/null +++ b/src/components/Icons/FulfillmentPolicyIcon.jsx @@ -0,0 +1,6 @@ +import Icon from '@ant-design/icons' +import CustomIconSvg from '../../../assets/icons/fulfillmentpolicyicon.svg?react' + +const FulfillmentPolicyIcon = (props) => + +export default FulfillmentPolicyIcon diff --git a/src/components/Icons/PaymentPolicyIcon.jsx b/src/components/Icons/PaymentPolicyIcon.jsx new file mode 100644 index 00000000..d93c0b91 --- /dev/null +++ b/src/components/Icons/PaymentPolicyIcon.jsx @@ -0,0 +1,6 @@ +import Icon from '@ant-design/icons' +import CustomIconSvg from '../../../assets/icons/paymentpolicyicon.svg?react' + +const PaymentPolicyIcon = (props) => + +export default PaymentPolicyIcon diff --git a/src/components/Icons/ReturnPolicyIcon.jsx b/src/components/Icons/ReturnPolicyIcon.jsx new file mode 100644 index 00000000..86d45d84 --- /dev/null +++ b/src/components/Icons/ReturnPolicyIcon.jsx @@ -0,0 +1,6 @@ +import Icon from '@ant-design/icons' +import CustomIconSvg from '../../../assets/icons/returnpolicyicon.svg?react' + +const ReturnPolicyIcon = (props) => + +export default ReturnPolicyIcon diff --git a/src/components/Icons/sidebarIconMap.jsx b/src/components/Icons/sidebarIconMap.jsx index c31016f0..d5300e0f 100644 --- a/src/components/Icons/sidebarIconMap.jsx +++ b/src/components/Icons/sidebarIconMap.jsx @@ -21,6 +21,7 @@ import ClientIcon from './ClientIcon' import SalesOrderIcon from './SalesOrderIcon' import MarketplaceIcon from './MarketplaceIcon' import ListingIcon from './ListingIcon' +import ListingVarientIcon from './ListingVarientIcon' import FinanceIcon from './FinanceIcon' import InvoiceIcon from './InvoiceIcon' import PaymentIcon from './PaymentIcon' @@ -51,6 +52,9 @@ import CourierIcon from './CourierIcon' import CourierServiceIcon from './CourierServiceIcon' import TaxRateIcon from './TaxRateIcon' import TaxRecordIcon from './TaxRecordIcon' +import FulfillmentPolicyIcon from './FulfillmentPolicyIcon' +import ReturnPolicyIcon from './ReturnPolicyIcon' +import PaymentPolicyIcon from './PaymentPolicyIcon' import AppPasswordIcon from './AppPasswordIcon' import InfoCircleIcon from './InfoCircleIcon' @@ -80,6 +84,10 @@ const sidebarIconMap = { salesOrder: , marketplace: , listing: , + listingVarient: , + fulfillmentPolicy: , + returnPolicy: , + paymentPolicy: , finance: , invoice: , payment: , diff --git a/src/database/ObjectModels.js b/src/database/ObjectModels.js index 0cdf4d50..dab7aba6 100644 --- a/src/database/ObjectModels.js +++ b/src/database/ObjectModels.js @@ -49,6 +49,9 @@ import { SalesOrder } from './models/SalesOrder.js' import { Marketplace } from './models/Marketplace.js' import { Listing } from './models/Listing.js' import { ListingVarient } from './models/ListingVarient.js' +import { FulfillmentPolicy } from './models/FulfillmentPolicy.js' +import { ReturnPolicy } from './models/ReturnPolicy.js' +import { PaymentPolicy } from './models/PaymentPolicy.js' import QuestionCircleIcon from '../components/Icons/QuestionCircleIcon' export const objectModels = [ @@ -102,7 +105,10 @@ export const objectModels = [ SalesOrder, Marketplace, Listing, - ListingVarient + ListingVarient, + FulfillmentPolicy, + ReturnPolicy, + PaymentPolicy ] // Re-export individual models for direct access @@ -157,7 +163,10 @@ export { SalesOrder, Marketplace, Listing, - ListingVarient + ListingVarient, + FulfillmentPolicy, + ReturnPolicy, + PaymentPolicy } export function getModelByName(name, ignoreCase = false) { @@ -179,6 +188,27 @@ export function getModelByName(name, ignoreCase = false) { ) } +export function getModelByPluralName(pluralName, ignoreCase = false) { + function formatName(formattedName) { + if (ignoreCase == true) { + formattedName = formattedName.toUpperCase().replace(' ', '') + } + return formattedName + } + return ( + objectModels.find( + (meta) => formatName(meta.labelPlural) === formatName(pluralName) + ) || { + name: 'unknown', + label: 'Unknown', + prefix: 'UNK', + icon: QuestionCircleIcon, + url: () => '#', + properties: {} + } + ) +} + export function getModelProperty(name, property) { const model = getModelByName(name) diff --git a/src/database/Sidebars.js b/src/database/Sidebars.js index b16cc600..b4943c06 100644 --- a/src/database/Sidebars.js +++ b/src/database/Sidebars.js @@ -24,10 +24,7 @@ const SIDEBARS = { key: 'sales', label: 'Sales', iconKey: 'sales', - items: salesSidebarItems, - routeAliases: { - '/dashboard/sales/listingvarients': 'listings' - } + items: salesSidebarItems }, finance: { key: 'finance', diff --git a/src/database/models/File.js b/src/database/models/File.js index cb2a08c8..4eff87c5 100644 --- a/src/database/models/File.js +++ b/src/database/models/File.js @@ -30,6 +30,15 @@ export const File = { label: 'List', icon: ListIcon }, + { + name: 'new', + type: 'page', + pageName: 'info', + label: 'New File', + visible: () => { + return false + } + }, { name: 'info', type: 'page', @@ -117,14 +126,7 @@ export const File = { 'updatedAt', '_reference' ], - sorters: [ - 'name', - 'type', - 'size', - 'createdAt', - 'temp', - 'updatedAt' - ], + sorters: ['name', 'type', 'size', 'createdAt', 'temp', 'updatedAt'], group: ['type'], properties: [ { diff --git a/src/database/models/FulfillmentPolicy.js b/src/database/models/FulfillmentPolicy.js new file mode 100644 index 00000000..2e00ac32 --- /dev/null +++ b/src/database/models/FulfillmentPolicy.js @@ -0,0 +1,231 @@ +import { createElement, lazy } from 'react' + +const FulfillmentPolicyInfo = lazy( + () => + import('../../components/Dashboard/Sales/FulfillmentPolicies/FulfillmentPolicyInfo') +) +const NewFulfillmentPolicy = lazy( + () => + import('../../components/Dashboard/Sales/FulfillmentPolicies/NewFulfillmentPolicy') +) +const DeleteObject = lazy( + () => import('../../components/Dashboard/common/DeleteObject') +) +import FulfillmentPolicyIcon from '../../components/Icons/FulfillmentPolicyIcon' +import PlusIcon from '../../components/Icons/PlusIcon' +import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' +import EditIcon from '../../components/Icons/EditIcon' +import CheckIcon from '../../components/Icons/CheckIcon' +import XMarkIcon from '../../components/Icons/XMarkIcon' +import BinIcon from '../../components/Icons/BinIcon' +import ListIcon from '../../components/Icons/ListIcon' +import { marketplaceSyncMappingsProperty } from './marketplaceMappings' + +export const FulfillmentPolicy = { + name: 'fulfillmentPolicy', + label: 'Fulfillment Policy', + labelPlural: 'Fulfillment Policies', + url: '/dashboard/sales/fulfillmentpolicies', + prefix: 'FPL', + endpoint: 'fulfillmentpolicies', + icon: FulfillmentPolicyIcon, + actions: [ + { + name: 'new', + type: 'modal', + pageName: 'list', + modalWidth: 700, + label: 'New Fulfillment Policy', + icon: PlusIcon, + content: (objectData, { onOk } = {}) => { + return createElement(NewFulfillmentPolicy, { + defaultValues: objectData, + onOk, + reset: true + }) + } + }, + { + name: 'list', + type: 'page', + pageName: 'list', + label: 'List', + icon: ListIcon + }, + { + name: 'info', + type: 'page', + pageName: 'info', + label: 'Info', + default: true, + row: true, + icon: InfoCircleIcon + }, + { + name: 'edit', + type: 'page', + pageName: 'info', + label: 'Edit', + row: true, + icon: EditIcon, + visible: (objectData) => { + return !(objectData?._isEditing && objectData?._isEditing == true) + } + }, + { + name: 'cancelEdit', + label: 'Cancel Edits', + type: 'page', + pageName: 'info', + icon: XMarkIcon, + visible: (objectData) => { + return objectData?._isEditing && objectData?._isEditing == true + } + }, + { + name: 'finishEdit', + label: 'Save Edits', + type: 'page', + pageName: 'info', + icon: CheckIcon, + visible: (objectData) => { + return objectData?._isEditing && objectData?._isEditing == true + } + }, + { type: 'divider' }, + { + name: 'delete', + type: 'modal', + modalWidth: 520, + modalCentered: true, + label: 'Delete', + icon: BinIcon, + danger: true, + content: (objectData, { onOk } = {}) => { + return createElement(DeleteObject, { objectData, onOk }) + } + } + ], + pages: [ + { + name: 'info', + content: () => createElement(FulfillmentPolicyInfo) + } + ], + columns: [ + '_reference', + 'name', + 'handlingTime', + 'courierServices', + 'createdAt', + 'updatedAt' + ], + filters: [ + 'name', + 'handlingTime', + 'courierServices', + 'localPickup', + 'createdAt', + 'updatedAt', + '_reference' + ], + sorters: ['name', 'handlingTime', 'createdAt', '_id', 'updatedAt'], + properties: [ + { + name: '_id', + label: 'ID', + columnFixed: 'left', + type: 'id', + objectType: 'fulfillmentPolicy', + showCopy: true, + columnWidth: 140 + }, + { + name: 'createdAt', + label: 'Created At', + type: 'dateTime', + readOnly: true, + columnWidth: 175 + }, + { + name: '_reference', + label: 'Reference', + type: 'reference', + columnFixed: 'left', + objectType: 'fulfillmentPolicy', + showCopy: true, + readOnly: true, + columnWidth: 180 + }, + { + name: 'updatedAt', + label: 'Updated At', + type: 'dateTime', + readOnly: true, + columnWidth: 175 + }, + { + name: 'name', + label: 'Name', + columnFixed: 'left', + required: true, + type: 'text', + columnWidth: 220 + }, + { + name: 'description', + label: 'Description', + type: 'text', + required: false, + span: 2, + columnWidth: 280 + }, + { + name: 'handlingTime', + label: 'Handling Time', + type: 'number', + min: 0, + required: false, + suffix: 'days', + columnWidth: 175 + }, + { + name: 'courierServices', + label: 'Courier Services', + type: 'objectList', + objectType: 'courierService', + showHyperlink: true, + required: false, + columnWidth: 250 + }, + { + name: 'localPickup', + label: 'Local Pickup', + type: 'bool', + required: false, + columnWidth: 140 + }, + { + name: 'globalShipping', + label: 'Global Shipping', + type: 'bool', + required: false, + columnWidth: 150 + }, + { + name: 'freightShipping', + label: 'Freight Shipping', + type: 'bool', + required: false, + columnWidth: 160 + }, + { + name: 'pickupDropOff', + label: 'Pickup Drop Off', + type: 'bool', + required: false, + columnWidth: 160 + }, + marketplaceSyncMappingsProperty('fulfillmentPolicy') + ] +} diff --git a/src/database/models/Listing.js b/src/database/models/Listing.js index 0d7fc111..7385165e 100644 --- a/src/database/models/Listing.js +++ b/src/database/models/Listing.js @@ -112,7 +112,10 @@ export const Listing = { visible: (objectData) => objectData?.state?.type === 'draft' || objectData?.state?.type === 'inactive', - disabled: (objectData) => objectData?.state?.type === 'syncing', + disabled: (objectData) => + ['syncing', 'publishing', 'unpublishing'].includes( + objectData?.state?.type + ), content: (objectData, { onOk } = {}) => { return createElement(PublishListing, { objectData, onOk }) } @@ -125,7 +128,10 @@ export const Listing = { icon: XMarkIcon, danger: true, visible: (objectData) => objectData?.state?.type === 'active', - disabled: (objectData) => objectData?.state?.type === 'syncing', + disabled: (objectData) => + ['syncing', 'publishing', 'unpublishing'].includes( + objectData?.state?.type + ), content: (objectData, { onOk } = {}) => { return createElement(UnpublishListing, { objectData, onOk }) } @@ -158,6 +164,9 @@ export const Listing = { 'stockLocation', 'stockQuantity', 'marketplace', + 'fulfillmentPolicy', + 'paymentPolicy', + 'returnPolicy', 'courierServices', 'state', 'condition', @@ -173,6 +182,9 @@ export const Listing = { 'stockLocation', 'stockQuantity', 'marketplace', + 'fulfillmentPolicy', + 'paymentPolicy', + 'returnPolicy', 'courierServices', 'state', 'state.type', @@ -252,6 +264,14 @@ export const Listing = { extra: 'Used as the eBay listing description. If empty, the title is sent instead.' }, + { + name: 'listingImages', + label: 'Listing Images', + type: 'fileList', + gallery: true, + required: false, + masterFilter: ['.jpg', '.jpeg', '.png', '.gif', '.webp'] + }, { name: 'lastSyncedAt', label: 'Last Synced', @@ -314,6 +334,58 @@ export const Listing = { required: true, columnWidth: 200 }, + { + name: 'fulfillmentPolicy', + label: 'Fulfillment Policy', + type: 'object', + objectType: 'fulfillmentPolicy', + showHyperlink: true, + readOnly: false, + required: false, + extra: + 'Defaults from the marketplace. Courier services are used as a fallback when this is empty.', + value: (objectData) => { + if (objectData?.fulfillmentPolicy == undefined) { + return objectData?.marketplace?.defaultFulfillmentPolicy + } + return objectData?.fulfillmentPolicy + }, + columnWidth: 220 + }, + { + name: 'paymentPolicy', + label: 'Payment Policy', + type: 'object', + objectType: 'paymentPolicy', + showHyperlink: true, + readOnly: false, + required: false, + extra: 'Defaults from the marketplace. Required when publishing to eBay.', + value: (objectData) => { + if (objectData?.paymentPolicy == undefined) { + return objectData?.marketplace?.defaultPaymentPolicy + } + return objectData?.paymentPolicy + }, + columnWidth: 200 + }, + { + name: 'returnPolicy', + label: 'Return Policy', + type: 'object', + objectType: 'returnPolicy', + showHyperlink: true, + readOnly: false, + required: false, + extra: 'Defaults from the marketplace. Required when publishing to eBay.', + value: (objectData) => { + if (objectData?.returnPolicy == undefined) { + return objectData?.marketplace?.defaultReturnPolicy + } + return objectData?.returnPolicy + }, + columnWidth: 200 + }, { name: 'state', label: 'State', @@ -378,6 +450,17 @@ export const Listing = { required: false, columnWidth: 250 }, + { + name: 'externalReference', + label: 'External Reference', + type: 'miscId', + readOnly: true, + required: false, + extra: + 'eBay item ID for this listing. Publish and unpublish reuse this ID when set.', + showCopy: true, + columnWidth: 180 + }, { name: 'courierServices', label: 'Courier Services', @@ -386,6 +469,8 @@ export const Listing = { objectType: 'courierService', readOnly: false, required: true, + extra: + 'Used for order shipping and as the eBay fulfillment fallback when no fulfillment policy is set.', columnWidth: 250 } ], @@ -414,6 +499,18 @@ export const Listing = { type: 'number', color: 'processing' }, + { + name: 'publishing.count', + label: 'Publishing', + type: 'number', + color: 'processing' + }, + { + name: 'unpublishing.count', + label: 'Unpublishing', + type: 'number', + color: 'processing' + }, { name: 'suspended.count', label: 'Suspended', diff --git a/src/database/models/ListingVarient.js b/src/database/models/ListingVarient.js index 0666805a..a5f773bc 100644 --- a/src/database/models/ListingVarient.js +++ b/src/database/models/ListingVarient.js @@ -108,7 +108,10 @@ export const ListingVarient = { visible: (objectData) => objectData?.state?.type === 'draft' || objectData?.state?.type === 'inactive', - disabled: (objectData) => objectData?.state?.type === 'syncing', + disabled: (objectData) => + ['syncing', 'publishing', 'unpublishing'].includes( + objectData?.state?.type + ), content: (objectData, { onOk } = {}) => { return createElement(PublishListingVarient, { objectData, onOk }) } @@ -121,7 +124,10 @@ export const ListingVarient = { icon: XMarkIcon, danger: true, visible: (objectData) => objectData?.state?.type === 'active', - disabled: (objectData) => objectData?.state?.type === 'syncing', + disabled: (objectData) => + ['syncing', 'publishing', 'unpublishing'].includes( + objectData?.state?.type + ), content: (objectData, { onOk } = {}) => { return createElement(UnpublishListingVarient, { objectData, onOk }) } @@ -312,6 +318,42 @@ export const ListingVarient = { type: 'dateTime', readOnly: true, columnWidth: 175 + }, + { + name: 'listingImages', + label: 'Listing Images', + type: 'fileList', + gallery: true, + required: false, + masterFilter: ['.jpg', '.jpeg', '.png', '.gif', '.webp'] + }, + { + name: 'aspects', + label: 'Aspects', + type: 'objectChildren', + required: false, + canAddRemove: true, + size: 'medium', + span: 2, + extra: + 'Required for multi-variation eBay listings. Use the same aspect names on every varient in a listing (e.g. Color, Size).', + columns: ['name', 'value'], + properties: [ + { + name: 'name', + label: 'Name', + type: 'text', + required: true, + columnWidth: 180 + }, + { + name: 'value', + label: 'Value', + type: 'text', + required: true, + columnWidth: 180 + } + ] } ] } diff --git a/src/database/models/Marketplace.js b/src/database/models/Marketplace.js index f837fa4d..5504bb0e 100644 --- a/src/database/models/Marketplace.js +++ b/src/database/models/Marketplace.js @@ -15,6 +15,21 @@ const SyncListings = lazy( const SyncOrders = lazy( () => import('../../components/Dashboard/Sales/Marketplaces/SyncOrders') ) +const SyncFulfillmentPolicies = lazy( + () => + import('../../components/Dashboard/Sales/Marketplaces/SyncFulfillmentPolicies') +) +const SyncPaymentPolicies = lazy( + () => + import('../../components/Dashboard/Sales/Marketplaces/SyncPaymentPolicies') +) +const SyncReturnPolicies = lazy( + () => + import('../../components/Dashboard/Sales/Marketplaces/SyncReturnPolicies') +) +const SyncTaxRates = lazy( + () => import('../../components/Dashboard/Sales/Marketplaces/SyncTaxRates') +) const DeleteObject = lazy( () => import('../../components/Dashboard/common/DeleteObject') ) @@ -134,19 +149,6 @@ export const Marketplace = { return createElement(SyncListings, { objectData, onOk }) } }, - { - name: 'syncMarketplace', - type: 'modal', - modalWidth: 520, - label: 'Sync Marketplace', - icon: ReloadIcon, - disabled: (objectData) => { - return objectData?.state?.type != 'ready' - }, - content: (objectData, { onOk } = {}) => { - return createElement(SyncMarketplace, { objectData, onOk }) - } - }, { name: 'syncOrders', type: 'modal', @@ -159,6 +161,58 @@ export const Marketplace = { content: (objectData, { onOk } = {}) => { return createElement(SyncOrders, { objectData, onOk }) } + }, + { + name: 'syncFulfillmentPolicies', + type: 'modal', + modalWidth: 520, + label: 'Sync Fulfillment Policies', + icon: ReloadIcon, + disabled: (objectData) => { + return objectData?.state?.type != 'ready' + }, + content: (objectData, { onOk } = {}) => { + return createElement(SyncFulfillmentPolicies, { objectData, onOk }) + } + }, + { + name: 'syncReturnPolicies', + type: 'modal', + modalWidth: 520, + label: 'Sync Return Policies', + icon: ReloadIcon, + disabled: (objectData) => { + return objectData?.state?.type != 'ready' + }, + content: (objectData, { onOk } = {}) => { + return createElement(SyncReturnPolicies, { objectData, onOk }) + } + }, + { + name: 'syncPaymentPolicies', + type: 'modal', + modalWidth: 520, + label: 'Sync Payment Policies', + icon: ReloadIcon, + disabled: (objectData) => { + return objectData?.state?.type != 'ready' + }, + content: (objectData, { onOk } = {}) => { + return createElement(SyncPaymentPolicies, { objectData, onOk }) + } + }, + { + name: 'syncTaxRates', + type: 'modal', + modalWidth: 520, + label: 'Sync Tax Rates', + icon: ReloadIcon, + disabled: (objectData) => { + return objectData?.state?.type != 'ready' + }, + content: (objectData, { onOk } = {}) => { + return createElement(SyncTaxRates, { objectData, onOk }) + } } ] }, @@ -313,17 +367,7 @@ export const Marketplace = { ], columnWidth: 150 }, - { - name: 'eBay.availableShippingServices', - label: 'Available Shipping Services', - type: 'stringList', - readOnly: true, - required: false, - visible: (objectData) => objectData?.provider === 'ebay', - extra: 'Loaded from eBay GeteBayDetails when this marketplace is synced.', - span: 2, - columnWidth: 280 - }, + { name: 'config.accessTokenExpiresAt', label: 'Access Token Expires At', @@ -331,7 +375,6 @@ export const Marketplace = { readOnly: true, columnWidth: 215 }, - { name: 'state', label: 'State', @@ -340,21 +383,7 @@ export const Marketplace = { readOnly: true, columnWidth: 260 }, - { - name: 'active', - label: 'Active', - type: 'bool', - readOnly: false, - required: true, - columnWidth: 125 - }, - { - name: 'connected', - label: 'Connected', - type: 'bool', - readOnly: true, - columnWidth: 125 - }, + { name: 'config.refreshTokenExpiresAt', label: 'Refresh Token Expires At', @@ -364,15 +393,40 @@ export const Marketplace = { columnWidth: 200 }, { - name: 'config.clientId', - label: 'Client ID', - type: 'secret', - readOnly: false, + name: 'eBay.availableShippingServices', + label: 'Available Shipping Services', + type: 'number', + readOnly: true, required: false, - columnWidth: 200, - visible: (objectData) => objectData?.provider === 'ebay' + visible: (objectData) => objectData?.provider === 'ebay', + extra: 'Loaded from eBay GeteBayDetails when this marketplace is synced.', + span: 2, + columnWidth: 280, + value: (objectData) => { + return objectData?.eBay?.availableShippingServices?.length + } + }, + { + name: 'eBay.categoryReferences', + label: 'Category References', + type: 'treeSelect', + readOnly: true, + required: false, + visible: false, + extra: + 'Loaded from the eBay taxonomy category tree when this marketplace is synced.', + span: 2, + columnWidth: 280 }, + { + name: 'active', + label: 'Active', + type: 'bool', + readOnly: false, + required: true, + columnWidth: 125 + }, { name: 'config.lastTokenRefreshAt', label: 'Last Token Refresh At', @@ -381,6 +435,23 @@ export const Marketplace = { showSince: true, columnWidth: 200 }, + { + name: 'connected', + label: 'Connected', + type: 'bool', + readOnly: true, + columnWidth: 125 + }, + + { + name: 'config.clientId', + label: 'Client ID', + type: 'secret', + readOnly: false, + required: false, + columnWidth: 200, + visible: (objectData) => objectData?.provider === 'ebay' + }, { name: 'config.clientSecret', @@ -555,6 +626,37 @@ export const Marketplace = { } return `${window.location.origin}/auth/marketplace/callback` } + }, + { + name: 'defaultFulfillmentPolicy', + label: 'Default Fulfillment Policy', + type: 'object', + objectType: 'fulfillmentPolicy', + showHyperlink: true, + required: false, + extra: + 'Used on new listings and when a listing has no fulfillment policy.', + columnWidth: 240 + }, + { + name: 'defaultPaymentPolicy', + label: 'Default Payment Policy', + type: 'object', + objectType: 'paymentPolicy', + showHyperlink: true, + required: false, + extra: 'Used on new listings and when a listing has no payment policy.', + columnWidth: 220 + }, + { + name: 'defaultReturnPolicy', + label: 'Default Return Policy', + type: 'object', + objectType: 'returnPolicy', + showHyperlink: true, + required: false, + extra: 'Used on new listings and when a listing has no return policy.', + columnWidth: 220 } ], stats: [ diff --git a/src/database/models/PartStock.js b/src/database/models/PartStock.js index dd3a22f8..183301b3 100644 --- a/src/database/models/PartStock.js +++ b/src/database/models/PartStock.js @@ -6,9 +6,20 @@ const PartStockInfo = lazy( const NewPartStock = lazy( () => import('../../components/Dashboard/Inventory/PartStocks/NewPartStock') ) +const PostPartStock = lazy( + () => + import('../../components/Dashboard/Inventory/PartStocks/PostPartStock.jsx') +) +const DeleteObject = lazy( + () => import('../../components/Dashboard/common/DeleteObject') +) import PartStockIcon from '../../components/Icons/PartStockIcon' import PlusIcon from '../../components/Icons/PlusIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' +import EditIcon from '../../components/Icons/EditIcon' +import CheckIcon from '../../components/Icons/CheckIcon' +import XMarkIcon from '../../components/Icons/XMarkIcon' +import BinIcon from '../../components/Icons/BinIcon' import ListIcon from '../../components/Icons/ListIcon' export const PartStock = { @@ -17,7 +28,6 @@ export const PartStock = { labelPlural: 'Part Stocks', url: '/dashboard/inventory/partstocks', prefix: 'PTS', - readOnly: true, icon: PartStockIcon, actions: [ { @@ -50,6 +60,75 @@ export const PartStock = { default: true, row: true, icon: InfoCircleIcon + }, + { + name: 'edit', + type: 'page', + pageName: 'info', + label: 'Edit', + row: true, + icon: EditIcon, + visible: (objectData) => { + return !(objectData?._isEditing && objectData?._isEditing == true) + }, + disabled: (objectData) => { + return objectData?.state?.type != 'draft' + } + }, + { + name: 'cancelEdit', + label: 'Cancel Edits', + type: 'page', + pageName: 'info', + icon: XMarkIcon, + visible: (objectData) => { + return objectData?._isEditing && objectData?._isEditing == true + } + }, + { + name: 'finishEdit', + label: 'Save Edits', + type: 'page', + pageName: 'info', + icon: CheckIcon, + visible: (objectData) => { + return objectData?._isEditing && objectData?._isEditing == true + } + }, + { + name: 'delete', + type: 'modal', + modalWidth: 520, + modalCentered: true, + label: 'Delete', + icon: BinIcon, + danger: true, + visible: (objectData) => { + return !(objectData?._isEditing && objectData?._isEditing == true) + }, + disabled: (objectData) => { + return objectData?.state?.type != 'draft' + }, + content: (objectData, { onOk } = {}) => { + return createElement(DeleteObject, { objectData, onOk }) + } + }, + { type: 'divider' }, + { + name: 'post', + type: 'modal', + modalWidth: 520, + label: 'Post', + icon: CheckIcon, + disabled: (objectData) => { + return objectData?._isEditing == true + }, + visible: (objectData) => { + return objectData?.state?.type == 'draft' + }, + content: (objectData, { onOk } = {}) => { + return createElement(PostPartStock, { objectData, onOk }) + } } ], pages: [ @@ -62,7 +141,6 @@ export const PartStock = { 'part', 'partSku', 'state', - 'startingQuantity', 'currentQuantity', 'stockLocation', 'createdAt', @@ -72,7 +150,6 @@ export const PartStock = { sorters: [ 'part', 'partSku', - 'startingQuantity', 'currentQuantity', 'state', 'createdAt', @@ -81,7 +158,6 @@ export const PartStock = { columns: [ '_reference', 'state', - 'startingQuantity', 'currentQuantity', 'part', 'partSku', @@ -116,13 +192,6 @@ export const PartStock = { readOnly: true, columnWidth: 180 }, - { - name: 'state', - label: 'State', - type: 'state', - readOnly: true, - columnWidth: 260 - }, { name: 'updatedAt', label: 'Updated At', @@ -131,13 +200,19 @@ export const PartStock = { columnWidth: 175 }, { - name: 'sourceType', - label: 'Source Type', - type: 'objectType', - readOnly: false, - columnWidth: 200, - required: true, - masterFilter: ['subJob', 'stockTransfer'] + name: 'state', + label: 'State', + type: 'state', + readOnly: true, + columnWidth: 260 + }, + + { + name: 'postedAt', + label: 'Posted At', + type: 'dateTime', + readOnly: true, + columnWidth: 175 }, { name: 'part', @@ -169,36 +244,22 @@ export const PartStock = { showHyperlink: true, columnWidth: 200 }, - - { - name: 'source', - label: 'Source', - type: 'object', - readOnly: false, - required: true, - columnWidth: 200, - objectType: (objectData) => { - return objectData?.sourceType - }, - showHyperlink: true - }, { name: 'currentQuantity', label: 'Current Quantity', type: 'number', - readOnly: true, columnWidth: 200, - required: true, - value: (objectData) => { - if (objectData?.state?.type === 'new') { - return objectData?.startingQuantity - } else { - return objectData.currentQuantity - } - } + required: true } ], stats: [ + { + name: 'draft.count', + label: 'Draft', + type: 'number', + color: 'default', + history: false + }, { name: 'new.count', label: 'New', diff --git a/src/database/models/PaymentPolicy.js b/src/database/models/PaymentPolicy.js new file mode 100644 index 00000000..7c690a1a --- /dev/null +++ b/src/database/models/PaymentPolicy.js @@ -0,0 +1,187 @@ +import { createElement, lazy } from 'react' + +const PaymentPolicyInfo = lazy( + () => + import('../../components/Dashboard/Finance/PaymentPolicies/PaymentPolicyInfo') +) +const NewPaymentPolicy = lazy( + () => + import('../../components/Dashboard/Finance/PaymentPolicies/NewPaymentPolicy') +) +const DeleteObject = lazy( + () => import('../../components/Dashboard/common/DeleteObject') +) +import PaymentPolicyIcon from '../../components/Icons/PaymentPolicyIcon' +import PlusIcon from '../../components/Icons/PlusIcon' +import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' +import EditIcon from '../../components/Icons/EditIcon' +import CheckIcon from '../../components/Icons/CheckIcon' +import XMarkIcon from '../../components/Icons/XMarkIcon' +import BinIcon from '../../components/Icons/BinIcon' +import ListIcon from '../../components/Icons/ListIcon' +import { marketplaceSyncMappingsProperty } from './marketplaceMappings' + +export const PaymentPolicy = { + name: 'paymentPolicy', + label: 'Payment Policy', + labelPlural: 'Payment Policies', + url: '/dashboard/finance/paymentpolicies', + prefix: 'PPL', + endpoint: 'paymentpolicies', + icon: PaymentPolicyIcon, + actions: [ + { + name: 'new', + type: 'modal', + pageName: 'list', + modalWidth: 700, + label: 'New Payment Policy', + icon: PlusIcon, + content: (objectData, { onOk } = {}) => { + return createElement(NewPaymentPolicy, { + defaultValues: objectData, + onOk, + reset: true + }) + } + }, + { + name: 'list', + type: 'page', + pageName: 'list', + label: 'List', + icon: ListIcon + }, + { + name: 'info', + type: 'page', + pageName: 'info', + label: 'Info', + default: true, + row: true, + icon: InfoCircleIcon + }, + { + name: 'edit', + type: 'page', + pageName: 'info', + label: 'Edit', + row: true, + icon: EditIcon, + visible: (objectData) => { + return !(objectData?._isEditing && objectData?._isEditing == true) + } + }, + { + name: 'cancelEdit', + label: 'Cancel Edits', + type: 'page', + pageName: 'info', + icon: XMarkIcon, + visible: (objectData) => { + return objectData?._isEditing && objectData?._isEditing == true + } + }, + { + name: 'finishEdit', + label: 'Save Edits', + type: 'page', + pageName: 'info', + icon: CheckIcon, + visible: (objectData) => { + return objectData?._isEditing && objectData?._isEditing == true + } + }, + { type: 'divider' }, + { + name: 'delete', + type: 'modal', + modalWidth: 520, + modalCentered: true, + label: 'Delete', + icon: BinIcon, + danger: true, + content: (objectData, { onOk } = {}) => { + return createElement(DeleteObject, { objectData, onOk }) + } + } + ], + pages: [ + { + name: 'info', + content: () => createElement(PaymentPolicyInfo) + } + ], + columns: ['_reference', 'name', 'immediatePay', 'createdAt', 'updatedAt'], + filters: ['name', 'immediatePay', 'createdAt', 'updatedAt', '_reference'], + sorters: ['name', 'immediatePay', 'createdAt', '_id', 'updatedAt'], + properties: [ + { + name: '_id', + label: 'ID', + columnFixed: 'left', + type: 'id', + objectType: 'paymentPolicy', + showCopy: true, + columnWidth: 140 + }, + { + name: 'createdAt', + label: 'Created At', + type: 'dateTime', + readOnly: true, + columnWidth: 175 + }, + { + name: '_reference', + label: 'Reference', + type: 'reference', + columnFixed: 'left', + objectType: 'paymentPolicy', + showCopy: true, + readOnly: true, + columnWidth: 180 + }, + { + name: 'updatedAt', + label: 'Updated At', + type: 'dateTime', + readOnly: true, + columnWidth: 175 + }, + { + name: 'name', + label: 'Name', + columnFixed: 'left', + required: true, + type: 'text', + columnWidth: 220 + }, + { + name: 'description', + label: 'Description', + type: 'text', + required: false, + span: 2, + columnWidth: 280 + }, + { + name: 'immediatePay', + label: 'Immediate Pay', + type: 'bool', + required: false, + default: true, + extra: 'Required for eBay managed payments.', + columnWidth: 150 + }, + { + name: 'paymentInstructions', + label: 'Payment Instructions', + type: 'text', + required: false, + span: 2, + columnWidth: 280 + }, + marketplaceSyncMappingsProperty('paymentPolicy') + ] +} diff --git a/src/database/models/ProductCategory.js b/src/database/models/ProductCategory.js index 363a492a..0fde6cfa 100644 --- a/src/database/models/ProductCategory.js +++ b/src/database/models/ProductCategory.js @@ -1,14 +1,17 @@ import { createElement, lazy } from 'react' const ProductCategoryInfo = lazy( - () => import('../../components/Dashboard/Management/ProductCategories/ProductCategoryInfo') + () => + import('../../components/Dashboard/Management/ProductCategories/ProductCategoryInfo') ) const NewProductCategory = lazy( - () => import('../../components/Dashboard/Management/ProductCategories/NewProductCategory') + () => + import('../../components/Dashboard/Management/ProductCategories/NewProductCategory') ) const DeleteObject = lazy( () => import('../../components/Dashboard/common/DeleteObject') ) +import { findTreeNode } from '../../components/Dashboard/common/CustomTreeSelect' import ProductCategoryIcon from '../../components/Icons/ProductCategoryIcon' import PlusIcon from '../../components/Icons/PlusIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' @@ -35,7 +38,11 @@ export const ProductCategory = { label: 'New Product Category', icon: PlusIcon, content: (objectData, { onOk } = {}) => { - return createElement(NewProductCategory, { defaultValues: objectData, onOk, reset: true }) + return createElement(NewProductCategory, { + defaultValues: objectData, + onOk, + reset: true + }) } }, { @@ -106,7 +113,14 @@ export const ProductCategory = { } ], columns: ['_reference', 'name', 'createdAt', 'updatedAt'], - filters: ['_id','name','createdAt','updatedAt','_reference'], + filters: [ + '_id', + 'name', + 'createdAt', + 'updatedAt', + '_reference', + 'marketplaces.marketplace' + ], sorters: ['name', 'createdAt', 'updatedAt', '_id'], properties: [ { @@ -136,6 +150,14 @@ export const ProductCategory = { readOnly: true, columnWidth: 180 }, + + { + name: 'updatedAt', + label: 'Updated At', + type: 'dateTime', + readOnly: true, + columnWidth: 175 + }, { name: 'name', label: 'Name', @@ -145,11 +167,47 @@ export const ProductCategory = { columnWidth: 200 }, { - name: 'updatedAt', - label: 'Updated At', - type: 'dateTime', - readOnly: true, - columnWidth: 175 + name: 'marketplaces', + label: 'Marketplaces', + type: 'objectChildren', + required: false, + canAddRemove: true, + size: 'medium', + span: 2, + columns: ['marketplace', 'externalReference'], + properties: [ + { + name: 'marketplace', + label: 'Marketplace', + type: 'object', + objectType: 'marketplace', + required: true, + showHyperlink: true, + columnWidth: 220 + }, + { + name: 'externalReference', + label: 'External Reference', + type: (objectData) => + objectData?.marketplace?.provider === 'ebay' + ? 'treeSelect' + : 'text', + options: (objectData) => { + const tree = objectData?.marketplace?.eBay?.categoryReferences || [] + const current = objectData?.externalReference + if (!current || findTreeNode(tree, current)) return tree + return [ + { title: current, value: current, selectable: true }, + ...tree + ] + }, + extra: + 'For eBay listings, choose a category from the selected marketplace. Sync the marketplace if the tree is empty.', + showCopy: true, + required: true, + columnWidth: 260 + } + ] } ] } diff --git a/src/database/models/ProductStock.js b/src/database/models/ProductStock.js index 3e2fabf8..3ab91a5f 100644 --- a/src/database/models/ProductStock.js +++ b/src/database/models/ProductStock.js @@ -371,10 +371,11 @@ export const ProductStock = { required: false, showHyperlink: true, columnWidth: 260, - masterFilter: (objectData) => { + masterFilter: (objectData, parentData) => { return { part: objectData?.part?._id, partSku: objectData?.partSku?._id, + stockLocation: parentData?.stockLocation?._id, $or: [{ 'state.type': 'new' }, { 'state.type': 'used' }] } } diff --git a/src/database/models/ReturnPolicy.js b/src/database/models/ReturnPolicy.js new file mode 100644 index 00000000..ac04f437 --- /dev/null +++ b/src/database/models/ReturnPolicy.js @@ -0,0 +1,294 @@ +import { createElement, lazy } from 'react' + +const ReturnPolicyInfo = lazy( + () => + import('../../components/Dashboard/Sales/ReturnPolicies/ReturnPolicyInfo') +) +const NewReturnPolicy = lazy( + () => + import('../../components/Dashboard/Sales/ReturnPolicies/NewReturnPolicy') +) +const DeleteObject = lazy( + () => import('../../components/Dashboard/common/DeleteObject') +) +import ReturnPolicyIcon from '../../components/Icons/ReturnPolicyIcon' +import PlusIcon from '../../components/Icons/PlusIcon' +import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' +import EditIcon from '../../components/Icons/EditIcon' +import CheckIcon from '../../components/Icons/CheckIcon' +import XMarkIcon from '../../components/Icons/XMarkIcon' +import BinIcon from '../../components/Icons/BinIcon' +import ListIcon from '../../components/Icons/ListIcon' +import { marketplaceSyncMappingsProperty } from './marketplaceMappings' + +export const ReturnPolicy = { + name: 'returnPolicy', + label: 'Return Policy', + labelPlural: 'Return Policies', + url: '/dashboard/sales/returnpolicies', + prefix: 'RPL', + endpoint: 'returnpolicies', + icon: ReturnPolicyIcon, + actions: [ + { + name: 'new', + type: 'modal', + pageName: 'list', + modalWidth: 700, + label: 'New Return Policy', + icon: PlusIcon, + content: (objectData, { onOk } = {}) => { + return createElement(NewReturnPolicy, { + defaultValues: objectData, + onOk, + reset: true + }) + } + }, + { + name: 'list', + type: 'page', + pageName: 'list', + label: 'List', + icon: ListIcon + }, + { + name: 'info', + type: 'page', + pageName: 'info', + label: 'Info', + default: true, + row: true, + icon: InfoCircleIcon + }, + { + name: 'edit', + type: 'page', + pageName: 'info', + label: 'Edit', + row: true, + icon: EditIcon, + visible: (objectData) => { + return !(objectData?._isEditing && objectData?._isEditing == true) + } + }, + { + name: 'cancelEdit', + label: 'Cancel Edits', + type: 'page', + pageName: 'info', + icon: XMarkIcon, + visible: (objectData) => { + return objectData?._isEditing && objectData?._isEditing == true + } + }, + { + name: 'finishEdit', + label: 'Save Edits', + type: 'page', + pageName: 'info', + icon: CheckIcon, + visible: (objectData) => { + return objectData?._isEditing && objectData?._isEditing == true + } + }, + { type: 'divider' }, + { + name: 'delete', + type: 'modal', + modalWidth: 520, + modalCentered: true, + label: 'Delete', + icon: BinIcon, + danger: true, + content: (objectData, { onOk } = {}) => { + return createElement(DeleteObject, { objectData, onOk }) + } + } + ], + pages: [ + { + name: 'info', + content: () => createElement(ReturnPolicyInfo) + } + ], + columns: [ + '_reference', + 'name', + 'returnsAccepted', + 'returnPeriodDays', + 'returnShippingCostPayer', + 'createdAt', + 'updatedAt' + ], + filters: [ + 'name', + 'returnsAccepted', + 'returnPeriodDays', + 'returnShippingCostPayer', + 'refundMethod', + 'createdAt', + 'updatedAt', + '_reference' + ], + sorters: [ + 'name', + 'returnsAccepted', + 'returnPeriodDays', + 'createdAt', + '_id', + 'updatedAt' + ], + properties: [ + { + name: '_id', + label: 'ID', + columnFixed: 'left', + type: 'id', + objectType: 'returnPolicy', + showCopy: true, + columnWidth: 140 + }, + { + name: 'createdAt', + label: 'Created At', + type: 'dateTime', + readOnly: true, + columnWidth: 175 + }, + { + name: '_reference', + label: 'Reference', + type: 'reference', + columnFixed: 'left', + objectType: 'returnPolicy', + showCopy: true, + readOnly: true, + columnWidth: 180 + }, + { + name: 'updatedAt', + label: 'Updated At', + type: 'dateTime', + readOnly: true, + columnWidth: 175 + }, + { + name: 'name', + label: 'Name', + columnFixed: 'left', + required: true, + type: 'text', + columnWidth: 220 + }, + { + name: 'description', + label: 'Description', + type: 'text', + required: false, + span: 2, + columnWidth: 280 + }, + { + name: 'returnsAccepted', + label: 'Returns Accepted', + type: 'bool', + required: true, + default: true, + columnWidth: 200 + }, + { + name: 'returnPeriodDays', + label: 'Return Period', + type: 'select', + options: [ + { label: '14 days', value: 14 }, + { label: '30 days', value: 30 }, + { label: '60 days', value: 60 } + ], + default: 30, + required: false, + extra: 'eBay only accepts 14, 30, or 60 days.', + visible: (objectData) => objectData?.returnsAccepted !== false, + columnWidth: 170 + }, + { + name: 'returnShippingCostPayer', + label: 'Return Shipping Paid By', + type: 'select', + options: [ + { label: 'Buyer', value: 'buyer' }, + { label: 'Seller', value: 'seller' } + ], + required: false, + visible: (objectData) => objectData?.returnsAccepted !== false, + columnWidth: 220 + }, + { + name: 'refundMethod', + label: 'Refund Method', + type: 'select', + options: [ + { label: 'Money Back', value: 'moneyBack' }, + { label: 'Merchandise Credit', value: 'merchandiseCredit' } + ], + required: false, + visible: (objectData) => objectData?.returnsAccepted !== false, + columnWidth: 180 + }, + { + name: 'restockingFeePercentage', + label: 'Restocking Fee', + type: 'number', + min: 0, + suffix: '%', + required: false, + extra: 'Stored locally. eBay no longer accepts restocking fees.', + columnWidth: 160 + }, + { + name: 'returnInstructions', + label: 'Return Instructions', + type: 'text', + required: false, + span: 2, + columnWidth: 280 + }, + { + name: 'internationalReturnsAccepted', + label: 'International Returns', + type: 'bool', + required: false, + columnWidth: 180 + }, + { + name: 'internationalReturnPeriodDays', + label: 'International Return Period', + type: 'select', + options: [ + { label: '14 days', value: 14 }, + { label: '30 days', value: 30 }, + { label: '60 days', value: 60 } + ], + required: false, + extra: 'eBay only accepts 14, 30, or 60 days.', + visible: (objectData) => + objectData?.internationalReturnsAccepted === true, + columnWidth: 220 + }, + { + name: 'internationalReturnShippingCostPayer', + label: 'International Return Shipping Paid By', + type: 'select', + options: [ + { label: 'Buyer', value: 'buyer' }, + { label: 'Seller', value: 'seller' } + ], + required: false, + visible: (objectData) => + objectData?.internationalReturnsAccepted === true, + columnWidth: 260 + }, + marketplaceSyncMappingsProperty('returnPolicy') + ] +} diff --git a/src/database/models/TaxRate.js b/src/database/models/TaxRate.js index 814b2095..c0dfc779 100644 --- a/src/database/models/TaxRate.js +++ b/src/database/models/TaxRate.js @@ -17,6 +17,7 @@ import CheckIcon from '../../components/Icons/CheckIcon' import XMarkIcon from '../../components/Icons/XMarkIcon' import BinIcon from '../../components/Icons/BinIcon' import ListIcon from '../../components/Icons/ListIcon' +import { marketplaceSyncMappingsProperty } from './marketplaceMappings' export const TaxRate = { name: 'taxRate', @@ -111,6 +112,7 @@ export const TaxRate = { 'rateType', 'active', 'country', + 'jurisdiction', 'createdAt', 'updatedAt' ], @@ -120,6 +122,7 @@ export const TaxRate = { 'rateType', 'active', 'country', + 'jurisdiction', 'createdAt', 'updatedAt', '_reference' @@ -130,6 +133,7 @@ export const TaxRate = { 'rateType', 'active', 'country', + 'jurisdiction', 'createdAt', '_id', 'updatedAt' @@ -237,6 +241,23 @@ export const TaxRate = { required: false, columnWidth: 150 }, + { + name: 'jurisdiction', + label: 'Jurisdiction', + type: 'text', + readOnly: false, + required: false, + extra: + 'Marketplace tax table region, for example GU or ON. eBay only uses US territories and Canada.', + columnWidth: 140 + }, + { + name: 'shippingAndHandlingTaxed', + label: 'Tax Shipping', + type: 'bool', + required: false, + columnWidth: 140 + }, { name: 'description', label: 'Description', @@ -244,6 +265,10 @@ export const TaxRate = { readOnly: false, required: false, columnWidth: 200 - } + }, + marketplaceSyncMappingsProperty( + 'taxRate', + 'eBay tax table key as COUNTRY:JURISDICTION, for example US:GU. Filled when synced.' + ) ] } diff --git a/src/database/models/marketplaceMappings.js b/src/database/models/marketplaceMappings.js new file mode 100644 index 00000000..32ca9822 --- /dev/null +++ b/src/database/models/marketplaceMappings.js @@ -0,0 +1,49 @@ +export function marketplaceSyncMappingsProperty(objectType, extra) { + return { + name: 'marketplaces', + label: 'Marketplaces', + type: 'objectChildren', + required: false, + canAddRemove: true, + size: 'medium', + span: 2, + columns: ['marketplace', 'state', 'externalReference'], + properties: [ + { + name: 'marketplace', + label: 'Marketplace', + type: 'object', + objectType: 'marketplace', + required: true, + showHyperlink: true, + columnWidth: 220 + }, + { + name: 'state', + label: 'State', + type: 'state', + objectType, + readOnly: true, + columnWidth: 160 + }, + { + name: 'externalReference', + label: 'External Reference', + type: (objectData) => { + return objectData?.marketplace?.provider === 'ebay' + ? 'miscId' + : 'text' + }, + readOnly: (objectData) => { + return objectData?.marketplace?.provider === 'ebay' + }, + extra: + extra || + 'Marketplace policy ID. Filled when the policy is synced; you can paste an existing ID.', + showCopy: true, + required: false, + columnWidth: 240 + } + ] + } +} diff --git a/src/database/sidebars/finance.js b/src/database/sidebars/finance.js index 5e321de3..898e7c24 100644 --- a/src/database/sidebars/finance.js +++ b/src/database/sidebars/finance.js @@ -18,6 +18,12 @@ const financeSidebarItems = [ iconKey: 'payment', path: '/dashboard/finance/payments' }, + { + key: 'paymentpolicies', + label: 'Payment Policies', + iconKey: 'paymentPolicy', + path: '/dashboard/finance/paymentpolicies' + }, { key: 'taxRecords', iconKey: 'taxRecord', diff --git a/src/database/sidebars/sales.js b/src/database/sidebars/sales.js index 9df640a5..ddb14762 100644 --- a/src/database/sidebars/sales.js +++ b/src/database/sidebars/sales.js @@ -18,6 +18,7 @@ const salesSidebarItems = [ iconKey: 'salesOrder', path: '/dashboard/sales/salesorders' }, + { type: 'divider' }, { key: 'marketplaces', label: 'Marketplaces', @@ -29,6 +30,24 @@ const salesSidebarItems = [ label: 'Listings', iconKey: 'listing', path: '/dashboard/sales/listings' + }, + { + key: 'listingvarients', + label: 'Listing Varients', + iconKey: 'listingVarient', + path: '/dashboard/sales/listingvarients' + }, + { + key: 'fulfillmentpolicies', + label: 'Fulfillment Policies', + iconKey: 'fulfillmentPolicy', + path: '/dashboard/sales/fulfillmentpolicies' + }, + { + key: 'returnpolicies', + label: 'Return Policies', + iconKey: 'returnPolicy', + path: '/dashboard/sales/returnpolicies' } ] diff --git a/src/routes/FinanceRoutes.jsx b/src/routes/FinanceRoutes.jsx index 125176a2..4fb54e25 100644 --- a/src/routes/FinanceRoutes.jsx +++ b/src/routes/FinanceRoutes.jsx @@ -23,6 +23,9 @@ const FinanceOverview = lazy( const TaxRecords = lazy( () => import('../components/Dashboard/Finance/TaxRecords.jsx') ) +const PaymentPolicies = lazy( + () => import('../components/Dashboard/Finance/PaymentPolicies.jsx') +) const FinanceRoutes = [ } />, } />, } />, + } + />, import('../components/Dashboard/Sales/Listings.jsx') ) +const ListingVarients = lazy( + () => import('../components/Dashboard/Sales/ListingVarients.jsx') +) +const FulfillmentPolicies = lazy( + () => import('../components/Dashboard/Sales/FulfillmentPolicies.jsx') +) +const ReturnPolicies = lazy( + () => import('../components/Dashboard/Sales/ReturnPolicies.jsx') +) const SalesRoutes = [ } />, @@ -28,7 +37,22 @@ const SalesRoutes = [ path='sales/marketplaces' element={} />, - } /> + } />, + } + />, + } + />, + } + /> ] export default SalesRoutes