From c4beb3d3a04a74730c0096a676a8d73df343b72e Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Mon, 24 Aug 2026 02:49:33 +0100 Subject: [PATCH] Enhance Dashboard Components with Marketplaces and Description Features - Updated CourierServiceInfo and ListingInfo components to include collapsible sections for Marketplaces and Listing Description, improving data organization and user experience. - Introduced ObjectProperty component to display marketplace details and listing descriptions dynamically. - Added new properties to the Courier and Listing models to support marketplaces and stock quantity, enhancing data management capabilities. - Implemented SyncMarketplace component for refreshing marketplace metadata, streamlining the synchronization process. - Enhanced CustomSelect and MarkdownDisplay components for better usability and content rendering. --- .../CourierServices/CourierServiceInfo.jsx | 81 ++++++++++------ .../ListingVarients/NewListingVarient.jsx | 3 +- .../Dashboard/Sales/Listings/ListingInfo.jsx | 27 +++++- .../Dashboard/Sales/Listings/NewListing.jsx | 3 +- .../Sales/Marketplaces/SyncMarketplace.jsx | 46 +++++++++ .../Dashboard/common/CustomSelect.jsx | 2 + .../Dashboard/common/MarkdownDisplay.jsx | 5 +- .../Dashboard/common/ObjectProperty.jsx | 6 +- src/database/models/Courier.js | 9 ++ src/database/models/CourierService.js | 54 ++++++++++- src/database/models/Listing.js | 52 ++++++++++ src/database/models/ListingVarient.js | 13 ++- src/database/models/Marketplace.js | 96 +++++++++++++++---- src/database/models/Printer.js | 12 ++- 14 files changed, 345 insertions(+), 64 deletions(-) create mode 100644 src/components/Dashboard/Sales/Marketplaces/SyncMarketplace.jsx diff --git a/src/components/Dashboard/Management/CourierServices/CourierServiceInfo.jsx b/src/components/Dashboard/Management/CourierServices/CourierServiceInfo.jsx index ceda405b..7eded5fd 100644 --- a/src/components/Dashboard/Management/CourierServices/CourierServiceInfo.jsx +++ b/src/components/Dashboard/Management/CourierServices/CourierServiceInfo.jsx @@ -8,10 +8,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' @@ -23,7 +25,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 log = loglevel.getLogger('CourierServiceInfo') @@ -40,6 +42,7 @@ const CourierServiceInfo = () => { 'CourierServiceInfo', { info: true, + marketplaces: true, notes: true, auditLogs: false } @@ -103,6 +106,7 @@ const CourierServiceInfo = () => { disabled={objectFormState.loading} items={[ { key: 'info', label: 'Courier Service Information' }, + { key: 'marketplaces', label: 'Marketplaces' }, { key: 'notes', label: 'Notes' }, { key: 'auditLogs', label: 'Audit Logs' } ]} @@ -155,33 +159,56 @@ const CourierServiceInfo = () => { 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' + > + + + + )} + { _reference: false, createdAt: false, updatedAt: false, - lastSyncedAt: false + lastSyncedAt: false, + stockQuantity: false }} isEditing={false} objectData={objectData} diff --git a/src/components/Dashboard/Sales/Listings/ListingInfo.jsx b/src/components/Dashboard/Sales/Listings/ListingInfo.jsx index 390df921..025afb42 100644 --- a/src/components/Dashboard/Sales/Listings/ListingInfo.jsx +++ b/src/components/Dashboard/Sales/Listings/ListingInfo.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 TextIcon from '../../../Icons/TextIcon.jsx' import NoteIcon from '../../../Icons/NoteIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' import ListingVarientIcon from '../../../Icons/ListingVarientIcon.jsx' @@ -25,7 +27,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 { getModelByName, getModelProperty } from '../../../../database/ObjectModels.js' import ScrollBox from '../../common/ScrollBox.jsx' const log = loglevel.getLogger('ListingInfo') @@ -47,6 +49,7 @@ const ListingInfo = () => { const listingId = new URLSearchParams(location.search).get('listingId') const [collapseState, updateCollapseState] = useCollapseState('ListingInfo', { info: true, + description: true, listingVarients: true, notes: true, auditLogs: false @@ -109,6 +112,7 @@ const ListingInfo = () => { disabled={objectFormState.loading} items={[ { key: 'info', label: 'Listing Information' }, + { key: 'description', label: 'Listing Description' }, { key: 'listingVarients', label: 'Listing Varients' }, { key: 'notes', label: 'Notes' }, { key: 'auditLogs', label: 'Audit Logs' } @@ -189,8 +193,29 @@ const ListingInfo = () => { type='listing' labelWidth={165} objectData={objectData} + visibleProperties={{ description: false }} /> + } + active={collapseState.description} + onToggle={(expanded) => + updateCollapseState('description', expanded) + } + collapseKey='description' + > + + + + } diff --git a/src/components/Dashboard/Sales/Listings/NewListing.jsx b/src/components/Dashboard/Sales/Listings/NewListing.jsx index cc31176c..1e6d36c9 100644 --- a/src/components/Dashboard/Sales/Listings/NewListing.jsx +++ b/src/components/Dashboard/Sales/Listings/NewListing.jsx @@ -53,7 +53,8 @@ const NewListing = ({ onOk, defaultValues }) => { _reference: false, createdAt: false, updatedAt: false, - lastSyncedAt: false + lastSyncedAt: false, + stockQuantity: false }} isEditing={false} objectData={objectData} diff --git a/src/components/Dashboard/Sales/Marketplaces/SyncMarketplace.jsx b/src/components/Dashboard/Sales/Marketplaces/SyncMarketplace.jsx new file mode 100644 index 00000000..cdba2b4d --- /dev/null +++ b/src/components/Dashboard/Sales/Marketplaces/SyncMarketplace.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 SyncMarketplace = ({ 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' + ) + if (result?.success) { + message.success('Marketplace synced successfully') + onOk(result) + } + } catch (error) { + console.error('Error syncing marketplace:', error) + } finally { + setSyncLoading(false) + } + } + + return ( + + ) +} + +SyncMarketplace.propTypes = { + onOk: PropTypes.func.isRequired, + objectData: PropTypes.object +} + +export default SyncMarketplace diff --git a/src/components/Dashboard/common/CustomSelect.jsx b/src/components/Dashboard/common/CustomSelect.jsx index ec69b294..c83fe3d1 100644 --- a/src/components/Dashboard/common/CustomSelect.jsx +++ b/src/components/Dashboard/common/CustomSelect.jsx @@ -40,6 +40,8 @@ const CustomSelect = ({ options={Array.isArray(options) ? options : []} value={value} onChange={onChange} + showSearch + optionFilterProp='label' {...rest} /> ) diff --git a/src/components/Dashboard/common/MarkdownDisplay.jsx b/src/components/Dashboard/common/MarkdownDisplay.jsx index 4b5b80bd..6835ea8d 100644 --- a/src/components/Dashboard/common/MarkdownDisplay.jsx +++ b/src/components/Dashboard/common/MarkdownDisplay.jsx @@ -30,6 +30,7 @@ const BlockquoteComponent = ({ children }) => ( BlockquoteComponent.propTypes = { children: PropTypes.node } const MarkdownDisplay = ({ content }) => { + const markdown = typeof content === 'string' ? content : '' const components = { h1: (props) =>

, h2: (props) =>

, @@ -57,14 +58,14 @@ const MarkdownDisplay = ({ content }) => { return (
- {content} + {markdown}
) } MarkdownDisplay.propTypes = { - content: PropTypes.string.isRequired + content: PropTypes.string } export default MarkdownDisplay diff --git a/src/components/Dashboard/common/ObjectProperty.jsx b/src/components/Dashboard/common/ObjectProperty.jsx index e2947ef3..3ef0cb5a 100644 --- a/src/components/Dashboard/common/ObjectProperty.jsx +++ b/src/components/Dashboard/common/ObjectProperty.jsx @@ -138,8 +138,8 @@ const ObjectProperty = ({ scrollHeight, ...rest }) => { - if (value && typeof value == 'function' && objectData) { - value = value(objectData, parentData) + if (typeof value === 'function') { + value = value(objectData || {}, parentData) } if (max && typeof max == 'function' && objectData) { @@ -417,7 +417,7 @@ const ObjectProperty = ({ ) } case 'markdown': - if (value != null && value != '') { + if (typeof value === 'string' && value !== '') { return } else { return ( diff --git a/src/database/models/Courier.js b/src/database/models/Courier.js index 6a8a040d..70ced73f 100644 --- a/src/database/models/Courier.js +++ b/src/database/models/Courier.js @@ -153,6 +153,15 @@ export const Courier = { required: true, columnWidth: 180 }, + { + name: 'externalReference', + label: 'External Reference', + type: 'text', + placeholder: 'RoyalMail', + extra: 'Marketplace shipping carrier code used when publishing to eBay.', + showCopy: true, + columnWidth: 200 + }, { name: 'name', label: 'Name', diff --git a/src/database/models/CourierService.js b/src/database/models/CourierService.js index b91b2d05..fc540ee4 100644 --- a/src/database/models/CourierService.js +++ b/src/database/models/CourierService.js @@ -1,10 +1,12 @@ import { createElement, lazy } from 'react' const CourierServiceInfo = lazy( - () => import('../../components/Dashboard/Management/CourierServices/CourierServiceInfo') + () => + import('../../components/Dashboard/Management/CourierServices/CourierServiceInfo') ) const NewCourierService = lazy( - () => import('../../components/Dashboard/Management/CourierServices/NewCourierService') + () => + import('../../components/Dashboard/Management/CourierServices/NewCourierService') ) const DeleteObject = lazy( () => import('../../components/Dashboard/common/DeleteObject') @@ -34,7 +36,11 @@ export const CourierService = { label: 'New Courier Service', icon: PlusIcon, content: (objectData, { onOk } = {}) => { - return createElement(NewCourierService, { defaultValues: objectData, onOk, reset: true }) + return createElement(NewCourierService, { + defaultValues: objectData, + onOk, + reset: true + }) } }, { @@ -193,6 +199,48 @@ export const CourierService = { required: true, columnWidth: 200 }, + { + 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' ? 'select' : 'text', + options: (objectData) => { + const codes = + objectData?.marketplace?.eBay?.availableShippingServices || [] + const current = objectData?.externalReference + const unique = [] + for (const code of [...codes, current]) { + if (code && !unique.includes(code)) unique.push(code) + } + return unique.map((code) => ({ label: code, value: code })) + }, + extra: + 'For eBay listings, choose a shipping service code from the selected marketplace. Sync the marketplace if the list is empty.', + showCopy: true, + required: true, + columnWidth: 260 + } + ] + }, { name: 'deliveryTime', label: 'Delivery Time', diff --git a/src/database/models/Listing.js b/src/database/models/Listing.js index 460c58ff..4a0dc5e6 100644 --- a/src/database/models/Listing.js +++ b/src/database/models/Listing.js @@ -152,9 +152,11 @@ export const Listing = { 'product', 'vendor', 'stockLocation', + 'stockQuantity', 'marketplace', 'courierServices', 'state', + 'condition', 'price', 'currency', 'lastSyncedAt', @@ -165,10 +167,12 @@ export const Listing = { 'product', 'vendor', 'stockLocation', + 'stockQuantity', 'marketplace', 'courierServices', 'state', 'state.type', + 'condition', 'createdAt', 'updatedAt', '_reference' @@ -177,6 +181,7 @@ export const Listing = { 'title', 'vendor', 'state', + 'stockQuantity', 'price', 'lastSyncedAt', 'createdAt', @@ -239,6 +244,16 @@ export const Listing = { return objectData?.title } }, + { + name: 'description', + label: 'Description', + type: 'markdown', + readOnly: false, + required: false, + span: 2, + extra: + 'Used as the eBay listing description. If empty, the title is sent instead.' + }, { name: 'lastSyncedAt', label: 'Last Synced', @@ -282,6 +297,15 @@ export const Listing = { required: true, columnWidth: 200 }, + { + name: 'stockQuantity', + label: 'Stock Quantity', + type: 'number', + min: 0, + readOnly: true, + required: false, + columnWidth: 150 + }, { name: 'marketplace', label: 'Marketplace', @@ -300,6 +324,34 @@ export const Listing = { readOnly: true, columnWidth: 260 }, + { + name: 'condition', + label: 'Condition', + type: 'select', + required: true, + extra: 'Required by eBay for most categories. Stored in camel case and converted when publishing.', + options: [ + { value: 'new', label: 'New' }, + { value: 'likeNew', label: 'Like New' }, + { value: 'newOther', label: 'New Other' }, + { value: 'newWithDefects', label: 'New With Defects' }, + { value: 'manufacturerRefurbished', label: 'Manufacturer Refurbished' }, + { value: 'certifiedRefurbished', label: 'Certified Refurbished' }, + { value: 'excellentRefurbished', label: 'Excellent Refurbished' }, + { value: 'veryGoodRefurbished', label: 'Very Good Refurbished' }, + { value: 'goodRefurbished', label: 'Good Refurbished' }, + { value: 'sellerRefurbished', label: 'Seller Refurbished' }, + { value: 'usedExcellent', label: 'Used Excellent' }, + { value: 'usedVeryGood', label: 'Used Very Good' }, + { value: 'usedGood', label: 'Used Good' }, + { value: 'usedAcceptable', label: 'Used Acceptable' }, + { value: 'forPartsOrNotWorking', label: 'For Parts Or Not Working' }, + { value: 'preOwnedExcellent', label: 'Pre Owned Excellent' }, + { value: 'preOwnedFair', label: 'Pre Owned Fair' } + ], + value: (objectData) => objectData?.condition || 'new', + columnWidth: 200 + }, { name: 'price', label: 'Price', diff --git a/src/database/models/ListingVarient.js b/src/database/models/ListingVarient.js index c9d2f159..11f3cae7 100644 --- a/src/database/models/ListingVarient.js +++ b/src/database/models/ListingVarient.js @@ -143,6 +143,7 @@ export const ListingVarient = { 'listing', 'product', 'productSku', + 'stockQuantity', 'state', 'price', 'currency', @@ -154,13 +155,14 @@ export const ListingVarient = { 'listing', 'product', 'productSku', + 'stockQuantity', 'state', 'state.type', 'createdAt', 'updatedAt', '_reference' ], - sorters: ['state', 'price', 'lastSyncedAt', 'createdAt', 'updatedAt', '_id'], + sorters: ['state', 'stockQuantity', 'price', 'lastSyncedAt', 'createdAt', 'updatedAt', '_id'], group: ['listing', 'state'], properties: [ { @@ -232,6 +234,15 @@ export const ListingVarient = { return { product: objectData?.listing?.product?._id } } }, + { + name: 'stockQuantity', + label: 'Stock Quantity', + type: 'number', + min: 0, + readOnly: true, + required: false, + columnWidth: 150 + }, { name: 'state', label: 'State', diff --git a/src/database/models/Marketplace.js b/src/database/models/Marketplace.js index 29402ba6..eb886bcd 100644 --- a/src/database/models/Marketplace.js +++ b/src/database/models/Marketplace.js @@ -6,6 +6,9 @@ const MarketplaceInfo = lazy( const NewMarketplace = lazy( () => import('../../components/Dashboard/Sales/Marketplaces/NewMarketplace') ) +const SyncMarketplace = lazy( + () => import('../../components/Dashboard/Sales/Marketplaces/SyncMarketplace') +) const SyncListings = lazy( () => import('../../components/Dashboard/Sales/Marketplaces/SyncListings') ) @@ -42,7 +45,11 @@ export const Marketplace = { label: 'New Marketplace', icon: PlusIcon, content: (objectData, { onOk } = {}) => { - return createElement(NewMarketplace, { defaultValues: objectData, onOk, reset: true }) + return createElement(NewMarketplace, { + defaultValues: objectData, + onOk, + reset: true + }) } }, { @@ -94,30 +101,66 @@ export const Marketplace = { }, { type: 'divider' }, { - name: 'syncListings', - type: 'modal', - modalWidth: 520, - label: 'Sync Listings', + name: 'sync', + label: 'Sync', icon: ReloadIcon, disabled: (objectData) => { return objectData?.state?.type != 'ready' }, - content: (objectData, { onOk } = {}) => { - return createElement(SyncListings, { objectData, onOk }) - } - }, - { - name: 'syncOrders', - type: 'modal', - modalWidth: 520, - label: 'Sync Orders', - icon: ReloadIcon, - disabled: (objectData) => { - return objectData?.state?.type != 'ready' - }, - content: (objectData, { onOk } = {}) => { - return createElement(SyncOrders, { objectData, onOk }) - } + children: [ + { + 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: 'syncListings', + type: 'modal', + modalWidth: 520, + label: 'Sync Listings', + icon: ReloadIcon, + disabled: (objectData) => { + return objectData?.state?.type != 'ready' + }, + content: (objectData, { onOk } = {}) => { + 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', + modalWidth: 520, + label: 'Sync Orders', + icon: ReloadIcon, + disabled: (objectData) => { + return objectData?.state?.type != 'ready' + }, + content: (objectData, { onOk } = {}) => { + return createElement(SyncOrders, { objectData, onOk }) + } + } + ] }, { type: 'divider' }, { @@ -270,6 +313,17 @@ 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', diff --git a/src/database/models/Printer.js b/src/database/models/Printer.js index 18849671..0c1ef202 100644 --- a/src/database/models/Printer.js +++ b/src/database/models/Printer.js @@ -56,7 +56,11 @@ export const Printer = { label: 'New Printer', icon: PlusIcon, content: (objectData, { onOk } = {}) => { - return createElement(NewPrinter, { defaultValues: objectData, onOk, reset: true }) + return createElement(NewPrinter, { + defaultValues: objectData, + onOk, + reset: true + }) } }, { @@ -123,7 +127,7 @@ export const Printer = { }, { type: 'divider' }, { - name: 'restartSubmenu', + name: 'restart', label: 'Restart', icon: ReloadIcon, disabled: (objectData) => { @@ -131,11 +135,11 @@ export const Printer = { }, children: [ { - name: 'restart', + name: 'restartPrinter', type: 'modal', modalWidth: 520, modalCentered: true, - label: 'Restart', + label: 'Restart Printer', icon: ReloadIcon, disabled: (objectData) => { return objectData?.online == false