From 8e393e229fa119b81ae29878d6c1dddd820f5e9d Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Fri, 13 Mar 2026 23:32:23 +0000 Subject: [PATCH] Implemented market places. --- assets/icons/marketplaceicon.svg | 5 + .../Dashboard/Sales/Marketplaces.jsx | 107 ++++++++ .../Sales/Marketplaces/MarketplaceInfo.jsx | 200 ++++++++++++++ .../Sales/Marketplaces/NewMarketplace.jsx | 103 ++++++++ .../Dashboard/Sales/SalesSidebar.jsx | 11 +- .../Dashboard/common/ObjectInfo.jsx | 5 + src/components/Icons/MarketplaceIcon.jsx | 6 + src/database/ObjectModels.js | 7 +- src/database/models/Client.js | 11 + src/database/models/Marketplace.js | 244 ++++++++++++++++++ src/database/models/SalesOrder.js | 15 +- src/routes/SalesRoutes.jsx | 12 + 12 files changed, 720 insertions(+), 6 deletions(-) create mode 100644 assets/icons/marketplaceicon.svg create mode 100644 src/components/Dashboard/Sales/Marketplaces.jsx create mode 100644 src/components/Dashboard/Sales/Marketplaces/MarketplaceInfo.jsx create mode 100644 src/components/Dashboard/Sales/Marketplaces/NewMarketplace.jsx create mode 100644 src/components/Icons/MarketplaceIcon.jsx create mode 100644 src/database/models/Marketplace.js diff --git a/assets/icons/marketplaceicon.svg b/assets/icons/marketplaceicon.svg new file mode 100644 index 0000000..58f212e --- /dev/null +++ b/assets/icons/marketplaceicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/Dashboard/Sales/Marketplaces.jsx b/src/components/Dashboard/Sales/Marketplaces.jsx new file mode 100644 index 0000000..9beab06 --- /dev/null +++ b/src/components/Dashboard/Sales/Marketplaces.jsx @@ -0,0 +1,107 @@ +import { useState, useRef } from 'react' +import { Button, Flex, Space, Modal, Dropdown } from 'antd' +import NewMarketplace from './Marketplaces/NewMarketplace' +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 Marketplaces = () => { + const [newMarketplaceOpen, setNewMarketplaceOpen] = useState(false) + const tableRef = useRef() + + const [viewMode, setViewMode] = useViewMode('marketplaces') + + const [columnVisibility, setColumnVisibility] = + useColumnVisibility('marketplaces') + + const [showFilterSidebar, setShowFilterSidebar] = + useFilterSidebarVisibility('Marketplaces') + + const actionItems = { + items: [ + { + label: 'New Marketplace', + key: 'newMarketplace', + icon: + }, + { type: 'divider' }, + { + label: 'Reload List', + key: 'reloadList', + icon: + } + ], + onClick: ({ key }) => { + if (key === 'reloadList') { + tableRef.current?.reload() + } else if (key === 'newMarketplace') { + setNewMarketplaceOpen(true) + } + } + } + + return ( + <> + + + + + + + + + + + setShowFilterSidebar(!showFilterSidebar)} + /> + + + + + + { + setNewMarketplaceOpen(false) + }} + destroyOnHidden={true} + > + { + setNewMarketplaceOpen(false) + tableRef.current?.reload() + }} + reset={newMarketplaceOpen} + /> + + + ) +} + +export default Marketplaces diff --git a/src/components/Dashboard/Sales/Marketplaces/MarketplaceInfo.jsx b/src/components/Dashboard/Sales/Marketplaces/MarketplaceInfo.jsx new file mode 100644 index 0000000..9bd461f --- /dev/null +++ b/src/components/Dashboard/Sales/Marketplaces/MarketplaceInfo.jsx @@ -0,0 +1,200 @@ +import { useRef, useState } from 'react' +import { useLocation } from 'react-router-dom' +import { Space, Flex } 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 { Card } from 'antd' + +const log = loglevel.getLogger('MarketplaceInfo') +log.setLevel(config.logLevel) + +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 [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 + } + } + + 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} + /> + + + + + + } + active={collapseState.info} + onToggle={(expanded) => updateCollapseState('info', expanded)} + collapseKey='info' + > + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} + > + {({ loading, isEditing, objectData }) => ( + + )} + + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + + + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) + } + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + + + ) +} + +export default MarketplaceInfo diff --git a/src/components/Dashboard/Sales/Marketplaces/NewMarketplace.jsx b/src/components/Dashboard/Sales/Marketplaces/NewMarketplace.jsx new file mode 100644 index 0000000..0221a1b --- /dev/null +++ b/src/components/Dashboard/Sales/Marketplaces/NewMarketplace.jsx @@ -0,0 +1,103 @@ +import PropTypes from 'prop-types' +import ObjectInfo from '../../common/ObjectInfo' +import NewObjectForm from '../../common/NewObjectForm' +import WizardView from '../../common/WizardView' + +const NewMarketplace = ({ onOk, defaultValues }) => { + return ( + + {({ handleSubmit, submitLoading, objectData, formValid }) => { + const steps = [ + { + title: 'Required', + key: 'required', + content: ( + + ) + }, + { + title: 'API Configuration', + key: 'config', + content: ( + + ) + }, + { + title: 'Summary', + key: 'summary', + content: ( + + ) + } + ] + return ( + { + const result = await handleSubmit() + if (result) { + onOk() + } + }} + /> + ) + }} + + ) +} + +NewMarketplace.propTypes = { + onOk: PropTypes.func.isRequired, + reset: PropTypes.bool, + defaultValues: PropTypes.object +} + +export default NewMarketplace diff --git a/src/components/Dashboard/Sales/SalesSidebar.jsx b/src/components/Dashboard/Sales/SalesSidebar.jsx index 9b89a29..da7dd8d 100644 --- a/src/components/Dashboard/Sales/SalesSidebar.jsx +++ b/src/components/Dashboard/Sales/SalesSidebar.jsx @@ -2,8 +2,8 @@ import { useLocation } from 'react-router-dom' import DashboardSidebar from '../common/DashboardSidebar' import ClientIcon from '../../Icons/ClientIcon' import SalesIcon from '../../Icons/SalesIcon' - import SalesOrderIcon from '../../Icons/SalesOrderIcon' +import MarketplaceIcon from '../../Icons/MarketplaceIcon' const items = [ { @@ -24,13 +24,20 @@ const items = [ label: 'Sales Orders', icon: , path: '/dashboard/sales/salesorders' + }, + { + key: 'marketplaces', + label: 'Marketplaces', + icon: , + path: '/dashboard/sales/marketplaces' } ] const routeKeyMap = { '/dashboard/sales/overview': 'overview', '/dashboard/sales/clients': 'clients', - '/dashboard/sales/salesorders': 'salesorders' + '/dashboard/sales/salesorders': 'salesorders', + '/dashboard/sales/marketplaces': 'marketplaces' } const SalesSidebar = (props) => { diff --git a/src/components/Dashboard/common/ObjectInfo.jsx b/src/components/Dashboard/common/ObjectInfo.jsx index 8f96fc1..4ec1d97 100644 --- a/src/components/Dashboard/common/ObjectInfo.jsx +++ b/src/components/Dashboard/common/ObjectInfo.jsx @@ -63,6 +63,11 @@ const ObjectInfo = ({ items = items.filter((item) => { const propertyName = item.name + // Support property.visible as a function (objectData) => boolean + if (typeof item.visible === 'function') { + const visible = item.visible(objectData || combinedObjectData || {}) + if (!visible) return false + } if (isWhitelistMode) { // Whitelist mode: only show properties that are explicitly set to true return visibleProperties[propertyName] === true diff --git a/src/components/Icons/MarketplaceIcon.jsx b/src/components/Icons/MarketplaceIcon.jsx new file mode 100644 index 0000000..985e064 --- /dev/null +++ b/src/components/Icons/MarketplaceIcon.jsx @@ -0,0 +1,6 @@ +import Icon from '@ant-design/icons' +import CustomIconSvg from '../../../assets/icons/marketplaceicon.svg?react' + +const MarketplaceIcon = (props) => + +export default MarketplaceIcon diff --git a/src/database/ObjectModels.js b/src/database/ObjectModels.js index ddb3043..3acf151 100644 --- a/src/database/ObjectModels.js +++ b/src/database/ObjectModels.js @@ -39,6 +39,7 @@ import { Invoice } from './models/Invoice.js' import { Payment } from './models/Payment.js' import { Client } from './models/Client.js' import { SalesOrder } from './models/SalesOrder.js' +import { Marketplace } from './models/Marketplace.js' import QuestionCircleIcon from '../components/Icons/QuestionCircleIcon' export const objectModels = [ @@ -82,7 +83,8 @@ export const objectModels = [ Invoice, Payment, Client, - SalesOrder + SalesOrder, + Marketplace ] // Re-export individual models for direct access @@ -127,7 +129,8 @@ export { Invoice, Payment, Client, - SalesOrder + SalesOrder, + Marketplace } export function getModelByName(name, ignoreCase = false) { diff --git a/src/database/models/Client.js b/src/database/models/Client.js index 62993f1..29f8e56 100644 --- a/src/database/models/Client.js +++ b/src/database/models/Client.js @@ -84,6 +84,7 @@ export const Client = { 'email', 'phone', 'active', + 'marketplace', 'createdAt', 'updatedAt' ], @@ -186,6 +187,16 @@ export const Client = { readOnly: false, required: false, columnWidth: 250 + }, + { + name: 'marketplace', + label: 'Marketplace', + type: 'object', + objectType: 'marketplace', + showHyperlink: true, + readOnly: false, + required: false, + columnWidth: 200 } ] } diff --git a/src/database/models/Marketplace.js b/src/database/models/Marketplace.js new file mode 100644 index 0000000..74c7ffb --- /dev/null +++ b/src/database/models/Marketplace.js @@ -0,0 +1,244 @@ +import MarketplaceIcon from '../../components/Icons/MarketplaceIcon' +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 ReloadIcon from '../../components/Icons/ReloadIcon' +import BinIcon from '../../components/Icons/BinIcon' + +export const Marketplace = { + name: 'marketplace', + label: 'Marketplace', + prefix: 'MKT', + icon: MarketplaceIcon, + actions: [ + { + name: 'info', + label: 'Info', + default: true, + row: true, + icon: InfoCircleIcon, + url: (_id) => `/dashboard/sales/marketplaces/info?marketplaceId=${_id}` + }, + { + name: 'reload', + label: 'Reload', + icon: ReloadIcon, + url: (_id) => + `/dashboard/sales/marketplaces/info?marketplaceId=${_id}&action=reload` + }, + { + name: 'edit', + label: 'Edit', + row: true, + icon: EditIcon, + url: (_id) => + `/dashboard/sales/marketplaces/info?marketplaceId=${_id}&action=edit`, + visible: (objectData) => { + return !(objectData?._isEditing && objectData?._isEditing == true) + } + }, + { + name: 'finishEdit', + label: 'Save Edits', + icon: CheckIcon, + url: (_id) => + `/dashboard/sales/marketplaces/info?marketplaceId=${_id}&action=finishEdit`, + visible: (objectData) => { + return objectData?._isEditing && objectData?._isEditing == true + } + }, + { + name: 'cancelEdit', + label: 'Cancel Edits', + icon: XMarkIcon, + url: (_id) => + `/dashboard/sales/marketplaces/info?marketplaceId=${_id}&action=cancelEdit`, + visible: (objectData) => { + return objectData?._isEditing && objectData?._isEditing == true + } + }, + { type: 'divider' }, + { + name: 'delete', + label: 'Delete', + icon: BinIcon, + danger: true, + url: (_id) => + `/dashboard/sales/marketplaces/info?marketplaceId=${_id}&action=delete` + } + ], + columns: [ + '_reference', + 'name', + 'provider', + 'active', + 'createdAt', + 'updatedAt' + ], + filters: ['name', '_id', 'provider', 'active', 'createdAt', 'updatedAt'], + sorters: ['name', 'provider', 'active', 'createdAt', 'updatedAt', '_id'], + group: ['provider'], + properties: [ + { + name: '_id', + label: 'ID', + columnFixed: 'left', + type: 'id', + objectType: 'marketplace', + showCopy: true, + columnWidth: 140 + }, + { + name: 'createdAt', + label: 'Created At', + type: 'dateTime', + readOnly: true, + columnWidth: 175 + }, + { + name: '_reference', + label: 'Reference', + type: 'reference', + columnFixed: 'left', + objectType: 'marketplace', + showCopy: true, + readOnly: true + }, + { + name: 'updatedAt', + label: 'Updated At', + type: 'dateTime', + readOnly: true, + columnWidth: 175 + }, + { + name: 'name', + label: 'Name', + columnFixed: 'left', + required: true, + type: 'text', + columnWidth: 200 + }, + { + name: 'active', + label: 'Active', + type: 'bool', + readOnly: false, + required: true, + columnWidth: 125 + }, + { + name: 'provider', + label: 'Provider', + type: 'select', + required: true, + options: [ + { value: 'ebay', label: 'eBay' }, + { value: 'etsy', label: 'Etsy' }, + { value: 'tiktokShop', label: 'TikTok Shop' } + ], + columnWidth: 150 + }, + + { + name: 'config.appId', + label: 'App ID', + type: 'secret', + readOnly: false, + required: false, + columnWidth: 200, + visible: (objectData) => objectData?.provider === 'ebay' + }, + { + name: 'config.certId', + label: 'Cert ID', + type: 'secret', + readOnly: false, + required: false, + columnWidth: 200, + visible: (objectData) => objectData?.provider === 'ebay' + }, + { + name: 'config.devId', + label: 'Dev ID', + type: 'secret', + readOnly: false, + required: false, + columnWidth: 200, + visible: (objectData) => objectData?.provider === 'ebay' + }, + { + name: 'config.userToken', + label: 'User Token', + type: 'secret', + readOnly: false, + required: false, + columnWidth: 200, + visible: (objectData) => objectData?.provider === 'ebay' + }, + { + name: 'config.siteId', + label: 'Site ID', + type: 'text', + readOnly: false, + required: false, + columnWidth: 120, + visible: (objectData) => objectData?.provider === 'ebay' + }, + { + name: 'config.accessToken', + label: 'Access Token', + type: 'secret', + readOnly: false, + required: false, + columnWidth: 200, + visible: (objectData) => objectData?.provider === 'etsy' + }, + { + name: 'config.refreshToken', + label: 'Refresh Token', + type: 'secret', + readOnly: false, + required: false, + columnWidth: 200, + visible: (objectData) => objectData?.provider === 'etsy' + }, + { + name: 'config.shopId', + label: 'Shop ID', + type: 'text', + readOnly: false, + required: false, + columnWidth: 200, + visible: (objectData) => objectData?.provider === 'etsy' + }, + { + name: 'config.appKey', + label: 'App Key', + type: 'secret', + readOnly: false, + required: false, + columnWidth: 200, + visible: (objectData) => objectData?.provider === 'tiktokShop' + }, + { + name: 'config.appSecret', + label: 'App Secret', + type: 'secret', + readOnly: false, + required: false, + columnWidth: 200, + visible: (objectData) => objectData?.provider === 'tiktokShop' + }, + { + name: 'config.shopCipher', + label: 'Shop Cipher', + type: 'text', + readOnly: false, + required: false, + columnWidth: 200, + visible: (objectData) => objectData?.provider === 'tiktokShop' + } + ] +} diff --git a/src/database/models/SalesOrder.js b/src/database/models/SalesOrder.js index 76522bb..3b1dd60 100644 --- a/src/database/models/SalesOrder.js +++ b/src/database/models/SalesOrder.js @@ -170,13 +170,14 @@ export const SalesOrder = { } } ], - group: ['client'], - filters: ['client'], + group: ['client', 'marketplace'], + filters: ['client', 'marketplace'], sorters: ['createdAt', 'state', 'updatedAt'], columns: [ '_reference', 'state', 'client', + 'marketplace', 'totalAmount', 'totalAmountWithTax', 'totalTaxAmount', @@ -245,6 +246,16 @@ export const SalesOrder = { showHyperlink: true, columnWidth: 200 }, + { + name: 'marketplace', + label: 'Marketplace', + type: 'object', + objectType: 'marketplace', + showHyperlink: true, + readOnly: false, + required: false, + columnWidth: 200 + }, { name: 'confirmedAt', label: 'Confirmed At', diff --git a/src/routes/SalesRoutes.jsx b/src/routes/SalesRoutes.jsx index 89d5a38..b6f1b45 100644 --- a/src/routes/SalesRoutes.jsx +++ b/src/routes/SalesRoutes.jsx @@ -16,6 +16,12 @@ const SalesOrderInfo = lazy( const SalesOverview = lazy( () => import('../components/Dashboard/Sales/SalesOverview.jsx') ) +const Marketplaces = lazy( + () => import('../components/Dashboard/Sales/Marketplaces.jsx') +) +const MarketplaceInfo = lazy( + () => import('../components/Dashboard/Sales/Marketplaces/MarketplaceInfo.jsx') +) const SalesRoutes = [ } + />, + } />, + } /> ]