diff --git a/assets/icons/listingicon.svg b/assets/icons/listingicon.svg
index 25fc30b..ed412f6 100644
--- a/assets/icons/listingicon.svg
+++ b/assets/icons/listingicon.svg
@@ -1,16 +1,10 @@
diff --git a/assets/icons/listingvarienticon.svg b/assets/icons/listingvarienticon.svg
new file mode 100644
index 0000000..34082ec
--- /dev/null
+++ b/assets/icons/listingvarienticon.svg
@@ -0,0 +1,19 @@
+
+
+
diff --git a/src/App.jsx b/src/App.jsx
index 8a7ca80..fa096b1 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -30,6 +30,7 @@ import { ElectronProvider } from './components/Dashboard/context/ElectronContext
import { MessageProvider } from './components/Dashboard/context/MessageContext.jsx'
import AuthCallback from './components/App/AuthCallback.jsx'
import EmailNotificationTemplate from './components/Email/EmailNotificationTemplate.jsx'
+import MarketplaceAuthCallback from './components/Dashboard/Sales/Marketplaces/MarketplaceAuthCallback.jsx'
import {
ProductionRoutes,
@@ -101,6 +102,10 @@ const AppContent = () => {
path='/auth/callback'
element={}
/>
+ }
+ />
}
diff --git a/src/components/Dashboard/Sales/ListingVarients/ListingVarientInfo.jsx b/src/components/Dashboard/Sales/ListingVarients/ListingVarientInfo.jsx
new file mode 100644
index 0000000..b7a7db4
--- /dev/null
+++ b/src/components/Dashboard/Sales/ListingVarients/ListingVarientInfo.jsx
@@ -0,0 +1,253 @@
+import { useRef, useState } from 'react'
+import { useLocation } from 'react-router-dom'
+import { Space, Flex, Modal } from 'antd'
+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 ViewButton from '../../common/ViewButton'
+import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
+import NoteIcon from '../../../Icons/NoteIcon.jsx'
+import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
+import ObjectForm from '../../common/ObjectForm'
+import EditButtons from '../../common/EditButtons'
+import LockIndicator from '../../common/LockIndicator.jsx'
+import ActionHandler from '../../common/ActionHandler.jsx'
+import ObjectActions from '../../common/ObjectActions.jsx'
+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 ScrollBox from '../../common/ScrollBox.jsx'
+import PublishListingVarient from './PublishListingVarient.jsx'
+import UnpublishListingVarient from './UnpublishListingVarient.jsx'
+import { Card } from 'antd'
+
+const log = loglevel.getLogger('ListingVarientInfo')
+log.setLevel(config.logLevel)
+
+const ListingVarientInfo = () => {
+ const location = useLocation()
+ const objectFormRef = useRef(null)
+ const actionHandlerRef = useRef(null)
+ const listingVarientId = new URLSearchParams(location.search).get(
+ 'listingVarientId'
+ )
+ const [publishOpen, setPublishOpen] = useState(false)
+ const [unpublishOpen, setUnpublishOpen] = useState(false)
+ const [collapseState, updateCollapseState] = useCollapseState(
+ 'ListingVarientInfo',
+ {
+ info: true,
+ notes: true,
+ auditLogs: true
+ }
+ )
+ const [objectFormState, setEditFormState] = useState({
+ isEditing: false,
+ editLoading: false,
+ formValid: false,
+ lock: null,
+ loading: false,
+ objectData: {}
+ })
+
+ const actions = {
+ reload: () => {
+ objectFormRef?.current?.handleFetchObject?.()
+ return true
+ },
+ edit: () => {
+ objectFormRef?.current?.startEditing?.()
+ return false
+ },
+ cancelEdit: () => {
+ objectFormRef?.current?.cancelEditing?.()
+ return true
+ },
+ finishEdit: () => {
+ objectFormRef?.current?.handleUpdate?.()
+ return true
+ },
+ delete: () => {
+ objectFormRef?.current?.handleDelete?.()
+ return true
+ },
+ publish: () => {
+ setPublishOpen(true)
+ return true
+ },
+ unpublish: () => {
+ setUnpublishOpen(true)
+ 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.lock?.locked || objectFormState.loading}
+ loading={objectFormState.editLoading}
+ />
+
+
+
+
+
+ {
+ setEditFormState((prev) => ({ ...prev, ...state }))
+ }}
+ >
+ {({ loading, isEditing, objectData }) => (
+
+ }
+ active={collapseState.info}
+ onToggle={(expanded) =>
+ updateCollapseState('info', expanded)
+ }
+ collapseKey='info'
+ >
+
+
+
+ )}
+
+
+ }
+ active={collapseState.notes}
+ onToggle={(expanded) => updateCollapseState('notes', expanded)}
+ collapseKey='notes'
+ >
+
+
+
+
+ }
+ active={collapseState.auditLogs}
+ onToggle={(expanded) =>
+ updateCollapseState('auditLogs', expanded)
+ }
+ collapseKey='auditLogs'
+ >
+ {objectFormState.loading ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ setPublishOpen(false)}
+ width={515}
+ footer={null}
+ destroyOnHidden={true}
+ centered={true}
+ >
+ {
+ setPublishOpen(false)
+ actions.reload()
+ }}
+ />
+
+ setUnpublishOpen(false)}
+ width={515}
+ footer={null}
+ destroyOnHidden={true}
+ centered={true}
+ >
+ {
+ setUnpublishOpen(false)
+ actions.reload()
+ }}
+ />
+
+ >
+ )
+}
+
+export default ListingVarientInfo
diff --git a/src/components/Dashboard/Sales/ListingVarients/NewListingVarient.jsx b/src/components/Dashboard/Sales/ListingVarients/NewListingVarient.jsx
new file mode 100644
index 0000000..87abcb6
--- /dev/null
+++ b/src/components/Dashboard/Sales/ListingVarients/NewListingVarient.jsx
@@ -0,0 +1,87 @@
+import PropTypes from 'prop-types'
+import ObjectInfo from '../../common/ObjectInfo'
+import NewObjectForm from '../../common/NewObjectForm'
+import WizardView from '../../common/WizardView'
+
+const NewListingVarient = ({ 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()
+ }
+ }}
+ />
+ )
+ }}
+
+ )
+}
+
+NewListingVarient.propTypes = {
+ onOk: PropTypes.func.isRequired,
+ reset: PropTypes.bool,
+ defaultValues: PropTypes.object
+}
+
+export default NewListingVarient
diff --git a/src/components/Dashboard/Sales/ListingVarients/PublishListingVarient.jsx b/src/components/Dashboard/Sales/ListingVarients/PublishListingVarient.jsx
new file mode 100644
index 0000000..e6c0910
--- /dev/null
+++ b/src/components/Dashboard/Sales/ListingVarients/PublishListingVarient.jsx
@@ -0,0 +1,54 @@
+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 PublishListingVarient = ({ onOk, objectData }) => {
+ const [loading, setLoading] = useState(false)
+ const { sendObjectFunction } = useContext(ApiServerContext)
+
+ const handlePublish = async () => {
+ setLoading(true)
+ try {
+ const result = await sendObjectFunction(
+ objectData._id,
+ 'ListingVarient',
+ 'publish'
+ )
+ if (result) {
+ message.success('Published successfully')
+ onOk(result)
+ }
+ } catch (error) {
+ console.error('Error publishing listing variant:', error)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const ref =
+ objectData?.title ||
+ objectData?._reference ||
+ objectData?.name ||
+ objectData?._id
+
+ return (
+
+ )
+}
+
+PublishListingVarient.propTypes = {
+ onOk: PropTypes.func.isRequired,
+ objectData: PropTypes.object
+}
+
+export default PublishListingVarient
diff --git a/src/components/Dashboard/Sales/ListingVarients/UnpublishListingVarient.jsx b/src/components/Dashboard/Sales/ListingVarients/UnpublishListingVarient.jsx
new file mode 100644
index 0000000..ae39061
--- /dev/null
+++ b/src/components/Dashboard/Sales/ListingVarients/UnpublishListingVarient.jsx
@@ -0,0 +1,54 @@
+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 UnpublishListingVarient = ({ onOk, objectData }) => {
+ const [loading, setLoading] = useState(false)
+ const { sendObjectFunction } = useContext(ApiServerContext)
+
+ const handleUnpublish = async () => {
+ setLoading(true)
+ try {
+ const result = await sendObjectFunction(
+ objectData._id,
+ 'ListingVarient',
+ 'unpublish'
+ )
+ if (result) {
+ message.success('Unpublished successfully')
+ onOk(result)
+ }
+ } catch (error) {
+ console.error('Error unpublishing listing variant:', error)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const ref =
+ objectData?.title ||
+ objectData?._reference ||
+ objectData?.name ||
+ objectData?._id
+
+ return (
+
+ )
+}
+
+UnpublishListingVarient.propTypes = {
+ onOk: PropTypes.func.isRequired,
+ objectData: PropTypes.object
+}
+
+export default UnpublishListingVarient
diff --git a/src/components/Dashboard/Sales/Listings.jsx b/src/components/Dashboard/Sales/Listings.jsx
new file mode 100644
index 0000000..fb7b4bd
--- /dev/null
+++ b/src/components/Dashboard/Sales/Listings.jsx
@@ -0,0 +1,107 @@
+import { useState, useRef } from 'react'
+import { Button, Flex, Space, Modal, Dropdown } from 'antd'
+import NewListing from './Listings/NewListing'
+import ObjectTable from '../common/ObjectTable'
+import PlusIcon from '../../Icons/PlusIcon'
+import ReloadIcon from '../../Icons/ReloadIcon'
+import useColumnVisibility from '../hooks/useColumnVisibility'
+import ObjectTableViewButton from '../common/ObjectTableViewButton'
+import FilterSidebarButton from '../common/FilterSidebarButton'
+import useViewMode from '../hooks/useViewMode'
+import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility'
+import ColumnViewButton from '../common/ColumnViewButton'
+import ExportListButton from '../common/ExportListButton'
+
+const Listings = () => {
+ const [newListingOpen, setNewListingOpen] = useState(false)
+ const tableRef = useRef()
+
+ const [viewMode, setViewMode] = useViewMode('listings')
+
+ const [columnVisibility, setColumnVisibility] =
+ useColumnVisibility('listings')
+
+ const [showFilterSidebar, setShowFilterSidebar] =
+ useFilterSidebarVisibility('Listings')
+
+ const actionItems = {
+ items: [
+ {
+ label: 'New Listing',
+ key: 'newListing',
+ icon:
+ },
+ { type: 'divider' },
+ {
+ label: 'Reload List',
+ key: 'reloadList',
+ icon:
+ }
+ ],
+ onClick: ({ key }) => {
+ if (key === 'reloadList') {
+ tableRef.current?.reload()
+ } else if (key === 'newListing') {
+ setNewListingOpen(true)
+ }
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ setShowFilterSidebar(!showFilterSidebar)}
+ />
+
+
+
+
+
+ {
+ setNewListingOpen(false)
+ }}
+ destroyOnHidden={true}
+ >
+ {
+ setNewListingOpen(false)
+ tableRef.current?.reload()
+ }}
+ reset={newListingOpen}
+ />
+
+ >
+ )
+}
+
+export default Listings
diff --git a/src/components/Dashboard/Sales/Listings/ListingInfo.jsx b/src/components/Dashboard/Sales/Listings/ListingInfo.jsx
new file mode 100644
index 0000000..a5b1609
--- /dev/null
+++ b/src/components/Dashboard/Sales/Listings/ListingInfo.jsx
@@ -0,0 +1,298 @@
+import { useRef, useState } from 'react'
+import { useLocation } from 'react-router-dom'
+import { Space, Flex, Modal } from 'antd'
+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 ViewButton from '../../common/ViewButton'
+import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
+import NoteIcon from '../../../Icons/NoteIcon.jsx'
+import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
+import ListingVarientIcon from '../../../Icons/ListingVarientIcon.jsx'
+import ObjectForm from '../../common/ObjectForm'
+import EditButtons from '../../common/EditButtons'
+import LockIndicator from '../../common/LockIndicator.jsx'
+import ActionHandler from '../../common/ActionHandler.jsx'
+import ObjectActions from '../../common/ObjectActions.jsx'
+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 ScrollBox from '../../common/ScrollBox.jsx'
+import NewListingVarient from '../ListingVarients/NewListingVarient.jsx'
+import PublishListing from './PublishListing.jsx'
+import UnpublishListing from './UnpublishListing.jsx'
+import { Card } from 'antd'
+
+const log = loglevel.getLogger('ListingInfo')
+log.setLevel(config.logLevel)
+
+const ListingInfo = () => {
+ const location = useLocation()
+ const objectFormRef = useRef(null)
+ const listingVarientsTableRef = useRef(null)
+ const actionHandlerRef = useRef(null)
+ const listingId = new URLSearchParams(location.search).get('listingId')
+ const [newListingVarientOpen, setNewListingVarientOpen] = useState(false)
+ const [publishListingOpen, setPublishListingOpen] = useState(false)
+ const [unpublishListingOpen, setUnpublishListingOpen] = useState(false)
+ const [collapseState, updateCollapseState] = useCollapseState('ListingInfo', {
+ info: true,
+ listingVarients: true,
+ notes: true,
+ auditLogs: true
+ })
+ const [objectFormState, setEditFormState] = useState({
+ isEditing: false,
+ editLoading: false,
+ formValid: false,
+ lock: null,
+ loading: false,
+ objectData: {}
+ })
+
+ const actions = {
+ reload: () => {
+ objectFormRef?.current?.handleFetchObject?.()
+ return true
+ },
+ edit: () => {
+ objectFormRef?.current?.startEditing?.()
+ return false
+ },
+ cancelEdit: () => {
+ objectFormRef?.current?.cancelEditing?.()
+ return true
+ },
+ finishEdit: () => {
+ objectFormRef?.current?.handleUpdate?.()
+ return true
+ },
+ delete: () => {
+ objectFormRef?.current?.handleDelete?.()
+ return true
+ },
+ newListingVarient: () => {
+ setNewListingVarientOpen(true)
+ return true
+ },
+ publish: () => {
+ setPublishListingOpen(true)
+ return true
+ },
+ unpublish: () => {
+ setUnpublishListingOpen(true)
+ 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.lock?.locked || objectFormState.loading}
+ loading={objectFormState.editLoading}
+ />
+
+
+
+
+
+ {
+ setEditFormState((prev) => ({ ...prev, ...state }))
+ }}
+ >
+ {({ loading, isEditing, objectData }) => (
+
+ }
+ active={collapseState.info}
+ onToggle={(expanded) =>
+ updateCollapseState('info', expanded)
+ }
+ collapseKey='info'
+ >
+
+
+ }
+ active={collapseState.listingVarients}
+ onToggle={(expanded) =>
+ updateCollapseState('listingVarients', expanded)
+ }
+ collapseKey='listingVarients'
+ >
+
+
+
+ )}
+
+
+ }
+ active={collapseState.notes}
+ onToggle={(expanded) => updateCollapseState('notes', expanded)}
+ collapseKey='notes'
+ >
+
+
+
+
+ }
+ active={collapseState.auditLogs}
+ onToggle={(expanded) =>
+ updateCollapseState('auditLogs', expanded)
+ }
+ collapseKey='auditLogs'
+ >
+ {objectFormState.loading ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {
+ setNewListingVarientOpen(false)
+ }}
+ width={800}
+ footer={null}
+ destroyOnHidden={true}
+ >
+ {
+ setNewListingVarientOpen(false)
+ listingVarientsTableRef.current?.reload()
+ }}
+ reset={newListingVarientOpen}
+ defaultValues={{
+ listing: { _id: listingId }
+ }}
+ />
+
+ setPublishListingOpen(false)}
+ width={515}
+ footer={null}
+ destroyOnHidden={true}
+ centered={true}
+ >
+ {
+ setPublishListingOpen(false)
+ actions.reload()
+ listingVarientsTableRef.current?.reload?.()
+ }}
+ />
+
+ setUnpublishListingOpen(false)}
+ width={515}
+ footer={null}
+ destroyOnHidden={true}
+ centered={true}
+ >
+ {
+ setUnpublishListingOpen(false)
+ actions.reload()
+ listingVarientsTableRef.current?.reload?.()
+ }}
+ />
+
+ >
+ )
+}
+
+export default ListingInfo
diff --git a/src/components/Dashboard/Sales/Listings/NewListing.jsx b/src/components/Dashboard/Sales/Listings/NewListing.jsx
new file mode 100644
index 0000000..6ee5f8d
--- /dev/null
+++ b/src/components/Dashboard/Sales/Listings/NewListing.jsx
@@ -0,0 +1,87 @@
+import PropTypes from 'prop-types'
+import ObjectInfo from '../../common/ObjectInfo'
+import NewObjectForm from '../../common/NewObjectForm'
+import WizardView from '../../common/WizardView'
+
+const NewListing = ({ 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()
+ }
+ }}
+ />
+ )
+ }}
+
+ )
+}
+
+NewListing.propTypes = {
+ onOk: PropTypes.func.isRequired,
+ reset: PropTypes.bool,
+ defaultValues: PropTypes.object
+}
+
+export default NewListing
diff --git a/src/components/Dashboard/Sales/Listings/PublishListing.jsx b/src/components/Dashboard/Sales/Listings/PublishListing.jsx
new file mode 100644
index 0000000..6e35655
--- /dev/null
+++ b/src/components/Dashboard/Sales/Listings/PublishListing.jsx
@@ -0,0 +1,50 @@
+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 PublishListing = ({ onOk, objectData }) => {
+ const [loading, setLoading] = useState(false)
+ const { sendObjectFunction } = useContext(ApiServerContext)
+
+ const handlePublish = async () => {
+ setLoading(true)
+ try {
+ const result = await sendObjectFunction(objectData._id, 'Listing', 'publish')
+ if (result) {
+ message.success('Published successfully')
+ onOk(result)
+ }
+ } catch (error) {
+ console.error('Error publishing listing:', error)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const ref =
+ objectData?.title ||
+ objectData?._reference ||
+ objectData?.name ||
+ objectData?._id
+
+ return (
+
+ )
+}
+
+PublishListing.propTypes = {
+ onOk: PropTypes.func.isRequired,
+ objectData: PropTypes.object
+}
+
+export default PublishListing
diff --git a/src/components/Dashboard/Sales/Listings/UnpublishListing.jsx b/src/components/Dashboard/Sales/Listings/UnpublishListing.jsx
new file mode 100644
index 0000000..732be52
--- /dev/null
+++ b/src/components/Dashboard/Sales/Listings/UnpublishListing.jsx
@@ -0,0 +1,50 @@
+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 UnpublishListing = ({ onOk, objectData }) => {
+ const [loading, setLoading] = useState(false)
+ const { sendObjectFunction } = useContext(ApiServerContext)
+
+ const handleUnpublish = async () => {
+ setLoading(true)
+ try {
+ const result = await sendObjectFunction(objectData._id, 'Listing', 'unpublish')
+ if (result) {
+ message.success('Unpublished successfully')
+ onOk(result)
+ }
+ } catch (error) {
+ console.error('Error unpublishing listing:', error)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const ref =
+ objectData?.title ||
+ objectData?._reference ||
+ objectData?.name ||
+ objectData?._id
+
+ return (
+
+ )
+}
+
+UnpublishListing.propTypes = {
+ onOk: PropTypes.func.isRequired,
+ objectData: PropTypes.object
+}
+
+export default UnpublishListing
diff --git a/src/components/Dashboard/Sales/Marketplaces/ConfigureMarketplace.jsx b/src/components/Dashboard/Sales/Marketplaces/ConfigureMarketplace.jsx
new file mode 100644
index 0000000..e7b13c2
--- /dev/null
+++ b/src/components/Dashboard/Sales/Marketplaces/ConfigureMarketplace.jsx
@@ -0,0 +1,68 @@
+import PropTypes from 'prop-types'
+import { Result, Typography, Flex, Button } from 'antd'
+import CopyButton from '../../common/CopyButton'
+import MarketplaceIcon from '../../../Icons/MarketplaceIcon'
+import { getMarketplaceCallbackUrl } from './authUtils'
+
+const { Text } = Typography
+
+const ConfigureMarketplace = ({
+ onConnect,
+ isConnected = false,
+ loading = false,
+ disabled = false
+}) => {
+ const callbackUrl = getMarketplaceCallbackUrl()
+
+ return (
+
+
+ Use this callback URL for TikTok Shop redirects and as the eBay
+ RuName accept URL target. Click Connect to authorize your
+ marketplace account.
+
+ }
+ icon={}
+ >
+
+
+
+
+
+ {callbackUrl}
+
+
+
+
+
+
+
+
+
+ )
+}
+
+ConfigureMarketplace.propTypes = {
+ onConnect: PropTypes.func,
+ isConnected: PropTypes.bool,
+ loading: PropTypes.bool,
+ disabled: PropTypes.bool
+}
+
+export default ConfigureMarketplace
diff --git a/src/components/Dashboard/Sales/Marketplaces/MarketplaceAuthCallback.jsx b/src/components/Dashboard/Sales/Marketplaces/MarketplaceAuthCallback.jsx
new file mode 100644
index 0000000..3b38115
--- /dev/null
+++ b/src/components/Dashboard/Sales/Marketplaces/MarketplaceAuthCallback.jsx
@@ -0,0 +1,186 @@
+import { useContext, useEffect, useMemo, useState } from 'react'
+import { Alert, Button, Card, Flex, message } from 'antd'
+import CheckIcon from '../../../Icons/CheckIcon'
+import { useLocation, useNavigate } from 'react-router-dom'
+import { LoadingOutlined } from '@ant-design/icons'
+import { ApiServerContext } from '../../context/ApiServerContext'
+import {
+ clearMarketplaceAuthState,
+ parseMarketplaceAuthState,
+ readMarketplaceAuthState
+} from './authUtils'
+import { AuthContext } from '../../context/AuthContext'
+import AuthParticles from '../../../App/AppParticles'
+import FarmControlLogo from '../../../Logos/FarmControlLogo'
+import ExclamationOctagonIcon from '../../../Icons/ExclamationOctagonIcon'
+import ArrowLeftIcon from '../../../Icons/ArrowLeftIcon'
+
+const MarketplaceAuthCallback = () => {
+ const location = useLocation()
+ const navigate = useNavigate()
+ const { token } = useContext(AuthContext)
+ const { sendObjectFunction, connected } = useContext(ApiServerContext)
+ const [status, setStatus] = useState('loading')
+ const [redirectLoading, setRedirectLoading] = useState(false)
+ const [resultMessage, setResultMessage] = useState('')
+
+ const params = useMemo(
+ () => new URLSearchParams(location.search),
+ [location.search]
+ )
+ const rawState = params.get('state') || ''
+ const parsedState = parseMarketplaceAuthState(rawState)
+ const storedState = readMarketplaceAuthState(rawState)
+ const marketplaceId =
+ params.get('marketplaceId') ||
+ parsedState?.marketplaceId ||
+ storedState?.marketplaceId ||
+ ''
+ const returnTo =
+ parsedState?.returnTo ||
+ storedState?.returnTo ||
+ (marketplaceId
+ ? `/dashboard/sales/marketplaces/info?marketplaceId=${marketplaceId}`
+ : '/dashboard/sales/marketplaces')
+
+ useEffect(() => {
+ if (!connected || token == null || !token) {
+ return
+ }
+
+ const error = params.get('error') || params.get('error_description')
+ const code = params.get('code') || params.get('auth_code')
+
+ if (error) {
+ setStatus('error')
+ setResultMessage(error)
+ if (rawState) {
+ clearMarketplaceAuthState(rawState)
+ }
+ return
+ }
+
+ if (!marketplaceId) {
+ setStatus('error')
+ setResultMessage('Could not determine which marketplace to connect.')
+ return
+ }
+
+ if (!code) {
+ setStatus('error')
+ setResultMessage('Authorization code was not returned by the provider.')
+ return
+ }
+
+ let cancelled = false
+
+ const exchangeCode = async () => {
+ try {
+ const result = await sendObjectFunction(
+ marketplaceId,
+ 'Marketplace',
+ 'auth/exchange',
+ {
+ code,
+ state: rawState
+ }
+ )
+ if (!result?.success) {
+ throw new Error(
+ result?.error || 'Failed to complete marketplace authorization.'
+ )
+ }
+
+ if (cancelled) {
+ return
+ }
+
+ setStatus('success')
+ setResultMessage('Marketplace authorization completed successfully.')
+ message.success('Marketplace connected')
+ if (rawState) {
+ clearMarketplaceAuthState(rawState)
+ }
+ } catch (error) {
+ if (cancelled) {
+ return
+ }
+
+ console.error('Marketplace auth callback failed:', error)
+ setStatus('error')
+ setResultMessage(
+ error?.response?.data?.error ||
+ error?.message ||
+ 'Failed to complete marketplace authorization.'
+ )
+ }
+ }
+
+ exchangeCode()
+
+ return () => {
+ cancelled = true
+ }
+ }, [marketplaceId, params, rawState, sendObjectFunction, token, connected])
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {status != 'error' && status != 'success' && (
+ }
+ type='info'
+ showIcon
+ />
+ )}
+
+ {status === 'success' && (
+ }
+ type='success'
+ showIcon
+ />
+ )}
+
+ {status === 'error' && (
+ }
+ type='error'
+ showIcon
+ />
+ )}
+
+
+ }
+ onClick={() => {
+ setRedirectLoading(true)
+ navigate(returnTo)
+ }}
+ loading={redirectLoading}
+ disabled={status === 'loading' || redirectLoading}
+ size='large'
+ >
+
+
+ >
+ )
+}
+
+export default MarketplaceAuthCallback
diff --git a/src/components/Dashboard/Sales/Marketplaces/MarketplaceInfo.jsx b/src/components/Dashboard/Sales/Marketplaces/MarketplaceInfo.jsx
index 9bd461f..3d53b46 100644
--- a/src/components/Dashboard/Sales/Marketplaces/MarketplaceInfo.jsx
+++ b/src/components/Dashboard/Sales/Marketplaces/MarketplaceInfo.jsx
@@ -1,6 +1,6 @@
-import { useRef, useState } from 'react'
+import { useContext, useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
-import { Space, Flex } from 'antd'
+import { Space, Flex, Modal, message } from 'antd'
import loglevel from 'loglevel'
import config from '../../../../config'
import useCollapseState from '../../hooks/useCollapseState'
@@ -22,6 +22,15 @@ import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
import { Card } from 'antd'
+import SyncListings from './SyncListings.jsx'
+import SyncOrders from './SyncOrders.jsx'
+import ConfigureMarketplace from './ConfigureMarketplace.jsx'
+import { ApiServerContext } from '../../context/ApiServerContext.jsx'
+import { ElectronContext } from '../../context/ElectronContext.jsx'
+import {
+ buildMarketplaceAuthState,
+ storeMarketplaceAuthState
+} from './authUtils.js'
const log = loglevel.getLogger('MarketplaceInfo')
log.setLevel(config.logLevel)
@@ -30,12 +39,20 @@ const MarketplaceInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
- const marketplaceId = new URLSearchParams(location.search).get('marketplaceId')
- const [collapseState, updateCollapseState] = useCollapseState('MarketplaceInfo', {
- info: true,
- notes: true,
- auditLogs: true
- })
+ const { getMarketplaceAuthUrl, refreshMarketplaceAuth } =
+ useContext(ApiServerContext)
+ const { openExternalUrl } = useContext(ElectronContext)
+ const marketplaceId = new URLSearchParams(location.search).get(
+ 'marketplaceId'
+ )
+ const [collapseState, updateCollapseState] = useCollapseState(
+ 'MarketplaceInfo',
+ {
+ info: true,
+ notes: true,
+ auditLogs: true
+ }
+ )
const [objectFormState, setEditFormState] = useState({
isEditing: false,
editLoading: false,
@@ -44,6 +61,55 @@ const MarketplaceInfo = () => {
loading: false,
objectData: {}
})
+ const [syncListingsOpen, setSyncListingsOpen] = useState(false)
+ const [syncOrdersOpen, setSyncOrdersOpen] = useState(false)
+ const [configureModalOpen, setConfigureModalOpen] = useState(false)
+
+ const startAuthorization = async () => {
+ const objectData = objectFormState.objectData
+ if (!objectData?._id) return
+
+ if (objectFormState.isEditing) {
+ message.warning('Save marketplace changes before starting authorization')
+ return
+ }
+
+ const returnTo = `/dashboard/sales/marketplaces/info?marketplaceId=${objectData._id}`
+ const state = buildMarketplaceAuthState({
+ marketplaceId: objectData._id,
+ returnTo
+ })
+
+ storeMarketplaceAuthState(state, {
+ marketplaceId: objectData._id,
+ returnTo
+ })
+
+ const result = await getMarketplaceAuthUrl(objectData._id, state)
+
+ if (!result?.url) {
+ message.error('Authorization URL was not returned')
+ return
+ }
+
+ const openedExternally = openExternalUrl(result.url)
+ if (!openedExternally) {
+ window.location.assign(result.url)
+ }
+ }
+
+ const handleRefreshToken = async () => {
+ const objectData = objectFormState.objectData
+ if (!objectData?._id) return
+
+ const result = await refreshMarketplaceAuth(objectData._id)
+ if (!result?.success) {
+ message.error(result?.error || 'Token refresh failed')
+ return
+ }
+ message.success('Marketplace token refreshed')
+ objectFormRef?.current?.handleFetchObject?.()
+ }
const actions = {
reload: () => {
@@ -65,6 +131,30 @@ const MarketplaceInfo = () => {
delete: () => {
objectFormRef?.current?.handleDelete?.()
return true
+ },
+ syncListings: () => {
+ setSyncListingsOpen(true)
+ return true
+ },
+ syncOrders: () => {
+ setSyncOrdersOpen(true)
+ return true
+ },
+ configure: () => {
+ setConfigureModalOpen(true)
+ return true
+ },
+ connect: () => {
+ startAuthorization()
+ return true
+ },
+ reconnect: () => {
+ startAuthorization()
+ return true
+ },
+ refreshToken: () => {
+ handleRefreshToken()
+ return true
}
}
@@ -154,6 +244,7 @@ const MarketplaceInfo = () => {
loading={loading}
isEditing={isEditing}
type='marketplace'
+ labelWidth={215}
objectData={objectData}
/>
)}
@@ -193,6 +284,62 @@ const MarketplaceInfo = () => {
+ setSyncListingsOpen(false)}
+ width={500}
+ footer={null}
+ destroyOnClose
+ centered
+ >
+ {
+ setSyncListingsOpen(false)
+ actions.reload()
+ }}
+ objectData={objectFormState.objectData}
+ />
+
+ setSyncOrdersOpen(false)}
+ width={500}
+ footer={null}
+ destroyOnClose
+ centered
+ >
+ {
+ setSyncOrdersOpen(false)
+ actions.reload()
+ }}
+ objectData={objectFormState.objectData}
+ />
+
+ setConfigureModalOpen(false)}
+ width={650}
+ footer={null}
+ destroyOnClose
+ centered
+ >
+ {
+ startAuthorization()
+ setConfigureModalOpen(false)
+ }}
+ isConnected={
+ !!(
+ objectFormState.objectData?.config?.refreshToken ||
+ objectFormState.objectData?.config?.accessToken ||
+ objectFormState.objectData?.config?.shopCipher
+ )
+ }
+ loading={objectFormState.loading}
+ disabled={objectFormState.isEditing}
+ />
+
>
)
}
diff --git a/src/components/Dashboard/Sales/Marketplaces/NewMarketplace.jsx b/src/components/Dashboard/Sales/Marketplaces/NewMarketplace.jsx
index 0221a1b..d69692d 100644
--- a/src/components/Dashboard/Sales/Marketplaces/NewMarketplace.jsx
+++ b/src/components/Dashboard/Sales/Marketplaces/NewMarketplace.jsx
@@ -2,12 +2,21 @@ import PropTypes from 'prop-types'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
+import { getMarketplaceCallbackUrl } from './authUtils'
const NewMarketplace = ({ onOk, defaultValues }) => {
+ const callbackUrl = getMarketplaceCallbackUrl()
+
return (
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
@@ -60,14 +69,23 @@ const NewMarketplace = ({ onOk, defaultValues }) => {
_id: false,
createdAt: false,
updatedAt: false,
- 'config.appId': false,
- 'config.certId': false,
- 'config.devId': false,
- 'config.userToken': false,
+ authConnected: false,
+ 'config.clientId': false,
+ 'config.clientSecret': false,
+ 'config.ruName': false,
+ 'config.marketplaceId': false,
+ 'config.sandbox': false,
+ 'config.verificationToken': false,
+ 'config.redirectUri': false,
'config.accessToken': false,
'config.refreshToken': false,
+ 'config.refreshTokenExpiresAt': false,
+ 'config.accessTokenExpiresAt': false,
+ 'config.lastTokenRefreshAt': false,
+ connectedAt: false,
'config.appKey': false,
- 'config.appSecret': false
+ 'config.appSecret': false,
+ 'config.shopCipher': false
}}
isEditing={false}
objectData={objectData}
diff --git a/src/components/Dashboard/Sales/Marketplaces/SyncListings.jsx b/src/components/Dashboard/Sales/Marketplaces/SyncListings.jsx
new file mode 100644
index 0000000..1eace47
--- /dev/null
+++ b/src/components/Dashboard/Sales/Marketplaces/SyncListings.jsx
@@ -0,0 +1,46 @@
+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 SyncListings = ({ onOk, objectData }) => {
+ const [syncLoading, setSyncLoading] = useState(false)
+ const { sendObjectFunction } = useContext(ApiServerContext)
+
+ const handleSync = async () => {
+ setSyncLoading(true)
+ try {
+ const result = await sendObjectFunction(
+ objectData._id,
+ 'Marketplace',
+ 'sync/items'
+ )
+ if (result) {
+ message.success('Listings synced successfully')
+ onOk(result)
+ }
+ } catch (error) {
+ console.error('Error syncing listings:', error)
+ } finally {
+ setSyncLoading(false)
+ }
+ }
+
+ return (
+
+ )
+}
+
+SyncListings.propTypes = {
+ onOk: PropTypes.func.isRequired,
+ objectData: PropTypes.object
+}
+
+export default SyncListings
diff --git a/src/components/Dashboard/Sales/Marketplaces/SyncOrders.jsx b/src/components/Dashboard/Sales/Marketplaces/SyncOrders.jsx
new file mode 100644
index 0000000..e9f6fe1
--- /dev/null
+++ b/src/components/Dashboard/Sales/Marketplaces/SyncOrders.jsx
@@ -0,0 +1,46 @@
+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 SyncOrders = ({ onOk, objectData }) => {
+ const [syncLoading, setSyncLoading] = useState(false)
+ const { sendObjectFunction } = useContext(ApiServerContext)
+
+ const handleSync = async () => {
+ setSyncLoading(true)
+ try {
+ const result = await sendObjectFunction(
+ objectData._id,
+ 'Marketplace',
+ 'sync/orders'
+ )
+ if (result) {
+ message.success('Orders synced successfully')
+ onOk(result)
+ }
+ } catch (error) {
+ console.error('Error syncing orders:', error)
+ } finally {
+ setSyncLoading(false)
+ }
+ }
+
+ return (
+
+ )
+}
+
+SyncOrders.propTypes = {
+ onOk: PropTypes.func.isRequired,
+ objectData: PropTypes.object
+}
+
+export default SyncOrders
diff --git a/src/components/Dashboard/Sales/Marketplaces/authUtils.js b/src/components/Dashboard/Sales/Marketplaces/authUtils.js
new file mode 100644
index 0000000..ef321a8
--- /dev/null
+++ b/src/components/Dashboard/Sales/Marketplaces/authUtils.js
@@ -0,0 +1,68 @@
+export const MARKETPLACE_AUTH_STATE_KEY = 'marketplace-auth-state'
+
+export function buildMarketplaceAuthState({ marketplaceId, returnTo } = {}) {
+ return btoa(
+ JSON.stringify({
+ marketplaceId,
+ returnTo
+ })
+ )
+}
+
+export function parseMarketplaceAuthState(rawState) {
+ if (!rawState) {
+ return null
+ }
+
+ try {
+ return JSON.parse(atob(rawState))
+ } catch {
+ return null
+ }
+}
+
+export function getMarketplaceCallbackUrl() {
+ if (typeof window === 'undefined') {
+ return ''
+ }
+
+ return `${window.location.origin}/auth/marketplace/callback`
+}
+
+export function storeMarketplaceAuthState(rawState, data) {
+ if (typeof window === 'undefined' || !rawState) {
+ return
+ }
+
+ window.sessionStorage.setItem(
+ `${MARKETPLACE_AUTH_STATE_KEY}:${rawState}`,
+ JSON.stringify(data)
+ )
+}
+
+export function readMarketplaceAuthState(rawState) {
+ if (typeof window === 'undefined' || !rawState) {
+ return null
+ }
+
+ const stored = window.sessionStorage.getItem(
+ `${MARKETPLACE_AUTH_STATE_KEY}:${rawState}`
+ )
+ if (!stored) {
+ return null
+ }
+
+ try {
+ return JSON.parse(stored)
+ } catch {
+ return null
+ }
+}
+
+export function clearMarketplaceAuthState(rawState) {
+ if (typeof window === 'undefined' || !rawState) {
+ return
+ }
+
+ window.sessionStorage.removeItem(`${MARKETPLACE_AUTH_STATE_KEY}:${rawState}`)
+}
diff --git a/src/components/Dashboard/Sales/SalesOrders/CancelSalesOrder.jsx b/src/components/Dashboard/Sales/SalesOrders/CancelSalesOrder.jsx
index f6adb7b..4ff617f 100644
--- a/src/components/Dashboard/Sales/SalesOrders/CancelSalesOrder.jsx
+++ b/src/components/Dashboard/Sales/SalesOrders/CancelSalesOrder.jsx
@@ -44,4 +44,3 @@ CancelSalesOrder.propTypes = {
}
export default CancelSalesOrder
-
diff --git a/src/components/Dashboard/Sales/SalesOrders/ConfirmSalesOrder.jsx b/src/components/Dashboard/Sales/SalesOrders/ConfirmSalesOrder.jsx
index bedb1be..5b430b0 100644
--- a/src/components/Dashboard/Sales/SalesOrders/ConfirmSalesOrder.jsx
+++ b/src/components/Dashboard/Sales/SalesOrders/ConfirmSalesOrder.jsx
@@ -44,4 +44,3 @@ ConfirmSalesOrder.propTypes = {
}
export default ConfirmSalesOrder
-
diff --git a/src/components/Dashboard/Sales/SalesOrders/PostSalesOrder.jsx b/src/components/Dashboard/Sales/SalesOrders/PostSalesOrder.jsx
index d906ddb..00ef6a2 100644
--- a/src/components/Dashboard/Sales/SalesOrders/PostSalesOrder.jsx
+++ b/src/components/Dashboard/Sales/SalesOrders/PostSalesOrder.jsx
@@ -44,4 +44,3 @@ PostSalesOrder.propTypes = {
}
export default PostSalesOrder
-
diff --git a/src/components/Dashboard/Sales/SalesSidebar.jsx b/src/components/Dashboard/Sales/SalesSidebar.jsx
index da7dd8d..b0ef781 100644
--- a/src/components/Dashboard/Sales/SalesSidebar.jsx
+++ b/src/components/Dashboard/Sales/SalesSidebar.jsx
@@ -4,6 +4,7 @@ import ClientIcon from '../../Icons/ClientIcon'
import SalesIcon from '../../Icons/SalesIcon'
import SalesOrderIcon from '../../Icons/SalesOrderIcon'
import MarketplaceIcon from '../../Icons/MarketplaceIcon'
+import ListingIcon from '../../Icons/ListingIcon'
const items = [
{
@@ -30,6 +31,12 @@ const items = [
label: 'Marketplaces',
icon: ,
path: '/dashboard/sales/marketplaces'
+ },
+ {
+ key: 'listings',
+ label: 'Listings',
+ icon: ,
+ path: '/dashboard/sales/listings'
}
]
@@ -37,7 +44,9 @@ const routeKeyMap = {
'/dashboard/sales/overview': 'overview',
'/dashboard/sales/clients': 'clients',
'/dashboard/sales/salesorders': 'salesorders',
- '/dashboard/sales/marketplaces': 'marketplaces'
+ '/dashboard/sales/marketplaces': 'marketplaces',
+ '/dashboard/sales/listings': 'listings',
+ '/dashboard/sales/listingvarients': 'listings'
}
const SalesSidebar = (props) => {
diff --git a/src/components/Dashboard/common/ObjectProperty.jsx b/src/components/Dashboard/common/ObjectProperty.jsx
index 041a197..89a2951 100644
--- a/src/components/Dashboard/common/ObjectProperty.jsx
+++ b/src/components/Dashboard/common/ObjectProperty.jsx
@@ -143,6 +143,10 @@ const ObjectProperty = ({
if (name?.includes('.')) {
formItemName = name ? name.split('.') : undefined
}
+ // For state type with options (editable), form field is state.type
+ if (type === 'state' && options?.length && !readOnly) {
+ formItemName = ['state', 'type']
+ }
var textParams = { style: { whiteSpace: 'nowrap', minWidth: '0' } }
@@ -662,6 +666,18 @@ const ObjectProperty = ({
{...inputProps}
/>
)
+ case 'state':
+ if (options?.length && !readOnly) {
+ return (
+
+ )
+ }
+ return null
case 'priceMode':
return (