Add Payment and Fulfillment Policy Components with SVG Icons
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- 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.
This commit is contained in:
Tom Butcher 2026-08-29 00:47:46 +01:00
parent 6cc6bf9a79
commit 26132d206c
67 changed files with 4116 additions and 251 deletions

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.607884,0,0,0.607884,2,6.470704)">
<path d="M50.153,64.047L37.296,64.047C35.825,69.222 31.078,73.031 25.453,73.031C19.816,73.031 15.034,69.222 13.552,64.047L10.547,64.047C3.781,64.047 0,60.266 0,53.516L0,11.688C0,4.938 3.781,1.141 10.547,1.141L57.859,1.141C64.609,1.141 68.391,4.938 68.391,11.688L68.391,18.422L76.766,18.422C79.761,18.422 81.953,19.046 83.846,20.626C83.618,20.616 83.389,20.612 83.159,20.611L83.154,20.611C78.507,20.611 74.687,22.221 70.737,26.132L70.727,26.142L60.359,36.468L60.359,12.328C60.359,10.141 59.281,9.141 57.172,9.141L11.203,9.141C9.078,9.141 8.031,10.141 8.031,12.328L8.031,52.859C8.031,55.062 9.078,56.031 11.203,56.031L13.984,56.031C15.828,51.524 20.28,48.344 25.453,48.344C30.614,48.344 35.037,51.524 36.867,56.031L47.85,56.031C47.915,58.873 48.681,61.577 50.153,64.047ZM25.453,66.422C28.609,66.422 31.156,63.828 31.156,60.656C31.156,57.5 28.609,54.906 25.453,54.906C22.281,54.906 19.688,57.5 19.688,60.656C19.688,63.828 22.281,66.422 25.453,66.422Z"/>
</g>
<g transform="matrix(0.319305,0,0,0.319305,12.718629,15.454198)">
<rect x="0" y="0" width="61.719" height="58.219" style="fill-opacity:0;"/>
<path d="M24.594,58.219C27.5,58.219 29.781,57.156 31.312,54.875L59.969,12.312C61.062,10.719 61.531,9.062 61.531,7.625C61.531,3.5 58.25,0.375 54,0.375C51.125,0.375 49.281,1.406 47.5,4.156L24.469,40.125L13.062,27C11.562,25.281 9.812,24.469 7.438,24.469C3.156,24.469 0,27.594 0,31.75C0,33.625 0.5,35.125 2.125,36.969L18.188,55.406C19.906,57.344 21.969,58.219 24.594,58.219Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(0.515011,0,0,0.515011,34.08479,22)">
<path d="M0.001,35.371C-0.038,39.202 1.473,42.727 4.441,45.671L16.349,57.61C11.913,60.839 9.844,64.493 9.844,69.059C9.844,76.236 15.431,81.432 22.42,81.549C26.022,81.612 29.343,80.305 31.751,77.938L53.883,55.695C59.804,49.751 59.453,42.361 52.939,35.83L41.879,24.794L44.775,21.89C47.234,19.407 48.675,16.156 48.692,12.936C48.785,5.665 43.16,0.007 35.85,0C31.88,0 28.667,1.489 25.293,4.83L3.649,26.387C1.324,28.753 0.032,31.925 0.001,35.371ZM18.613,24.255L31.665,11.203C33.369,9.474 34.298,9.018 35.85,9.018C37.991,9.018 39.664,10.692 39.664,12.825C39.664,13.85 39.304,14.61 38.379,15.518L24.077,29.836C23.14,27.191 21.326,25.315 18.613,24.255ZM9.135,35.594C9.135,33.425 10.709,31.799 12.756,31.799C14.857,31.799 16.468,33.464 16.468,35.669C16.468,36.68 16.444,37.157 16.216,37.941C15.03,43.335 21.775,44.881 24.421,42.268L35.475,31.174L46.542,42.227C49.615,45.227 49.817,47.01 47.511,49.299L33.566,63.268C32.715,61.558 31.327,59.892 29.126,57.643L11.994,40.417C9.701,38.148 9.135,37.157 9.135,35.594ZM18.887,69.149C18.887,67.295 20.04,65.793 22.846,64.06L23.303,64.516C25.504,66.773 26.164,67.845 26.172,69.173C26.22,71.078 24.636,72.476 22.444,72.476C20.445,72.476 18.887,71.13 18.887,69.149Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.81149,0,0,0.81149,2,10.044134)">
<path d="M42.914,54.093L10.251,54.093C3.506,54.093 0,50.638 0,43.969L0,10.169C0,3.501 3.506,0.02 10.251,0.02L63.687,0.02C70.463,0.02 73.938,3.501 73.938,10.169L73.938,20.595C73.272,16.95 70.992,13.955 67.816,12.338L67.816,10.709C67.816,7.667 66.186,6.142 63.332,6.142L10.606,6.142C7.701,6.142 6.122,7.667 6.122,10.709L6.122,12.998L55.627,12.998C54.744,13.579 53.871,14.299 52.989,15.172L52.981,15.179L47.835,20.305L6.122,20.305L6.122,43.429C6.122,46.471 7.701,47.97 10.606,47.97L41.376,47.97L44.616,51.219C43.884,52.13 43.321,53.086 42.914,54.093ZM14.864,42.322C13.02,42.322 11.801,41.078 11.801,39.328L11.801,33.547C11.801,31.766 13.02,30.547 14.864,30.547L22.52,30.547C24.364,30.547 25.582,31.766 25.582,33.547L25.582,39.328C25.582,41.078 24.364,42.322 22.52,42.322L14.864,42.322Z"/>
</g>
<g transform="matrix(0.515011,0,0,0.515011,34.08479,22)">
<path d="M0.001,35.371C-0.038,39.202 1.473,42.727 4.441,45.671L16.349,57.61C11.913,60.839 9.844,64.493 9.844,69.059C9.844,76.236 15.431,81.432 22.42,81.549C26.022,81.612 29.343,80.305 31.751,77.938L53.883,55.695C59.804,49.751 59.453,42.361 52.939,35.83L41.879,24.794L44.775,21.89C47.234,19.407 48.675,16.156 48.692,12.936C48.785,5.665 43.16,0.007 35.85,0C31.88,0 28.667,1.489 25.293,4.83L3.649,26.387C1.324,28.753 0.032,31.925 0.001,35.371ZM18.613,24.255L31.665,11.203C33.369,9.474 34.298,9.018 35.85,9.018C37.991,9.018 39.664,10.692 39.664,12.825C39.664,13.85 39.304,14.61 38.379,15.518L24.077,29.836C23.14,27.191 21.326,25.315 18.613,24.255ZM9.135,35.594C9.135,33.425 10.709,31.799 12.756,31.799C14.857,31.799 16.468,33.464 16.468,35.669C16.468,36.68 16.444,37.157 16.216,37.941C15.03,43.335 21.775,44.881 24.421,42.268L35.475,31.174L46.542,42.227C49.615,45.227 49.817,47.01 47.511,49.299L33.566,63.268C32.715,61.558 31.327,59.892 29.126,57.643L11.994,40.417C9.701,38.148 9.135,37.157 9.135,35.594ZM18.887,69.149C18.887,67.295 20.04,65.793 22.846,64.06L23.303,64.516C25.504,66.773 26.164,67.845 26.172,69.173C26.22,71.078 24.636,72.476 22.444,72.476C20.445,72.476 18.887,71.13 18.887,69.149Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.607884,0,0,0.607884,2,6.470704)">
<path d="M50.153,64.047L37.296,64.047C35.825,69.222 31.078,73.031 25.453,73.031C19.816,73.031 15.034,69.222 13.552,64.047L10.547,64.047C3.781,64.047 0,60.266 0,53.516L0,11.688C0,4.938 3.781,1.141 10.547,1.141L57.859,1.141C64.609,1.141 68.391,4.938 68.391,11.688L68.391,18.422L76.766,18.422C79.761,18.422 81.953,19.046 83.846,20.626C83.618,20.616 83.389,20.612 83.159,20.611L83.154,20.611C78.507,20.611 74.687,22.221 70.737,26.132L70.727,26.142L60.359,36.468L60.359,12.328C60.359,10.141 59.281,9.141 57.172,9.141L11.203,9.141C9.078,9.141 8.031,10.141 8.031,12.328L8.031,52.859C8.031,55.062 9.078,56.031 11.203,56.031L13.984,56.031C15.828,51.524 20.28,48.344 25.453,48.344C30.614,48.344 35.037,51.524 36.867,56.031L47.85,56.031C47.915,58.873 48.681,61.577 50.153,64.047ZM25.453,66.422C28.609,66.422 31.156,63.828 31.156,60.656C31.156,57.5 28.609,54.906 25.453,54.906C22.281,54.906 19.688,57.5 19.688,60.656C19.688,63.828 22.281,66.422 25.453,66.422Z"/>
</g>
<g transform="matrix(0.33037,0,0,0.33037,12.639581,15.454198)">
<path d="M53.25,58.219C54.656,59.562 56.219,60.219 58.125,60.219C62.062,60.219 65.312,56.969 65.312,53.125C65.312,51.156 64.469,49.312 62.938,47.812L43.562,29.156L43.562,30.906L62.938,12.375C64.5,10.844 65.312,9.031 65.312,7.094C65.312,3.219 62.062,0 58.125,0C56.188,0 54.594,0.625 53.25,2L30.719,24C28.781,25.969 27.969,27.75 27.906,30.094C27.906,32.406 28.75,34.219 30.719,36.188L53.25,58.219Z" style="fill-rule:nonzero;"/>
<g transform="matrix(1,0,0,1,-4.437961,0)">
<path d="M22.23,55.204L2.781,36.187C0.844,34.219 0,32.406 0,30.094C0.031,27.75 0.875,25.969 2.781,24L25.313,2C26.688,0.625 28.25,0 30.188,0C34.125,0 37.406,3.219 37.406,7.094C37.406,9.031 36.594,10.844 35,12.375L16.537,30.034L35,47.812C36.562,49.313 37.406,51.156 37.406,53.125C37.406,55.669 35.969,57.953 33.855,59.207C30.383,57.109 26.443,55.71 22.23,55.204Z"/>
</g>
</g>
<g transform="matrix(0.515011,0,0,0.515011,34.08479,22)">
<path d="M0.001,35.371C-0.038,39.202 1.473,42.727 4.441,45.671L16.349,57.61C11.913,60.839 9.844,64.493 9.844,69.059C9.844,76.236 15.431,81.432 22.42,81.549C26.022,81.612 29.343,80.305 31.751,77.938L53.883,55.695C59.804,49.751 59.453,42.361 52.939,35.83L41.879,24.794L44.775,21.89C47.234,19.407 48.675,16.156 48.692,12.936C48.785,5.665 43.16,0.007 35.85,0C31.88,0 28.667,1.489 25.293,4.83L3.649,26.387C1.324,28.753 0.032,31.925 0.001,35.371ZM18.613,24.255L31.665,11.203C33.369,9.474 34.298,9.018 35.85,9.018C37.991,9.018 39.664,10.692 39.664,12.825C39.664,13.85 39.304,14.61 38.379,15.518L24.077,29.836C23.14,27.191 21.326,25.315 18.613,24.255ZM9.135,35.594C9.135,33.425 10.709,31.799 12.756,31.799C14.857,31.799 16.468,33.464 16.468,35.669C16.468,36.68 16.444,37.157 16.216,37.941C15.03,43.335 21.775,44.881 24.421,42.268L35.475,31.174L46.542,42.227C49.615,45.227 49.817,47.01 47.511,49.299L33.566,63.268C32.715,61.558 31.327,59.892 29.126,57.643L11.994,40.417C9.701,38.148 9.135,37.157 9.135,35.594ZM18.887,69.149C18.887,67.295 20.04,65.793 22.846,64.06L23.303,64.516C25.504,66.773 26.164,67.845 26.172,69.173C26.22,71.078 24.636,72.476 22.444,72.476C20.445,72.476 18.887,71.13 18.887,69.149Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -2143,3 +2143,160 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
border-radius: 1px; border-radius: 1px;
background: color-mix(in srgb, var(--color-text) 50%, transparent); 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);
}

View File

@ -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 (
<Flex vertical={'true'} gap='large' className='h-100'>
<Flex justify={'space-between'} style={{ minHeight: 0 }}>
<Space size='small'>
<ObjectActions
type='paymentPolicy'
pageName='list'
onReload={() => tableRef.current?.reload()}
/>
<ColumnViewButton
type='paymentPolicy'
loading={false}
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
<ExportListButton objectType='paymentPolicy' />
</Space>
<Space>
<SortSidebarButton
active={showSortSidebar}
onClick={() => setShowSortSidebar(!showSortSidebar)}
/>
<FilterSidebarButton
active={showFilterSidebar}
onClick={() => setShowFilterSidebar(!showFilterSidebar)}
/>
<ObjectTableViewButton
viewMode={viewMode}
setViewMode={setViewMode}
showEndingDivider={true}
/>
</Space>
</Flex>
<ObjectTable
ref={tableRef}
visibleColumns={columnVisibility}
type='paymentPolicy'
cards={viewMode === 'cards'}
showFilterSidebar={showFilterSidebar}
showSortSidebar={showSortSidebar}
expandHeight={true}
saveFilterInSession={true}
saveFilterInUrl={true}
useFilterInSession={true}
useFilterInUrl={true}
saveSortInSession={true}
saveSortInUrl={true}
useSortInSession={true}
useSortInUrl={true}
/>
</Flex>
)
}
export default PaymentPolicies

View File

@ -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 (
<NewObjectForm
type={'paymentPolicy'}
defaultValues={{ immediatePay: true, ...defaultValues }}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='paymentPolicy'
column={1}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
labelWidth={140}
/>
)
},
{
title: 'Optional',
key: 'optional',
content: (
<ObjectInfo
type='paymentPolicy'
column={1}
bordered={false}
isEditing={true}
required={false}
labelWidth={140}
objectData={objectData}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='paymentPolicy'
column={1}
bordered={false}
visibleProperties={{
_id: false,
_reference: false,
createdAt: false,
updatedAt: false
}}
labelWidth={140}
isEditing={false}
objectData={objectData}
/>
)
}
]
return (
<WizardView
steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Payment Policy'
onSubmit={async () => {
const result = await handleSubmit()
if (result) {
onOk()
}
}}
/>
)
}}
</NewObjectForm>
)
}
NewPaymentPolicy.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool,
defaultValues: PropTypes.object
}
export default NewPaymentPolicy

View File

@ -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(
() => (
<ObjectTableNavigationButtons
disabled={objectFormState.loading || objectFormState.isEditing}
_id={paymentPolicyId}
objectType='paymentPolicy'
showEndingDivider={false}
/>
),
[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 (
<>
<Flex
gap='large'
vertical='true'
style={{ maxHeight: '100%', minHeight: 0 }}
>
<Flex justify={'space-between'}>
<Space size='small'>
<Space size='small'>
<ObjectActions
type='paymentPolicy'
pageName='info'
id={paymentPolicyId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Payment Policy Information' },
{ key: 'marketplaces', label: 'Marketplaces' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<UserNotifierToggle
type='paymentPolicy'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
<DocumentPrintButton
type='paymentPolicy'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<ActivityIndicator
activities={objectFormState.activities}
showStartingDivider={true}
/>
</Space>
<Space>
<EditButtons
isEditing={objectFormState.isEditing}
handleUpdate={() => {
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}
/>
</Space>
</Flex>
<ScrollBox>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
loading={objectFormState.loading}
ref={actionHandlerRef}
>
<ObjectForm
id={paymentPolicyId}
type='paymentPolicy'
style={{ height: '100%' }}
ref={objectFormRef}
setCurrentObject={true}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => (
<Flex vertical gap={'large'}>
<InfoCollapse
title='Payment Policy Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) =>
updateCollapseState('info', expanded)
}
collapseKey='info'
>
<ObjectInfo
loading={loading}
isEditing={isEditing}
type='paymentPolicy'
objectData={objectData}
labelWidth={210}
visibleProperties={{
marketplaces: false
}}
/>
</InfoCollapse>
<InfoCollapse
title='Marketplaces'
icon={<MarketplaceIcon />}
active={collapseState.marketplaces}
onToggle={(expanded) =>
updateCollapseState('marketplaces', expanded)
}
collapseKey='marketplaces'
>
<ObjectProperty
{...getModelProperty('paymentPolicy', 'marketplaces')}
isEditing={isEditing}
objectData={objectData}
loading={loading}
/>
</InfoCollapse>
</Flex>
)}
</ObjectForm>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Spin
spinning={objectFormState.loading}
indicator={<LoadingOutlined />}
>
<Card>
<NotesPanel _id={paymentPolicyId} type='paymentPolicy' />
</Card>
</Spin>
</InfoCollapse>
<InfoCollapse
title='Audit Logs'
icon={<AuditLogIcon />}
active={collapseState.auditLogs}
onToggle={(expanded) =>
updateCollapseState('auditLogs', expanded)
}
collapseKey='auditLogs'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='auditLog'
masterFilter={{
parent:
getModelByName('paymentPolicy').prefix +
':' +
paymentPolicyId
}}
visibleColumns={{ _id: false, parent: false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
</>
)
}
export default PaymentPolicyInfo

View File

@ -8,7 +8,7 @@ const NewPartStock = ({ onOk, reset, defaultValues }) => {
<NewObjectForm <NewObjectForm
type={'partStock'} type={'partStock'}
reset={reset} reset={reset}
defaultValues={{ state: { type: 'new' }, ...defaultValues }} defaultValues={{ state: { type: 'draft' }, ...defaultValues }}
> >
{({ handleSubmit, submitLoading, objectData, formValid }) => { {({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [ const steps = [
@ -38,7 +38,8 @@ const NewPartStock = ({ onOk, reset, defaultValues }) => {
_id: false, _id: false,
_reference: false, _reference: false,
createdAt: false, createdAt: false,
updatedAt: false updatedAt: false,
postedAt: false
}} }}
isEditing={false} isEditing={false}
objectData={objectData} objectData={objectData}

View File

@ -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 (
<MessageDialogView
title={'Are you sure you want to post this part stock?'}
description={`Posting part stock ${objectData?.name || objectData?._reference || objectData?._id} will finalize it and make it read-only.`}
onOk={handlePost}
okText='Post'
okLoading={postLoading}
/>
)
}
PostPartStock.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default PostPartStock

View File

@ -3,9 +3,9 @@ import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm' import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView' import WizardView from '../../common/WizardView'
const NewProductCategory = ({ onOk }) => { const NewProductCategory = ({ onOk, defaultValues }) => {
return ( return (
<NewObjectForm type={'productCategory'}> <NewObjectForm type={'productCategory'} defaultValues={defaultValues}>
{({ handleSubmit, submitLoading, objectData, formValid }) => { {({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [ const steps = [
{ {
@ -23,6 +23,21 @@ const NewProductCategory = ({ onOk }) => {
/> />
) )
}, },
{
title: 'Optional',
key: 'optional',
content: (
<ObjectInfo
type='productCategory'
column={1}
bordered={false}
isEditing={true}
required={false}
objectData={objectData}
labelWidth={120}
/>
)
},
{ {
title: 'Summary', title: 'Summary',
key: 'summary', key: 'summary',
@ -39,7 +54,7 @@ const NewProductCategory = ({ onOk }) => {
}} }}
isEditing={false} isEditing={false}
objectData={objectData} objectData={objectData}
labelWidth={70} labelWidth={120}
/> />
) )
} }
@ -65,7 +80,8 @@ const NewProductCategory = ({ onOk }) => {
NewProductCategory.propTypes = { NewProductCategory.propTypes = {
onOk: PropTypes.func.isRequired, onOk: PropTypes.func.isRequired,
reset: PropTypes.bool reset: PropTypes.bool,
defaultValues: PropTypes.object
} }
export default NewProductCategory export default NewProductCategory

View File

@ -6,10 +6,12 @@ import useCollapseState from '../../hooks/useCollapseState'
import NotesPanel from '../../common/NotesPanel' import NotesPanel from '../../common/NotesPanel'
import InfoCollapse from '../../common/InfoCollapse' import InfoCollapse from '../../common/InfoCollapse'
import ObjectInfo from '../../common/ObjectInfo' import ObjectInfo from '../../common/ObjectInfo'
import ObjectProperty from '../../common/ObjectProperty.jsx'
import ViewButton from '../../common/ViewButton' import ViewButton from '../../common/ViewButton'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx' import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import MarketplaceIcon from '../../../Icons/MarketplaceIcon.jsx'
import ObjectForm from '../../common/ObjectForm' import ObjectForm from '../../common/ObjectForm'
import EditButtons from '../../common/EditButtons' import EditButtons from '../../common/EditButtons'
import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx'
@ -21,7 +23,7 @@ import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import UserNotifierToggle from '../../common/UserNotifierToggle.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' import ScrollBox from '../../common/ScrollBox.jsx'
const ProductCategoryInfo = () => { const ProductCategoryInfo = () => {
@ -35,6 +37,7 @@ const ProductCategoryInfo = () => {
'ProductCategoryInfo', 'ProductCategoryInfo',
{ {
info: true, info: true,
marketplaces: true,
notes: true, notes: true,
auditLogs: false auditLogs: false
} }
@ -98,6 +101,7 @@ const ProductCategoryInfo = () => {
disabled={objectFormState.loading} disabled={objectFormState.loading}
items={[ items={[
{ key: 'info', label: 'Product Category Information' }, { key: 'info', label: 'Product Category Information' },
{ key: 'marketplaces', label: 'Marketplaces' },
{ key: 'notes', label: 'Notes' }, { key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' } { key: 'auditLogs', label: 'Audit Logs' }
]} ]}
@ -150,33 +154,56 @@ const ProductCategoryInfo = () => {
loading={objectFormState.loading} loading={objectFormState.loading}
ref={actionHandlerRef} ref={actionHandlerRef}
> >
<InfoCollapse <ObjectForm
title='Product Category Information' id={productCategoryId}
icon={<InfoCircleIcon />} type='productCategory'
active={collapseState.info} style={{ height: '100%' }}
onToggle={(expanded) => updateCollapseState('info', expanded)} ref={objectFormRef}
collapseKey='info' setCurrentObject={true}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
> >
<ObjectForm {({ loading, isEditing, objectData }) => (
id={productCategoryId} <Flex vertical gap={'large'}>
type='productCategory' <InfoCollapse
style={{ height: '100%' }} title='Product Category Information'
ref={objectFormRef} icon={<InfoCircleIcon />}
setCurrentObject={true} active={collapseState.info}
onStateChange={(state) => { onToggle={(expanded) =>
setEditFormState((prev) => ({ ...prev, ...state })) updateCollapseState('info', expanded)
}} }
> collapseKey='info'
{({ loading, isEditing, objectData }) => ( >
<ObjectInfo <ObjectInfo
loading={loading} loading={loading}
isEditing={isEditing} isEditing={isEditing}
type='productCategory' type='productCategory'
objectData={objectData} objectData={objectData}
/> visibleProperties={{
)} marketplaces: false
</ObjectForm> }}
</InfoCollapse> />
</InfoCollapse>
<InfoCollapse
title='Marketplaces'
icon={<MarketplaceIcon />}
active={collapseState.marketplaces}
onToggle={(expanded) =>
updateCollapseState('marketplaces', expanded)
}
collapseKey='marketplaces'
>
<ObjectProperty
{...getModelProperty('productCategory', 'marketplaces')}
isEditing={isEditing}
objectData={objectData}
loading={loading}
/>
</InfoCollapse>
</Flex>
)}
</ObjectForm>
</ActionHandler> </ActionHandler>
<InfoCollapse <InfoCollapse
title='Notes' title='Notes'

View File

@ -8,10 +8,12 @@ import useCollapseState from '../../hooks/useCollapseState'
import NotesPanel from '../../common/NotesPanel' import NotesPanel from '../../common/NotesPanel'
import InfoCollapse from '../../common/InfoCollapse' import InfoCollapse from '../../common/InfoCollapse'
import ObjectInfo from '../../common/ObjectInfo' import ObjectInfo from '../../common/ObjectInfo'
import ObjectProperty from '../../common/ObjectProperty.jsx'
import ViewButton from '../../common/ViewButton' import ViewButton from '../../common/ViewButton'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx' import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import MarketplaceIcon from '../../../Icons/MarketplaceIcon.jsx'
import ObjectForm from '../../common/ObjectForm' import ObjectForm from '../../common/ObjectForm'
import EditButtons from '../../common/EditButtons' import EditButtons from '../../common/EditButtons'
import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx'
@ -23,7 +25,7 @@ import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import UserNotifierToggle from '../../common/UserNotifierToggle.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' import ScrollBox from '../../common/ScrollBox.jsx'
const log = loglevel.getLogger('TaxRateInfo') const log = loglevel.getLogger('TaxRateInfo')
@ -36,6 +38,7 @@ const TaxRateInfo = () => {
const taxRateId = new URLSearchParams(location.search).get('taxRateId') const taxRateId = new URLSearchParams(location.search).get('taxRateId')
const [collapseState, updateCollapseState] = useCollapseState('TaxRateInfo', { const [collapseState, updateCollapseState] = useCollapseState('TaxRateInfo', {
info: true, info: true,
marketplaces: true,
notes: true, notes: true,
auditLogs: false auditLogs: false
}) })
@ -98,6 +101,7 @@ const TaxRateInfo = () => {
disabled={objectFormState.loading} disabled={objectFormState.loading}
items={[ items={[
{ key: 'info', label: 'Tax Rate Information' }, { key: 'info', label: 'Tax Rate Information' },
{ key: 'marketplaces', label: 'Marketplaces' },
{ key: 'notes', label: 'Notes' }, { key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' } { key: 'auditLogs', label: 'Audit Logs' }
]} ]}
@ -150,33 +154,56 @@ const TaxRateInfo = () => {
loading={objectFormState.loading} loading={objectFormState.loading}
ref={actionHandlerRef} ref={actionHandlerRef}
> >
<InfoCollapse <ObjectForm
title='Tax Rate Information' id={taxRateId}
icon={<InfoCircleIcon />} type='taxRate'
active={collapseState.info} style={{ height: '100%' }}
onToggle={(expanded) => updateCollapseState('info', expanded)} ref={objectFormRef}
collapseKey='info' setCurrentObject={true}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
> >
<ObjectForm {({ loading, isEditing, objectData }) => (
id={taxRateId} <Flex vertical gap={'large'}>
type='taxRate' <InfoCollapse
style={{ height: '100%' }} title='Tax Rate Information'
ref={objectFormRef} icon={<InfoCircleIcon />}
setCurrentObject={true} active={collapseState.info}
onStateChange={(state) => { onToggle={(expanded) =>
setEditFormState((prev) => ({ ...prev, ...state })) updateCollapseState('info', expanded)
}} }
> collapseKey='info'
{({ loading, isEditing, objectData }) => ( >
<ObjectInfo <ObjectInfo
loading={loading} loading={loading}
isEditing={isEditing} isEditing={isEditing}
type='taxRate' type='taxRate'
objectData={objectData} objectData={objectData}
/> visibleProperties={{
)} marketplaces: false
</ObjectForm> }}
</InfoCollapse> />
</InfoCollapse>
<InfoCollapse
title='Marketplaces'
icon={<MarketplaceIcon />}
active={collapseState.marketplaces}
onToggle={(expanded) =>
updateCollapseState('marketplaces', expanded)
}
collapseKey='marketplaces'
>
<ObjectProperty
{...getModelProperty('taxRate', 'marketplaces')}
isEditing={isEditing}
objectData={objectData}
loading={loading}
/>
</InfoCollapse>
</Flex>
)}
</ObjectForm>
</ActionHandler> </ActionHandler>
<InfoCollapse <InfoCollapse
title='Notes' title='Notes'

View File

@ -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 FulfillmentPolicies = () => {
const tableRef = useRef()
const [viewMode, setViewMode] = useViewMode('fulfillmentPolicy')
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('fulfillmentPolicy')
const [showFilterSidebar, setShowFilterSidebar] =
useFilterSidebarVisibility('FulfillmentPolicies')
const [showSortSidebar, setShowSortSidebar] =
useSortSidebarVisibility('FulfillmentPolicies')
return (
<Flex vertical={'true'} gap='large' className='h-100'>
<Flex justify={'space-between'} style={{ minHeight: 0 }}>
<Space size='small'>
<ObjectActions
type='fulfillmentPolicy'
pageName='list'
onReload={() => tableRef.current?.reload()}
/>
<ColumnViewButton
type='fulfillmentPolicy'
loading={false}
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
<ExportListButton objectType='fulfillmentPolicy' />
</Space>
<Space>
<SortSidebarButton
active={showSortSidebar}
onClick={() => setShowSortSidebar(!showSortSidebar)}
/>
<FilterSidebarButton
active={showFilterSidebar}
onClick={() => setShowFilterSidebar(!showFilterSidebar)}
/>
<ObjectTableViewButton
viewMode={viewMode}
setViewMode={setViewMode}
showEndingDivider={true}
/>
</Space>
</Flex>
<ObjectTable
ref={tableRef}
visibleColumns={columnVisibility}
type='fulfillmentPolicy'
cards={viewMode === 'cards'}
showFilterSidebar={showFilterSidebar}
showSortSidebar={showSortSidebar}
expandHeight={true}
saveFilterInSession={true}
saveFilterInUrl={true}
useFilterInSession={true}
useFilterInUrl={true}
saveSortInSession={true}
saveSortInUrl={true}
useSortInSession={true}
useSortInUrl={true}
/>
</Flex>
)
}
export default FulfillmentPolicies

View File

@ -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(
() => (
<ObjectTableNavigationButtons
disabled={objectFormState.loading || objectFormState.isEditing}
_id={fulfillmentPolicyId}
objectType='fulfillmentPolicy'
showEndingDivider={false}
/>
),
[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 (
<>
<Flex
gap='large'
vertical='true'
style={{ maxHeight: '100%', minHeight: 0 }}
>
<Flex justify={'space-between'}>
<Space size='small'>
<Space size='small'>
<ObjectActions
type='fulfillmentPolicy'
pageName='info'
id={fulfillmentPolicyId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Fulfillment Policy Information' },
{ key: 'marketplaces', label: 'Marketplaces' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<UserNotifierToggle
type='fulfillmentPolicy'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
<DocumentPrintButton
type='fulfillmentPolicy'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<ActivityIndicator
activities={objectFormState.activities}
showStartingDivider={true}
/>
</Space>
<Space>
<EditButtons
isEditing={objectFormState.isEditing}
handleUpdate={() => {
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}
/>
</Space>
</Flex>
<ScrollBox>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
loading={objectFormState.loading}
ref={actionHandlerRef}
>
<ObjectForm
id={fulfillmentPolicyId}
type='fulfillmentPolicy'
style={{ height: '100%' }}
ref={objectFormRef}
setCurrentObject={true}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => (
<Flex vertical gap={'large'}>
<InfoCollapse
title='Fulfillment Policy Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) =>
updateCollapseState('info', expanded)
}
collapseKey='info'
>
<ObjectInfo
loading={loading}
isEditing={isEditing}
labelWidth={175}
type='fulfillmentPolicy'
objectData={objectData}
visibleProperties={{
marketplaces: false
}}
/>
</InfoCollapse>
<InfoCollapse
title='Marketplaces'
icon={<MarketplaceIcon />}
active={collapseState.marketplaces}
onToggle={(expanded) =>
updateCollapseState('marketplaces', expanded)
}
collapseKey='marketplaces'
>
<ObjectProperty
{...getModelProperty(
'fulfillmentPolicy',
'marketplaces'
)}
isEditing={isEditing}
objectData={objectData}
loading={loading}
/>
</InfoCollapse>
</Flex>
)}
</ObjectForm>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Spin
spinning={objectFormState.loading}
indicator={<LoadingOutlined />}
>
<Card>
<NotesPanel
_id={fulfillmentPolicyId}
type='fulfillmentPolicy'
/>
</Card>
</Spin>
</InfoCollapse>
<InfoCollapse
title='Audit Logs'
icon={<AuditLogIcon />}
active={collapseState.auditLogs}
onToggle={(expanded) =>
updateCollapseState('auditLogs', expanded)
}
collapseKey='auditLogs'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='auditLog'
masterFilter={{
parent:
getModelByName('fulfillmentPolicy').prefix +
':' +
fulfillmentPolicyId
}}
visibleColumns={{ _id: false, parent: false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
</>
)
}
export default FulfillmentPolicyInfo

View File

@ -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 (
<NewObjectForm
type={'fulfillmentPolicy'}
defaultValues={{ handlingTime: 1, ...defaultValues }}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='fulfillmentPolicy'
column={1}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
labelWidth={130}
/>
)
},
{
title: 'Optional',
key: 'optional',
content: (
<ObjectInfo
type='fulfillmentPolicy'
column={1}
bordered={false}
isEditing={true}
required={false}
labelWidth={130}
objectData={objectData}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='fulfillmentPolicy'
column={1}
bordered={false}
visibleProperties={{
_id: false,
_reference: false,
createdAt: false,
updatedAt: false
}}
labelWidth={130}
isEditing={false}
objectData={objectData}
/>
)
}
]
return (
<WizardView
steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Fulfillment Policy'
onSubmit={async () => {
const result = await handleSubmit()
if (result) {
onOk()
}
}}
/>
)
}}
</NewObjectForm>
)
}
NewFulfillmentPolicy.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool,
defaultValues: PropTypes.object
}
export default NewFulfillmentPolicy

View File

@ -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 (
<>
<Flex vertical={'true'} gap='large' className='h-100'>
<Flex justify={'space-between'} style={{ minHeight: 0 }}>
<Space size='small'>
<ObjectActions
type='listingVarient'
pageName='list'
onReload={() => tableRef.current?.reload()}
/>
<ColumnViewButton
type='listingVarient'
loading={false}
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
<ExportListButton objectType='listingVarient' />
</Space>
<Space>
<SortSidebarButton
active={showSortSidebar}
onClick={() => setShowSortSidebar(!showSortSidebar)}
/>
<FilterSidebarButton
active={showFilterSidebar}
onClick={() => setShowFilterSidebar(!showFilterSidebar)}
/>
<ObjectTableViewButton
viewMode={viewMode}
setViewMode={setViewMode}
showEndingDivider={true}
/>
</Space>
</Flex>
<ObjectTable
ref={tableRef}
visibleColumns={columnVisibility}
type='listingVarient'
cards={viewMode === 'cards'}
showFilterSidebar={showFilterSidebar}
showSortSidebar={showSortSidebar}
expandHeight={true}
saveFilterInSession={true}
saveFilterInUrl={true}
useFilterInSession={true}
useFilterInUrl={true}
saveSortInSession={true}
saveSortInUrl={true}
useSortInSession={true}
useSortInUrl={true}
/>
</Flex>
</>
)
}
export default ListingVarients

View File

@ -8,8 +8,10 @@ import useCollapseState from '../../hooks/useCollapseState'
import NotesPanel from '../../common/NotesPanel' import NotesPanel from '../../common/NotesPanel'
import InfoCollapse from '../../common/InfoCollapse' import InfoCollapse from '../../common/InfoCollapse'
import ObjectInfo from '../../common/ObjectInfo' import ObjectInfo from '../../common/ObjectInfo'
import ObjectProperty from '../../common/ObjectProperty.jsx'
import ViewButton from '../../common/ViewButton' import ViewButton from '../../common/ViewButton'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import PictureIcon from '../../../Icons/PictureIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx' import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import ObjectForm from '../../common/ObjectForm' import ObjectForm from '../../common/ObjectForm'
@ -25,7 +27,10 @@ import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
import ScrollBox from '../../common/ScrollBox.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') const log = loglevel.getLogger('ListingVarientInfo')
log.setLevel(config.logLevel) log.setLevel(config.logLevel)
@ -49,6 +54,7 @@ const ListingVarientInfo = () => {
'ListingVarientInfo', 'ListingVarientInfo',
{ {
info: true, info: true,
listingImages: true,
notes: true, notes: true,
auditLogs: false auditLogs: false
} }
@ -111,6 +117,7 @@ const ListingVarientInfo = () => {
disabled={objectFormState.loading} disabled={objectFormState.loading}
items={[ items={[
{ key: 'info', label: 'Listing Varient Information' }, { key: 'info', label: 'Listing Varient Information' },
{ key: 'listingImages', label: 'Listing Images' },
{ key: 'notes', label: 'Notes' }, { key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' } { key: 'auditLogs', label: 'Audit Logs' }
]} ]}
@ -189,6 +196,25 @@ const ListingVarientInfo = () => {
isEditing={isEditing} isEditing={isEditing}
type='listingVarient' type='listingVarient'
objectData={objectData} objectData={objectData}
visibleProperties={{ listingImages: false }}
/>
</InfoCollapse>
<InfoCollapse
title='Listing Images'
icon={<PictureIcon />}
active={collapseState.listingImages}
onToggle={(expanded) =>
updateCollapseState('listingImages', expanded)
}
collapseKey='listingImages'
>
<ObjectProperty
{...getModelProperty('listingVarient', 'listingImages')}
isEditing={isEditing}
objectData={objectData}
loading={loading}
size='medium'
showCard={false}
/> />
</InfoCollapse> </InfoCollapse>
</Flex> </Flex>

View File

@ -37,6 +37,7 @@ const NewListingVarient = ({ onOk, defaultValues }) => {
isEditing={true} isEditing={true}
required={false} required={false}
objectData={objectData} objectData={objectData}
visibleProperties={{ listingImages: false }}
/> />
) )
}, },
@ -54,7 +55,8 @@ const NewListingVarient = ({ onOk, defaultValues }) => {
createdAt: false, createdAt: false,
updatedAt: false, updatedAt: false,
lastSyncedAt: false, lastSyncedAt: false,
stockQuantity: false stockQuantity: false,
listingImages: false
}} }}
isEditing={false} isEditing={false}
objectData={objectData} objectData={objectData}

View File

@ -17,7 +17,7 @@ const PublishListingVarient = ({ onOk, objectData }) => {
'publish' 'publish'
) )
if (result) { if (result) {
message.success('Published successfully') message.success('Publishing started...')
onOk(result) onOk(result)
} }
} catch (error) { } catch (error) {

View File

@ -17,7 +17,7 @@ const UnpublishListingVarient = ({ onOk, objectData }) => {
'unpublish' 'unpublish'
) )
if (result) { if (result) {
message.success('Unpublished successfully') message.success('Unpublishing started...')
onOk(result) onOk(result)
} }
} catch (error) { } catch (error) {
@ -35,7 +35,7 @@ const UnpublishListingVarient = ({ onOk, objectData }) => {
return ( return (
<MessageDialogView <MessageDialogView
title="Withdraw this variant from the marketplace?" title='Withdraw this variant from the marketplace?'
description={`Ends the live marketplace listing for this SKU where applicable (eBay).${ description={`Ends the live marketplace listing for this SKU where applicable (eBay).${
ref ? ` (variant: ${ref})` : '' ref ? ` (variant: ${ref})` : ''
}`} }`}

View File

@ -12,6 +12,7 @@ import ObjectProperty from '../../common/ObjectProperty.jsx'
import ViewButton from '../../common/ViewButton' import ViewButton from '../../common/ViewButton'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import TextIcon from '../../../Icons/TextIcon.jsx' import TextIcon from '../../../Icons/TextIcon.jsx'
import PictureIcon from '../../../Icons/PictureIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx' import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import ListingVarientIcon from '../../../Icons/ListingVarientIcon.jsx' import ListingVarientIcon from '../../../Icons/ListingVarientIcon.jsx'
@ -27,7 +28,10 @@ import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
import { getModelByName, getModelProperty } from '../../../../database/ObjectModels.js' import {
getModelByName,
getModelProperty
} from '../../../../database/ObjectModels.js'
import ScrollBox from '../../common/ScrollBox.jsx' import ScrollBox from '../../common/ScrollBox.jsx'
const log = loglevel.getLogger('ListingInfo') const log = loglevel.getLogger('ListingInfo')
@ -49,6 +53,7 @@ const ListingInfo = () => {
const listingId = new URLSearchParams(location.search).get('listingId') const listingId = new URLSearchParams(location.search).get('listingId')
const [collapseState, updateCollapseState] = useCollapseState('ListingInfo', { const [collapseState, updateCollapseState] = useCollapseState('ListingInfo', {
info: true, info: true,
listingImages: true,
description: true, description: true,
listingVarients: true, listingVarients: true,
notes: true, notes: true,
@ -89,7 +94,7 @@ const ListingInfo = () => {
finishEdit: () => { finishEdit: () => {
objectFormRef?.current?.handleUpdate?.() objectFormRef?.current?.handleUpdate?.()
return true return true
}, }
} }
return ( return (
@ -112,6 +117,7 @@ const ListingInfo = () => {
disabled={objectFormState.loading} disabled={objectFormState.loading}
items={[ items={[
{ key: 'info', label: 'Listing Information' }, { key: 'info', label: 'Listing Information' },
{ key: 'listingImages', label: 'Listing Images' },
{ key: 'description', label: 'Listing Description' }, { key: 'description', label: 'Listing Description' },
{ key: 'listingVarients', label: 'Listing Varients' }, { key: 'listingVarients', label: 'Listing Varients' },
{ key: 'notes', label: 'Notes' }, { key: 'notes', label: 'Notes' },
@ -191,11 +197,35 @@ const ListingInfo = () => {
loading={loading} loading={loading}
isEditing={isEditing} isEditing={isEditing}
type='listing' type='listing'
labelWidth={165} labelWidth={180}
objectData={objectData} objectData={objectData}
visibleProperties={{ description: false }} visibleProperties={{
description: false,
listingImages: false
}}
/> />
</InfoCollapse> </InfoCollapse>
<InfoCollapse
title='Listing Images'
icon={<PictureIcon />}
active={collapseState.listingImages}
onToggle={(expanded) =>
updateCollapseState('listingImages', expanded)
}
collapseKey='listingImages'
>
<Spin spinning={loading} indicator={<LoadingOutlined />}>
<ObjectProperty
{...getModelProperty('listing', 'listingImages')}
isEditing={isEditing}
objectData={objectData}
loading={loading}
size='medium'
showCard={false}
/>
</Spin>
</InfoCollapse>
<InfoCollapse <InfoCollapse
title='Listing Description' title='Listing Description'
icon={<TextIcon />} icon={<TextIcon />}
@ -205,16 +235,18 @@ const ListingInfo = () => {
} }
collapseKey='description' collapseKey='description'
> >
<Card> <Spin spinning={loading} indicator={<LoadingOutlined />}>
<ObjectProperty <Card>
{...getModelProperty('listing', 'description')} <ObjectProperty
isEditing={isEditing} {...getModelProperty('listing', 'description')}
objectData={objectData} isEditing={isEditing}
loading={loading} objectData={objectData}
size='medium' loading={loading}
showCard={false} size='medium'
/> showCard={false}
</Card> />
</Card>
</Spin>
</InfoCollapse> </InfoCollapse>
<InfoCollapse <InfoCollapse
title='Listing Varients' title='Listing Varients'
@ -228,7 +260,8 @@ const ListingInfo = () => {
<ObjectTable <ObjectTable
type='listingVarient' type='listingVarient'
masterFilter={{ masterFilter={{
listing: getModelByName('listing').prefix + ':' + listingId listing:
getModelByName('listing').prefix + ':' + listingId
}} }}
visibleColumns={{ listing: false }} visibleColumns={{ listing: false }}
ref={listingVarientsTableRef} ref={listingVarientsTableRef}
@ -250,7 +283,7 @@ const ListingInfo = () => {
indicator={<LoadingOutlined />} indicator={<LoadingOutlined />}
> >
<Card> <Card>
<NotesPanel _id={listingId} type='listing' /> <NotesPanel _id={listingId} type='listing' />
</Card> </Card>
</Spin> </Spin>
</InfoCollapse> </InfoCollapse>
@ -267,10 +300,7 @@ const ListingInfo = () => {
<ObjectTable <ObjectTable
type='auditLog' type='auditLog'
masterFilter={{ masterFilter={{
parent: parent: getModelByName('listing').prefix + ':' + listingId
getModelByName('listing').prefix +
':' +
listingId
}} }}
visibleColumns={{ _id: false, parent: false }} visibleColumns={{ _id: false, parent: false }}
/> />

View File

@ -37,6 +37,7 @@ const NewListing = ({ onOk, defaultValues }) => {
isEditing={true} isEditing={true}
required={false} required={false}
objectData={objectData} objectData={objectData}
visibleProperties={{ listingImages: false }}
/> />
) )
}, },
@ -54,7 +55,8 @@ const NewListing = ({ onOk, defaultValues }) => {
createdAt: false, createdAt: false,
updatedAt: false, updatedAt: false,
lastSyncedAt: false, lastSyncedAt: false,
stockQuantity: false stockQuantity: false,
listingImages: false
}} }}
isEditing={false} isEditing={false}
objectData={objectData} objectData={objectData}

View File

@ -11,9 +11,13 @@ const PublishListing = ({ onOk, objectData }) => {
const handlePublish = async () => { const handlePublish = async () => {
setLoading(true) setLoading(true)
try { try {
const result = await sendObjectFunction(objectData._id, 'Listing', 'publish') const result = await sendObjectFunction(
objectData._id,
'Listing',
'publish'
)
if (result) { if (result) {
message.success('Published successfully') message.success('Publishing started...')
onOk(result) onOk(result)
} }
} catch (error) { } catch (error) {

View File

@ -11,9 +11,13 @@ const UnpublishListing = ({ onOk, objectData }) => {
const handleUnpublish = async () => { const handleUnpublish = async () => {
setLoading(true) setLoading(true)
try { try {
const result = await sendObjectFunction(objectData._id, 'Listing', 'unpublish') const result = await sendObjectFunction(
objectData._id,
'Listing',
'unpublish'
)
if (result) { if (result) {
message.success('Unpublished successfully') message.success('Unpublishing started...')
onOk(result) onOk(result)
} }
} catch (error) { } catch (error) {

View File

@ -253,6 +253,10 @@ const MarketplaceInfo = () => {
isEditing={isEditing} isEditing={isEditing}
type='marketplace' type='marketplace'
labelWidth={215} labelWidth={215}
visibleProperties={{
'eBay.categoryReferences': false,
'eBay.availableShippingServices': false
}}
objectData={objectData} objectData={objectData}
/> />
)} )}
@ -271,7 +275,7 @@ const MarketplaceInfo = () => {
indicator={<LoadingOutlined />} indicator={<LoadingOutlined />}
> >
<Card> <Card>
<NotesPanel _id={marketplaceId} type='marketplace' /> <NotesPanel _id={marketplaceId} type='marketplace' />
</Card> </Card>
</Spin> </Spin>
</InfoCollapse> </InfoCollapse>
@ -288,10 +292,8 @@ const MarketplaceInfo = () => {
<ObjectTable <ObjectTable
type='auditLog' type='auditLog'
masterFilter={{ masterFilter={{
parent: parent:
getModelByName('marketplace').prefix + getModelByName('marketplace').prefix + ':' + marketplaceId
':' +
marketplaceId
}} }}
visibleColumns={{ _id: false, parent: false }} visibleColumns={{ _id: false, parent: false }}
/> />

View File

@ -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 (
<MessageDialogView
title={title}
description={description}
onOk={handleSync}
okText='Sync'
okLoading={syncLoading}
/>
)
}
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

View File

@ -0,0 +1,20 @@
import PropTypes from 'prop-types'
import SyncAccountPolicies from './SyncAccountPolicies.jsx'
const SyncFulfillmentPolicies = ({ onOk, objectData }) => (
<SyncAccountPolicies
onOk={onOk}
objectData={objectData}
functionName='sync/fulfillmentPolicies'
title='Sync fulfillment policies from marketplace?'
description={`This will import fulfillment policies from ${objectData?.name || objectData?._reference || objectData?._id}.`}
successMessage='Fulfillment policies synced successfully'
/>
)
SyncFulfillmentPolicies.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default SyncFulfillmentPolicies

View File

@ -30,7 +30,7 @@ const SyncMarketplace = ({ onOk, objectData }) => {
return ( return (
<MessageDialogView <MessageDialogView
title='Sync marketplace?' title='Sync marketplace?'
description={`This will refresh marketplace metadata from ${objectData?.name || objectData?._reference || objectData?._id}, including available shipping services.`} description={`This will refresh marketplace metadata from ${objectData?.name || objectData?._reference || objectData?._id}, including shipping services, categories, business policies, and tax tables.`}
onOk={handleSync} onOk={handleSync}
okText='Sync' okText='Sync'
okLoading={syncLoading} okLoading={syncLoading}

View File

@ -0,0 +1,20 @@
import PropTypes from 'prop-types'
import SyncAccountPolicies from './SyncAccountPolicies.jsx'
const SyncPaymentPolicies = ({ onOk, objectData }) => (
<SyncAccountPolicies
onOk={onOk}
objectData={objectData}
functionName='sync/paymentPolicies'
title='Sync payment policies from marketplace?'
description={`This will import payment policies from ${objectData?.name || objectData?._reference || objectData?._id}.`}
successMessage='Payment policies synced successfully'
/>
)
SyncPaymentPolicies.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default SyncPaymentPolicies

View File

@ -0,0 +1,20 @@
import PropTypes from 'prop-types'
import SyncAccountPolicies from './SyncAccountPolicies.jsx'
const SyncReturnPolicies = ({ onOk, objectData }) => (
<SyncAccountPolicies
onOk={onOk}
objectData={objectData}
functionName='sync/returnPolicies'
title='Sync return policies from marketplace?'
description={`This will import return policies from ${objectData?.name || objectData?._reference || objectData?._id}.`}
successMessage='Return policies synced successfully'
/>
)
SyncReturnPolicies.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default SyncReturnPolicies

View File

@ -0,0 +1,20 @@
import PropTypes from 'prop-types'
import SyncAccountPolicies from './SyncAccountPolicies.jsx'
const SyncTaxRates = ({ onOk, objectData }) => (
<SyncAccountPolicies
onOk={onOk}
objectData={objectData}
functionName='sync/taxRates'
title='Sync tax rates from marketplace?'
description={`This will import tax tables from ${objectData?.name || objectData?._reference || objectData?._id} and push matching local tax rates.`}
successMessage='Tax rates synced successfully'
/>
)
SyncTaxRates.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default SyncTaxRates

View File

@ -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 (
<Flex vertical={'true'} gap='large' className='h-100'>
<Flex justify={'space-between'} style={{ minHeight: 0 }}>
<Space size='small'>
<ObjectActions
type='returnPolicy'
pageName='list'
onReload={() => tableRef.current?.reload()}
/>
<ColumnViewButton
type='returnPolicy'
loading={false}
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
<ExportListButton objectType='returnPolicy' />
</Space>
<Space>
<SortSidebarButton
active={showSortSidebar}
onClick={() => setShowSortSidebar(!showSortSidebar)}
/>
<FilterSidebarButton
active={showFilterSidebar}
onClick={() => setShowFilterSidebar(!showFilterSidebar)}
/>
<ObjectTableViewButton
viewMode={viewMode}
setViewMode={setViewMode}
showEndingDivider={true}
/>
</Space>
</Flex>
<ObjectTable
ref={tableRef}
visibleColumns={columnVisibility}
type='returnPolicy'
cards={viewMode === 'cards'}
showFilterSidebar={showFilterSidebar}
showSortSidebar={showSortSidebar}
expandHeight={true}
saveFilterInSession={true}
saveFilterInUrl={true}
useFilterInSession={true}
useFilterInUrl={true}
saveSortInSession={true}
saveSortInUrl={true}
useSortInSession={true}
useSortInUrl={true}
/>
</Flex>
)
}
export default ReturnPolicies

View File

@ -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 (
<NewObjectForm
type={'returnPolicy'}
defaultValues={{
returnsAccepted: true,
returnPeriodDays: 30,
returnShippingCostPayer: 'buyer',
refundMethod: 'moneyBack',
...defaultValues
}}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='returnPolicy'
column={1}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
labelWidth={160}
/>
)
},
{
title: 'Optional',
key: 'optional',
content: (
<ObjectInfo
type='returnPolicy'
column={1}
bordered={false}
isEditing={true}
required={false}
labelWidth={160}
objectData={objectData}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='returnPolicy'
column={1}
bordered={false}
visibleProperties={{
_id: false,
_reference: false,
createdAt: false,
updatedAt: false
}}
labelWidth={160}
isEditing={false}
objectData={objectData}
/>
)
}
]
return (
<WizardView
steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Return Policy'
onSubmit={async () => {
const result = await handleSubmit()
if (result) {
onOk()
}
}}
/>
)
}}
</NewObjectForm>
)
}
NewReturnPolicy.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool,
defaultValues: PropTypes.object
}
export default NewReturnPolicy

View File

@ -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(
() => (
<ObjectTableNavigationButtons
disabled={objectFormState.loading || objectFormState.isEditing}
_id={returnPolicyId}
objectType='returnPolicy'
showEndingDivider={false}
/>
),
[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 (
<>
<Flex
gap='large'
vertical='true'
style={{ maxHeight: '100%', minHeight: 0 }}
>
<Flex justify={'space-between'}>
<Space size='small'>
<Space size='small'>
<ObjectActions
type='returnPolicy'
pageName='info'
id={returnPolicyId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Return Policy Information' },
{ key: 'marketplaces', label: 'Marketplaces' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<UserNotifierToggle
type='returnPolicy'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
<DocumentPrintButton
type='returnPolicy'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<ActivityIndicator
activities={objectFormState.activities}
showStartingDivider={true}
/>
</Space>
<Space>
<EditButtons
isEditing={objectFormState.isEditing}
handleUpdate={() => {
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}
/>
</Space>
</Flex>
<ScrollBox>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
loading={objectFormState.loading}
ref={actionHandlerRef}
>
<ObjectForm
id={returnPolicyId}
type='returnPolicy'
style={{ height: '100%' }}
ref={objectFormRef}
setCurrentObject={true}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => (
<Flex vertical gap={'large'}>
<InfoCollapse
title='Return Policy Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) =>
updateCollapseState('info', expanded)
}
collapseKey='info'
>
<ObjectInfo
loading={loading}
isEditing={isEditing}
type='returnPolicy'
objectData={objectData}
labelWidth={225}
visibleProperties={{
marketplaces: false
}}
/>
</InfoCollapse>
<InfoCollapse
title='Marketplaces'
icon={<MarketplaceIcon />}
active={collapseState.marketplaces}
onToggle={(expanded) =>
updateCollapseState('marketplaces', expanded)
}
collapseKey='marketplaces'
>
<ObjectProperty
{...getModelProperty('returnPolicy', 'marketplaces')}
isEditing={isEditing}
objectData={objectData}
loading={loading}
/>
</InfoCollapse>
</Flex>
)}
</ObjectForm>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Spin
spinning={objectFormState.loading}
indicator={<LoadingOutlined />}
>
<Card>
<NotesPanel _id={returnPolicyId} type='returnPolicy' />
</Card>
</Spin>
</InfoCollapse>
<InfoCollapse
title='Audit Logs'
icon={<AuditLogIcon />}
active={collapseState.auditLogs}
onToggle={(expanded) =>
updateCollapseState('auditLogs', expanded)
}
collapseKey='auditLogs'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='auditLog'
masterFilter={{
parent:
getModelByName('returnPolicy').prefix +
':' +
returnPolicyId
}}
visibleColumns={{ _id: false, parent: false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
</>
)
}
export default ReturnPolicyInfo

View File

@ -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 (
<TreeSelect
placeholder={placeholder}
disabled={disabled}
treeData={treeData}
value={value}
onChange={onChange}
showSearch
allowClear
treeNodeFilterProp='title'
treeLine
listHeight={400}
popupMatchSelectWidth={false}
style={{ width: '100%' }}
{...rest}
/>
)
}
CustomTreeSelect.propTypes = {
placeholder: PropTypes.string,
disabled: PropTypes.bool,
options: PropTypes.array,
value: PropTypes.any,
onChange: PropTypes.func
}
export default CustomTreeSelect

View File

@ -4,7 +4,7 @@ import { Link, useLocation, useNavigate } from 'react-router-dom'
import ArrowLeftIcon from '../../Icons/ArrowLeftIcon' import ArrowLeftIcon from '../../Icons/ArrowLeftIcon'
import ArrowRightIcon from '../../Icons/ArrowRightIcon' import ArrowRightIcon from '../../Icons/ArrowRightIcon'
import FilterIcon from '../../Icons/FilterIcon' import FilterIcon from '../../Icons/FilterIcon'
import { getModelByName } from '../../../database/ObjectModels' import { getModelByPluralName } from '../../../database/ObjectModels'
import { useTableState } from '../context/TableStateContext' import { useTableState } from '../context/TableStateContext'
const breadcrumbNameMap = { const breadcrumbNameMap = {
@ -19,6 +19,7 @@ const breadcrumbNameMap = {
design: 'Design', design: 'Design',
control: 'Control', control: 'Control',
preview: 'Preview', preview: 'Preview',
settings: 'Settings',
about: 'About' about: 'About'
} }
@ -32,13 +33,8 @@ const DashboardBreadcrumb = () => {
function segmentToModel(segment) { function segmentToModel(segment) {
if (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 // Otherwise, get the model as is
return getModelByName(segment, true) return getModelByPluralName(segment, true)
} }
return null return null
} }
@ -48,7 +44,7 @@ const DashboardBreadcrumb = () => {
if (segment !== 'dashboard') { if (segment !== 'dashboard') {
const isMainSection = mainSections.includes(segment) const isMainSection = mainSections.includes(segment)
const model = segmentToModel(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 name = breadcrumbNameMap[segment] || modelLabelPlural || segment
const showFilterIcon = const showFilterIcon =
hasPageFilter(url) || hasPageFilter(url) ||

View File

@ -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 (
<Card
className='file-gallery-list'
styles={{
body: {
paddingTop: GALLERY_PADDING,
paddingBottom: GALLERY_PADDING,
paddingLeft: 0,
paddingRight: 0
}
}}
>
{filesToRender.length > 0 ? (
<div
className={`file-gallery-list-preview${isMobile ? ' is-mobile' : ''}`}
ref={previewRef}
style={
slideHeight
? {
height: slideHeight,
'--file-gallery-side-padding': `${activePadding}px`
}
: undefined
}
>
<Button
className='file-gallery-list-nav file-gallery-list-nav-left'
icon={<ChevronLeftIcon />}
disabled={activeIndex <= 0}
onClick={() => scrollToIndex(activeIndex - 1)}
aria-label='Previous image'
/>
<ScrollBox
className='file-gallery-list-scroll'
horizontalBottomPadding={0}
verticalRightPadding={0}
scrollableNodeProps={{
ref: (node) => {
scrollElRef.current = node
}
}}
>
<div
className='file-gallery-list-track'
style={slideHeight ? { height: slideHeight } : undefined}
>
{filesToRender.map((file, index) => {
const layout = slideLayouts[index] || {
padding: MIN_SIDE_PADDING,
width: 0,
height: 0
}
return (
<div
key={file._id || file.id || index}
className='file-gallery-list-slide'
ref={(node) => {
slideRefs.current[index] = node
}}
style={{
flex: `0 0 ${containerWidth || 0}px`,
width: containerWidth || undefined,
paddingLeft: layout.padding,
paddingRight: layout.padding
}}
>
<div
className='file-gallery-list-preview-frame'
style={
layout.width && layout.height
? {
width: layout.width,
height: layout.height
}
: undefined
}
>
<FilePreview
file={file}
style={{
width: '100%',
height: '100%',
objectFit: 'contain',
borderRadius: 8
}}
/>
</div>
</div>
)
})}
</div>
</ScrollBox>
<Button
className='file-gallery-list-nav file-gallery-list-nav-right'
icon={<ChevronRightIcon />}
disabled={activeIndex >= filesToRender.length - 1}
onClick={() => scrollToIndex(activeIndex + 1)}
aria-label='Next image'
/>
</div>
) : null}
<div className='file-gallery-footer'>
{filesToRender.length > 0 ? (
<Divider style={{ margin: '24px 0' }} />
) : null}
<Flex
className='file-gallery-list-thumbs'
gap={GALLERY_PADDING}
align='center'
justify='center'
wrap='nowrap'
>
{filesToRender.map((file, index) => (
<div
key={file._id || file.id || index}
className={`file-gallery-list-thumb${
index === activeIndex ? ' is-active' : ''
}`}
onClick={() => 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}
>
<Thumbnail
file={file}
size={64}
alt={file.name || file.filename || ''}
/>
{editing ? (
<Card
size='small'
className='file-gallery-list-thumb-delete'
styles={{ body: { padding: 2 } }}
>
<Button
type='text'
size='small'
icon={<BinIcon />}
onClick={(event) => {
event.stopPropagation()
handleRemove(file)
}}
aria-label='Remove image'
/>
</Card>
) : null}
</div>
))}
{editing && uploadSlot ? (
<div className='file-gallery-list-upload'>{uploadSlot}</div>
) : null}
</Flex>
</div>
</Card>
)
}
FileGalleryList.propTypes = {
files: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
editing: PropTypes.bool,
onRemove: PropTypes.func,
uploadSlot: PropTypes.node
}
export default FileGalleryList

View File

@ -7,11 +7,13 @@ import DownloadIcon from '../../Icons/DownloadIcon'
import { useContext, useState } from 'react' import { useContext, useState } from 'react'
import { ApiServerContext } from '../context/ApiServerContext' import { ApiServerContext } from '../context/ApiServerContext'
import FilePreview from './FilePreview' import FilePreview from './FilePreview'
import FileGalleryList from './FileGalleryList'
import EyeSlashIcon from '../../Icons/EyeSlashIcon' import EyeSlashIcon from '../../Icons/EyeSlashIcon'
import { getModelByName } from '../../../database/ObjectModels' import { getModelByName } from '../../../database/ObjectModels'
import InfoCircleIcon from '../../Icons/InfoCircleIcon' import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import ElipsisText from './ElipsisText' import ElipsisText from './ElipsisText'
import MissingPlaceholder from './MissingPlaceholder'
const FileList = ({ const FileList = ({
files, files,
@ -24,6 +26,8 @@ const FileList = ({
defaultPreviewOpen = false, defaultPreviewOpen = false,
minimal = false, minimal = false,
card = true, card = true,
gallery = false,
uploadSlot = null,
maxWidth = '100%' maxWidth = '100%'
}) => { }) => {
const { fetchFileContent, flushFile } = useContext(ApiServerContext) const { fetchFileContent, flushFile } = useContext(ApiServerContext)
@ -37,10 +41,6 @@ const FileList = ({
? !files || !Array.isArray(files) || files.length === 0 ? !files || !Array.isArray(files) || files.length === 0
: !files : !files
if (hasNoItems) {
return null
}
const handleRemove = (fileToRemove) => { const handleRemove = (fileToRemove) => {
flushFile(fileToRemove._id) flushFile(fileToRemove._id)
if (multiple) { if (multiple) {
@ -60,7 +60,41 @@ const FileList = ({
await fetchFileContent(file, true) 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 <MissingPlaceholder message='No listing images.' />
}
return (
<div style={rootStyle}>
<FileGalleryList
files={filesToRender}
editing={editing}
uploadSlot={uploadSlot}
onRemove={handleRemove}
/>
</div>
)
}
if (hasNoItems && !(gallery && (editing || uploadSlot))) {
return null
}
const renderFileContent = (file) => ( const renderFileContent = (file) => (
<Flex <Flex
@ -151,14 +185,6 @@ const FileList = ({
</Flex> </Flex>
) )
const rootStyle = {
minWidth: 0,
maxWidth,
...(card
? { width: '100%' }
: { width: 'fit-content', alignSelf: 'flex-start' })
}
return ( return (
<div style={rootStyle}> <div style={rootStyle}>
{filesToRender.map((file, index) => { {filesToRender.map((file, index) => {
@ -205,6 +231,8 @@ FileList.propTypes = {
showDownload: PropTypes.bool, showDownload: PropTypes.bool,
defaultPreviewOpen: PropTypes.bool, defaultPreviewOpen: PropTypes.bool,
card: PropTypes.bool, card: PropTypes.bool,
gallery: PropTypes.bool,
uploadSlot: PropTypes.node,
minimal: PropTypes.bool, minimal: PropTypes.bool,
maxWidth: PropTypes.string maxWidth: PropTypes.string
} }

View File

@ -1,12 +1,22 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { ApiServerContext } from '../context/ApiServerContext' 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 LoadingPlaceholder from './LoadingPlaceholder'
import GCodePreview from './GCodePreview' import GCodePreview from './GCodePreview'
import ThreeDPreview from './ThreeDPreview' import ThreeDPreview from './ThreeDPreview'
import { AuthContext } from '../context/AuthContext' import { AuthContext } from '../context/AuthContext'
const FilePreview = ({ file, style = {} }) => { const FilePreview = ({ file, style = {} }) => {
useEffect(() => {
console.log('FILEPREVIEWFILE', file)
}, [file])
const { token } = useContext(AuthContext) const { token } = useContext(AuthContext)
const { fetchFileContent } = useContext(ApiServerContext) const { fetchFileContent } = useContext(ApiServerContext)
@ -14,6 +24,8 @@ const FilePreview = ({ file, style = {} }) => {
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState(null) const [error, setError] = useState(null)
const currentId = useRef(null)
const fetchPreview = useCallback(async () => { const fetchPreview = useCallback(async () => {
if (error != null) { if (error != null) {
return return
@ -31,7 +43,8 @@ const FilePreview = ({ file, style = {} }) => {
}, [file, fetchFileContent, error]) }, [file, fetchFileContent, error])
useEffect(() => { useEffect(() => {
if (file?.type && token != null) { if (file?.type && token != null && file._id !== currentId.current) {
currentId.current = file._id
fetchPreview() fetchPreview()
} }
}, [file._id, file?.type, fetchPreview, token]) }, [file._id, file?.type, fetchPreview, token])

View File

@ -28,6 +28,62 @@ const getFileIdentity = (value, multiple) => {
return value?._id || value || '' 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 (
<Upload.Dragger
beforeUpload={handleFileUpload}
showUploadList={false}
multiple={multiple}
accept={getMasterFilterAccept(masterFilter)}
className='file-gallery-list-dropzone'
openFileDialogOnClick
>
{uploading ? (
uploadProgress > 0 && uploadProgress < 100 ? (
<Text style={{ fontSize: 11 }}>{uploadProgress}%</Text>
) : (
<LoadingOutlined />
)
) : (
<PlusIcon />
)}
</Upload.Dragger>
)
}
FileUploadDropzone.propTypes = {
onUploaded: PropTypes.func,
multiple: PropTypes.bool,
masterFilter: PropTypes.array
}
const FileUpload = ({ const FileUpload = ({
value, value,
onChange, onChange,
@ -36,6 +92,7 @@ const FileUpload = ({
defaultPreviewOpen = false, defaultPreviewOpen = false,
showPreview = true, showPreview = true,
showInfo, showInfo,
gallery = false,
masterFilter = [] masterFilter = []
}) => { }) => {
const { uploadFile } = useContext(ApiServerContext) const { uploadFile } = useContext(ApiServerContext)
@ -135,6 +192,37 @@ const FileUpload = ({
) )
} }
if (gallery) {
return (
<FileList
files={currentFiles}
multiple={multiple}
editing={true}
gallery={true}
showInfo={showInfo || false}
showPreview={showPreview}
defaultPreviewOpen={defaultPreviewOpen}
uploadSlot={
<FileUploadDropzone
multiple={multiple}
masterFilter={masterFilter}
onUploaded={(uploadedFile) => {
if (multiple) {
const existing = Array.isArray(currentFiles) ? currentFiles : []
updateCurrentFiles([...existing, uploadedFile])
} else {
updateCurrentFiles(uploadedFile)
}
}}
/>
}
onChange={(updatedFiles) => {
updateCurrentFiles(updatedFiles)
}}
/>
)
}
return ( return (
<Flex gap={'small'} vertical> <Flex gap={'small'} vertical>
{hasNoItems && uploading == false ? ( {hasNoItems && uploading == false ? (
@ -220,6 +308,7 @@ FileUpload.propTypes = {
showInfo: PropTypes.bool, showInfo: PropTypes.bool,
defaultPreviewOpen: PropTypes.bool, defaultPreviewOpen: PropTypes.bool,
minimal: PropTypes.bool, minimal: PropTypes.bool,
gallery: PropTypes.bool,
masterFilter: PropTypes.array masterFilter: PropTypes.array
} }

View File

@ -783,19 +783,16 @@ const ObjectForm = forwardRef(
isEditingRef.current = false isEditingRef.current = false
onStateChangeRef.current({ isEditing: isEditingRef.current }) onStateChangeRef.current({ isEditing: isEditingRef.current })
serverObjectData.current = updatedObject const mergedObject = mergeFormData(updatedObject, currentFormData)
const computedEntries = calculateComputedValues( serverObjectData.current = mergedObject
updatedObject, const computedEntries = calculateComputedValues(mergedObject, model, {
model, skipObjectChildrenValue: true
{ })
skipObjectChildrenValue: true
}
)
const nextObjectData = applyComputedEntries( const nextObjectData = applyComputedEntries(
updatedObject, mergedObject,
computedEntries computedEntries
) )
copyObjectChildren(nextObjectData, updatedObject, model) copyObjectChildren(nextObjectData, mergedObject, model)
setObjectData({ setObjectData({
...nextObjectData, ...nextObjectData,
_isEditing: false _isEditing: false

View File

@ -6,7 +6,8 @@ import {
InputNumber, InputNumber,
Form, Form,
Select, Select,
Switch Switch,
Tree
} from 'antd' } from 'antd'
import IdDisplay from './IdDisplay' import IdDisplay from './IdDisplay'
import TimeDisplay from './TimeDisplay' import TimeDisplay from './TimeDisplay'
@ -41,6 +42,7 @@ import ObjectTypeSelect from './ObjectTypeSelect'
import ObjectTypeDisplay from './ObjectTypeDisplay' import ObjectTypeDisplay from './ObjectTypeDisplay'
import CodeBlockEditor from './CodeBlockEditor' import CodeBlockEditor from './CodeBlockEditor'
import CustomSelect from './CustomSelect' import CustomSelect from './CustomSelect'
import CustomTreeSelect, { findTreeLabel } from './CustomTreeSelect'
import StateDisplay from './StateDisplay' import StateDisplay from './StateDisplay'
import AlertsDisplay from './AlertsDisplay' import AlertsDisplay from './AlertsDisplay'
import FileUpload from './FileUpload' import FileUpload from './FileUpload'
@ -136,6 +138,7 @@ const ObjectProperty = ({
hiddenPropertyWidth = '100px', hiddenPropertyWidth = '100px',
inherit = true, inherit = true,
scrollHeight, scrollHeight,
gallery = false,
...rest ...rest
}) => { }) => {
if (typeof value === 'function') { if (typeof value === 'function') {
@ -270,6 +273,28 @@ const ObjectProperty = ({
) )
} }
} }
case 'treeSelect': {
if (Array.isArray(value) && value.length) {
return (
<Tree
treeData={value}
fieldNames={{ title: 'title', key: 'value', children: 'children' }}
height={320}
virtual
defaultExpandAll={false}
/>
)
}
const treeLabel = findTreeLabel(options, value)
if (treeLabel || (value != null && value !== '')) {
return <Text {...textParams}>{treeLabel || String(value)}</Text>
}
return (
<Text type='secondary' {...textParams}>
n/a
</Text>
)
}
case 'priceMode': case 'priceMode':
switch (value) { switch (value) {
case 'margin': case 'margin':
@ -655,6 +680,8 @@ const ObjectProperty = ({
showPreview={showPreview} showPreview={showPreview}
showInfo={showHyperlink} showInfo={showHyperlink}
showDownload={showDownload} showDownload={showDownload}
gallery={gallery}
editing={false}
masterFilter={masterFilter} masterFilter={masterFilter}
/> />
) )
@ -670,6 +697,8 @@ const ObjectProperty = ({
defaultPreviewOpen={previewOpen} defaultPreviewOpen={previewOpen}
showPreview={showPreview} showPreview={showPreview}
showInfo={showHyperlink} showInfo={showHyperlink}
gallery={gallery}
editing={false}
masterFilter={masterFilter} masterFilter={masterFilter}
/> />
) )
@ -838,6 +867,14 @@ const ObjectProperty = ({
{...inputProps} {...inputProps}
/> />
) )
case 'treeSelect':
return (
<CustomTreeSelect
placeholder={'Select a ' + label.toLowerCase() + '...'}
options={Array.isArray(options) ? options : []}
{...inputProps}
/>
)
case 'state': case 'state':
if (options?.length && !readOnly) { if (options?.length && !readOnly) {
return ( return (
@ -1002,6 +1039,7 @@ const ObjectProperty = ({
defaultPreviewOpen={previewOpen} defaultPreviewOpen={previewOpen}
showPreview={showPreview} showPreview={showPreview}
showInfo={showHyperlink} showInfo={showHyperlink}
gallery={gallery}
masterFilter={masterFilter} masterFilter={masterFilter}
{...inputProps} {...inputProps}
/> />
@ -1014,6 +1052,7 @@ const ObjectProperty = ({
defaultPreviewOpen={previewOpen} defaultPreviewOpen={previewOpen}
showPreview={showPreview} showPreview={showPreview}
showInfo={showHyperlink} showInfo={showHyperlink}
gallery={gallery}
masterFilter={masterFilter} masterFilter={masterFilter}
{...inputProps} {...inputProps}
/> />
@ -1073,7 +1112,7 @@ const ObjectProperty = ({
} }
ObjectProperty.propTypes = { ObjectProperty.propTypes = {
type: PropTypes.string.isRequired, type: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
modelType: PropTypes.string, modelType: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.any, PropTypes.func]), value: PropTypes.oneOfType([PropTypes.any, PropTypes.func]),
isEditing: PropTypes.bool, isEditing: PropTypes.bool,
@ -1101,7 +1140,7 @@ ObjectProperty.propTypes = {
showPreview: PropTypes.bool, showPreview: PropTypes.bool,
useFormItem: PropTypes.bool, useFormItem: PropTypes.bool,
showHyperlink: PropTypes.bool, showHyperlink: PropTypes.bool,
options: PropTypes.array, options: PropTypes.oneOfType([PropTypes.array, PropTypes.func]),
showSince: PropTypes.bool, showSince: PropTypes.bool,
inTable: PropTypes.bool, inTable: PropTypes.bool,
loading: PropTypes.bool, loading: PropTypes.bool,
@ -1112,7 +1151,8 @@ ObjectProperty.propTypes = {
style: PropTypes.object, style: PropTypes.object,
hiddenPropertyWidth: PropTypes.string, hiddenPropertyWidth: PropTypes.string,
inherit: PropTypes.bool, inherit: PropTypes.bool,
scrollHeight: PropTypes.string scrollHeight: PropTypes.string,
gallery: PropTypes.bool
} }
export default ObjectProperty export default ObjectProperty

View File

@ -20,10 +20,19 @@ const StateDisplay = ({
'queued', 'queued',
'printing', 'printing',
'used', 'used',
'deploying' 'deploying',
'syncing',
'publishing',
'unpublishing'
] ]
const orangeProgressTypes = ['used', 'deploying', 'queued'] const orangeProgressTypes = ['used', 'deploying', 'queued']
const activeProgressTypes = ['printing', 'deploying'] const activeProgressTypes = [
'printing',
'deploying',
'publishing',
'unpublishing',
'syncing'
]
const currentState = state || { const currentState = state || {
type: 'unknown', type: 'unknown',

View File

@ -100,6 +100,14 @@ const StateTag = ({ state, showBadge = true, showTag = true, style = {} }) => {
status = 'processing' status = 'processing'
text = 'Syncing' text = 'Syncing'
break break
case 'publishing':
status = 'processing'
text = 'Publishing'
break
case 'unpublishing':
status = 'processing'
text = 'Unpublishing'
break
case 'disconnected': case 'disconnected':
status = 'default' status = 'default'
text = 'Disconnected' text = 'Disconnected'
@ -112,6 +120,10 @@ const StateTag = ({ state, showBadge = true, showTag = true, style = {} }) => {
status = 'warning' status = 'warning'
text = 'Queued' text = 'Queued'
break break
case 'pending':
status = 'default'
text = 'Pending'
break
case 'used': case 'used':
status = 'warning' status = 'warning'
text = 'Used' text = 'Used'

View File

@ -1,4 +1,8 @@
import get from 'lodash/get'
import mergeWith from 'lodash/mergeWith' import mergeWith from 'lodash/mergeWith'
import set from 'lodash/set'
const NESTED_OBJECT_KEYS = ['_id', '_reference', 'name', 'state']
export function capitalizeFirstLetter(string) { export function capitalizeFirstLetter(string) {
try { try {
@ -71,3 +75,72 @@ export function mergeFormData(...sources) {
mergeFormCustomizer 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)
}

View File

@ -0,0 +1,6 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/fulfillmentpolicyicon.svg?react'
const FulfillmentPolicyIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default FulfillmentPolicyIcon

View File

@ -0,0 +1,6 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/paymentpolicyicon.svg?react'
const PaymentPolicyIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default PaymentPolicyIcon

View File

@ -0,0 +1,6 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/returnpolicyicon.svg?react'
const ReturnPolicyIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default ReturnPolicyIcon

View File

@ -21,6 +21,7 @@ import ClientIcon from './ClientIcon'
import SalesOrderIcon from './SalesOrderIcon' import SalesOrderIcon from './SalesOrderIcon'
import MarketplaceIcon from './MarketplaceIcon' import MarketplaceIcon from './MarketplaceIcon'
import ListingIcon from './ListingIcon' import ListingIcon from './ListingIcon'
import ListingVarientIcon from './ListingVarientIcon'
import FinanceIcon from './FinanceIcon' import FinanceIcon from './FinanceIcon'
import InvoiceIcon from './InvoiceIcon' import InvoiceIcon from './InvoiceIcon'
import PaymentIcon from './PaymentIcon' import PaymentIcon from './PaymentIcon'
@ -51,6 +52,9 @@ import CourierIcon from './CourierIcon'
import CourierServiceIcon from './CourierServiceIcon' import CourierServiceIcon from './CourierServiceIcon'
import TaxRateIcon from './TaxRateIcon' import TaxRateIcon from './TaxRateIcon'
import TaxRecordIcon from './TaxRecordIcon' import TaxRecordIcon from './TaxRecordIcon'
import FulfillmentPolicyIcon from './FulfillmentPolicyIcon'
import ReturnPolicyIcon from './ReturnPolicyIcon'
import PaymentPolicyIcon from './PaymentPolicyIcon'
import AppPasswordIcon from './AppPasswordIcon' import AppPasswordIcon from './AppPasswordIcon'
import InfoCircleIcon from './InfoCircleIcon' import InfoCircleIcon from './InfoCircleIcon'
@ -80,6 +84,10 @@ const sidebarIconMap = {
salesOrder: <SalesOrderIcon />, salesOrder: <SalesOrderIcon />,
marketplace: <MarketplaceIcon />, marketplace: <MarketplaceIcon />,
listing: <ListingIcon />, listing: <ListingIcon />,
listingVarient: <ListingVarientIcon />,
fulfillmentPolicy: <FulfillmentPolicyIcon />,
returnPolicy: <ReturnPolicyIcon />,
paymentPolicy: <PaymentPolicyIcon />,
finance: <FinanceIcon />, finance: <FinanceIcon />,
invoice: <InvoiceIcon />, invoice: <InvoiceIcon />,
payment: <PaymentIcon />, payment: <PaymentIcon />,

View File

@ -49,6 +49,9 @@ import { SalesOrder } from './models/SalesOrder.js'
import { Marketplace } from './models/Marketplace.js' import { Marketplace } from './models/Marketplace.js'
import { Listing } from './models/Listing.js' import { Listing } from './models/Listing.js'
import { ListingVarient } from './models/ListingVarient.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' import QuestionCircleIcon from '../components/Icons/QuestionCircleIcon'
export const objectModels = [ export const objectModels = [
@ -102,7 +105,10 @@ export const objectModels = [
SalesOrder, SalesOrder,
Marketplace, Marketplace,
Listing, Listing,
ListingVarient ListingVarient,
FulfillmentPolicy,
ReturnPolicy,
PaymentPolicy
] ]
// Re-export individual models for direct access // Re-export individual models for direct access
@ -157,7 +163,10 @@ export {
SalesOrder, SalesOrder,
Marketplace, Marketplace,
Listing, Listing,
ListingVarient ListingVarient,
FulfillmentPolicy,
ReturnPolicy,
PaymentPolicy
} }
export function getModelByName(name, ignoreCase = false) { 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) { export function getModelProperty(name, property) {
const model = getModelByName(name) const model = getModelByName(name)

View File

@ -24,10 +24,7 @@ const SIDEBARS = {
key: 'sales', key: 'sales',
label: 'Sales', label: 'Sales',
iconKey: 'sales', iconKey: 'sales',
items: salesSidebarItems, items: salesSidebarItems
routeAliases: {
'/dashboard/sales/listingvarients': 'listings'
}
}, },
finance: { finance: {
key: 'finance', key: 'finance',

View File

@ -30,6 +30,15 @@ export const File = {
label: 'List', label: 'List',
icon: ListIcon icon: ListIcon
}, },
{
name: 'new',
type: 'page',
pageName: 'info',
label: 'New File',
visible: () => {
return false
}
},
{ {
name: 'info', name: 'info',
type: 'page', type: 'page',
@ -117,14 +126,7 @@ export const File = {
'updatedAt', 'updatedAt',
'_reference' '_reference'
], ],
sorters: [ sorters: ['name', 'type', 'size', 'createdAt', 'temp', 'updatedAt'],
'name',
'type',
'size',
'createdAt',
'temp',
'updatedAt'
],
group: ['type'], group: ['type'],
properties: [ properties: [
{ {

View File

@ -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')
]
}

View File

@ -112,7 +112,10 @@ export const Listing = {
visible: (objectData) => visible: (objectData) =>
objectData?.state?.type === 'draft' || objectData?.state?.type === 'draft' ||
objectData?.state?.type === 'inactive', objectData?.state?.type === 'inactive',
disabled: (objectData) => objectData?.state?.type === 'syncing', disabled: (objectData) =>
['syncing', 'publishing', 'unpublishing'].includes(
objectData?.state?.type
),
content: (objectData, { onOk } = {}) => { content: (objectData, { onOk } = {}) => {
return createElement(PublishListing, { objectData, onOk }) return createElement(PublishListing, { objectData, onOk })
} }
@ -125,7 +128,10 @@ export const Listing = {
icon: XMarkIcon, icon: XMarkIcon,
danger: true, danger: true,
visible: (objectData) => objectData?.state?.type === 'active', visible: (objectData) => objectData?.state?.type === 'active',
disabled: (objectData) => objectData?.state?.type === 'syncing', disabled: (objectData) =>
['syncing', 'publishing', 'unpublishing'].includes(
objectData?.state?.type
),
content: (objectData, { onOk } = {}) => { content: (objectData, { onOk } = {}) => {
return createElement(UnpublishListing, { objectData, onOk }) return createElement(UnpublishListing, { objectData, onOk })
} }
@ -158,6 +164,9 @@ export const Listing = {
'stockLocation', 'stockLocation',
'stockQuantity', 'stockQuantity',
'marketplace', 'marketplace',
'fulfillmentPolicy',
'paymentPolicy',
'returnPolicy',
'courierServices', 'courierServices',
'state', 'state',
'condition', 'condition',
@ -173,6 +182,9 @@ export const Listing = {
'stockLocation', 'stockLocation',
'stockQuantity', 'stockQuantity',
'marketplace', 'marketplace',
'fulfillmentPolicy',
'paymentPolicy',
'returnPolicy',
'courierServices', 'courierServices',
'state', 'state',
'state.type', 'state.type',
@ -252,6 +264,14 @@ export const Listing = {
extra: extra:
'Used as the eBay listing description. If empty, the title is sent instead.' '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', name: 'lastSyncedAt',
label: 'Last Synced', label: 'Last Synced',
@ -314,6 +334,58 @@ export const Listing = {
required: true, required: true,
columnWidth: 200 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', name: 'state',
label: 'State', label: 'State',
@ -378,6 +450,17 @@ export const Listing = {
required: false, required: false,
columnWidth: 250 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', name: 'courierServices',
label: 'Courier Services', label: 'Courier Services',
@ -386,6 +469,8 @@ export const Listing = {
objectType: 'courierService', objectType: 'courierService',
readOnly: false, readOnly: false,
required: true, required: true,
extra:
'Used for order shipping and as the eBay fulfillment fallback when no fulfillment policy is set.',
columnWidth: 250 columnWidth: 250
} }
], ],
@ -414,6 +499,18 @@ export const Listing = {
type: 'number', type: 'number',
color: 'processing' color: 'processing'
}, },
{
name: 'publishing.count',
label: 'Publishing',
type: 'number',
color: 'processing'
},
{
name: 'unpublishing.count',
label: 'Unpublishing',
type: 'number',
color: 'processing'
},
{ {
name: 'suspended.count', name: 'suspended.count',
label: 'Suspended', label: 'Suspended',

View File

@ -108,7 +108,10 @@ export const ListingVarient = {
visible: (objectData) => visible: (objectData) =>
objectData?.state?.type === 'draft' || objectData?.state?.type === 'draft' ||
objectData?.state?.type === 'inactive', objectData?.state?.type === 'inactive',
disabled: (objectData) => objectData?.state?.type === 'syncing', disabled: (objectData) =>
['syncing', 'publishing', 'unpublishing'].includes(
objectData?.state?.type
),
content: (objectData, { onOk } = {}) => { content: (objectData, { onOk } = {}) => {
return createElement(PublishListingVarient, { objectData, onOk }) return createElement(PublishListingVarient, { objectData, onOk })
} }
@ -121,7 +124,10 @@ export const ListingVarient = {
icon: XMarkIcon, icon: XMarkIcon,
danger: true, danger: true,
visible: (objectData) => objectData?.state?.type === 'active', visible: (objectData) => objectData?.state?.type === 'active',
disabled: (objectData) => objectData?.state?.type === 'syncing', disabled: (objectData) =>
['syncing', 'publishing', 'unpublishing'].includes(
objectData?.state?.type
),
content: (objectData, { onOk } = {}) => { content: (objectData, { onOk } = {}) => {
return createElement(UnpublishListingVarient, { objectData, onOk }) return createElement(UnpublishListingVarient, { objectData, onOk })
} }
@ -312,6 +318,42 @@ export const ListingVarient = {
type: 'dateTime', type: 'dateTime',
readOnly: true, readOnly: true,
columnWidth: 175 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
}
]
} }
] ]
} }

View File

@ -15,6 +15,21 @@ const SyncListings = lazy(
const SyncOrders = lazy( const SyncOrders = lazy(
() => import('../../components/Dashboard/Sales/Marketplaces/SyncOrders') () => 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( const DeleteObject = lazy(
() => import('../../components/Dashboard/common/DeleteObject') () => import('../../components/Dashboard/common/DeleteObject')
) )
@ -134,19 +149,6 @@ export const Marketplace = {
return createElement(SyncListings, { 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', name: 'syncOrders',
type: 'modal', type: 'modal',
@ -159,6 +161,58 @@ export const Marketplace = {
content: (objectData, { onOk } = {}) => { content: (objectData, { onOk } = {}) => {
return createElement(SyncOrders, { 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 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', name: 'config.accessTokenExpiresAt',
label: 'Access Token Expires At', label: 'Access Token Expires At',
@ -331,7 +375,6 @@ export const Marketplace = {
readOnly: true, readOnly: true,
columnWidth: 215 columnWidth: 215
}, },
{ {
name: 'state', name: 'state',
label: 'State', label: 'State',
@ -340,21 +383,7 @@ export const Marketplace = {
readOnly: true, readOnly: true,
columnWidth: 260 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', name: 'config.refreshTokenExpiresAt',
label: 'Refresh Token Expires At', label: 'Refresh Token Expires At',
@ -364,15 +393,40 @@ export const Marketplace = {
columnWidth: 200 columnWidth: 200
}, },
{ {
name: 'config.clientId', name: 'eBay.availableShippingServices',
label: 'Client ID', label: 'Available Shipping Services',
type: 'secret', type: 'number',
readOnly: false, readOnly: true,
required: false, 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', name: 'config.lastTokenRefreshAt',
label: 'Last Token Refresh At', label: 'Last Token Refresh At',
@ -381,6 +435,23 @@ export const Marketplace = {
showSince: true, showSince: true,
columnWidth: 200 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', name: 'config.clientSecret',
@ -555,6 +626,37 @@ export const Marketplace = {
} }
return `${window.location.origin}/auth/marketplace/callback` 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: [ stats: [

View File

@ -6,9 +6,20 @@ const PartStockInfo = lazy(
const NewPartStock = lazy( const NewPartStock = lazy(
() => import('../../components/Dashboard/Inventory/PartStocks/NewPartStock') () => 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 PartStockIcon from '../../components/Icons/PartStockIcon'
import PlusIcon from '../../components/Icons/PlusIcon' import PlusIcon from '../../components/Icons/PlusIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' 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 ListIcon from '../../components/Icons/ListIcon'
export const PartStock = { export const PartStock = {
@ -17,7 +28,6 @@ export const PartStock = {
labelPlural: 'Part Stocks', labelPlural: 'Part Stocks',
url: '/dashboard/inventory/partstocks', url: '/dashboard/inventory/partstocks',
prefix: 'PTS', prefix: 'PTS',
readOnly: true,
icon: PartStockIcon, icon: PartStockIcon,
actions: [ actions: [
{ {
@ -50,6 +60,75 @@ export const PartStock = {
default: true, default: true,
row: true, row: true,
icon: InfoCircleIcon 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: [ pages: [
@ -62,7 +141,6 @@ export const PartStock = {
'part', 'part',
'partSku', 'partSku',
'state', 'state',
'startingQuantity',
'currentQuantity', 'currentQuantity',
'stockLocation', 'stockLocation',
'createdAt', 'createdAt',
@ -72,7 +150,6 @@ export const PartStock = {
sorters: [ sorters: [
'part', 'part',
'partSku', 'partSku',
'startingQuantity',
'currentQuantity', 'currentQuantity',
'state', 'state',
'createdAt', 'createdAt',
@ -81,7 +158,6 @@ export const PartStock = {
columns: [ columns: [
'_reference', '_reference',
'state', 'state',
'startingQuantity',
'currentQuantity', 'currentQuantity',
'part', 'part',
'partSku', 'partSku',
@ -116,13 +192,6 @@ export const PartStock = {
readOnly: true, readOnly: true,
columnWidth: 180 columnWidth: 180
}, },
{
name: 'state',
label: 'State',
type: 'state',
readOnly: true,
columnWidth: 260
},
{ {
name: 'updatedAt', name: 'updatedAt',
label: 'Updated At', label: 'Updated At',
@ -131,13 +200,19 @@ export const PartStock = {
columnWidth: 175 columnWidth: 175
}, },
{ {
name: 'sourceType', name: 'state',
label: 'Source Type', label: 'State',
type: 'objectType', type: 'state',
readOnly: false, readOnly: true,
columnWidth: 200, columnWidth: 260
required: true, },
masterFilter: ['subJob', 'stockTransfer']
{
name: 'postedAt',
label: 'Posted At',
type: 'dateTime',
readOnly: true,
columnWidth: 175
}, },
{ {
name: 'part', name: 'part',
@ -169,36 +244,22 @@ export const PartStock = {
showHyperlink: true, showHyperlink: true,
columnWidth: 200 columnWidth: 200
}, },
{
name: 'source',
label: 'Source',
type: 'object',
readOnly: false,
required: true,
columnWidth: 200,
objectType: (objectData) => {
return objectData?.sourceType
},
showHyperlink: true
},
{ {
name: 'currentQuantity', name: 'currentQuantity',
label: 'Current Quantity', label: 'Current Quantity',
type: 'number', type: 'number',
readOnly: true,
columnWidth: 200, columnWidth: 200,
required: true, required: true
value: (objectData) => {
if (objectData?.state?.type === 'new') {
return objectData?.startingQuantity
} else {
return objectData.currentQuantity
}
}
} }
], ],
stats: [ stats: [
{
name: 'draft.count',
label: 'Draft',
type: 'number',
color: 'default',
history: false
},
{ {
name: 'new.count', name: 'new.count',
label: 'New', label: 'New',

View File

@ -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')
]
}

View File

@ -1,14 +1,17 @@
import { createElement, lazy } from 'react' import { createElement, lazy } from 'react'
const ProductCategoryInfo = lazy( const ProductCategoryInfo = lazy(
() => import('../../components/Dashboard/Management/ProductCategories/ProductCategoryInfo') () =>
import('../../components/Dashboard/Management/ProductCategories/ProductCategoryInfo')
) )
const NewProductCategory = lazy( const NewProductCategory = lazy(
() => import('../../components/Dashboard/Management/ProductCategories/NewProductCategory') () =>
import('../../components/Dashboard/Management/ProductCategories/NewProductCategory')
) )
const DeleteObject = lazy( const DeleteObject = lazy(
() => import('../../components/Dashboard/common/DeleteObject') () => import('../../components/Dashboard/common/DeleteObject')
) )
import { findTreeNode } from '../../components/Dashboard/common/CustomTreeSelect'
import ProductCategoryIcon from '../../components/Icons/ProductCategoryIcon' import ProductCategoryIcon from '../../components/Icons/ProductCategoryIcon'
import PlusIcon from '../../components/Icons/PlusIcon' import PlusIcon from '../../components/Icons/PlusIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
@ -35,7 +38,11 @@ export const ProductCategory = {
label: 'New Product Category', label: 'New Product Category',
icon: PlusIcon, icon: PlusIcon,
content: (objectData, { onOk } = {}) => { 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'], columns: ['_reference', 'name', 'createdAt', 'updatedAt'],
filters: ['_id','name','createdAt','updatedAt','_reference'], filters: [
'_id',
'name',
'createdAt',
'updatedAt',
'_reference',
'marketplaces.marketplace'
],
sorters: ['name', 'createdAt', 'updatedAt', '_id'], sorters: ['name', 'createdAt', 'updatedAt', '_id'],
properties: [ properties: [
{ {
@ -136,6 +150,14 @@ export const ProductCategory = {
readOnly: true, readOnly: true,
columnWidth: 180 columnWidth: 180
}, },
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true,
columnWidth: 175
},
{ {
name: 'name', name: 'name',
label: 'Name', label: 'Name',
@ -145,11 +167,47 @@ export const ProductCategory = {
columnWidth: 200 columnWidth: 200
}, },
{ {
name: 'updatedAt', name: 'marketplaces',
label: 'Updated At', label: 'Marketplaces',
type: 'dateTime', type: 'objectChildren',
readOnly: true, required: false,
columnWidth: 175 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
}
]
} }
] ]
} }

View File

@ -371,10 +371,11 @@ export const ProductStock = {
required: false, required: false,
showHyperlink: true, showHyperlink: true,
columnWidth: 260, columnWidth: 260,
masterFilter: (objectData) => { masterFilter: (objectData, parentData) => {
return { return {
part: objectData?.part?._id, part: objectData?.part?._id,
partSku: objectData?.partSku?._id, partSku: objectData?.partSku?._id,
stockLocation: parentData?.stockLocation?._id,
$or: [{ 'state.type': 'new' }, { 'state.type': 'used' }] $or: [{ 'state.type': 'new' }, { 'state.type': 'used' }]
} }
} }

View File

@ -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')
]
}

View File

@ -17,6 +17,7 @@ import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon' import XMarkIcon from '../../components/Icons/XMarkIcon'
import BinIcon from '../../components/Icons/BinIcon' import BinIcon from '../../components/Icons/BinIcon'
import ListIcon from '../../components/Icons/ListIcon' import ListIcon from '../../components/Icons/ListIcon'
import { marketplaceSyncMappingsProperty } from './marketplaceMappings'
export const TaxRate = { export const TaxRate = {
name: 'taxRate', name: 'taxRate',
@ -111,6 +112,7 @@ export const TaxRate = {
'rateType', 'rateType',
'active', 'active',
'country', 'country',
'jurisdiction',
'createdAt', 'createdAt',
'updatedAt' 'updatedAt'
], ],
@ -120,6 +122,7 @@ export const TaxRate = {
'rateType', 'rateType',
'active', 'active',
'country', 'country',
'jurisdiction',
'createdAt', 'createdAt',
'updatedAt', 'updatedAt',
'_reference' '_reference'
@ -130,6 +133,7 @@ export const TaxRate = {
'rateType', 'rateType',
'active', 'active',
'country', 'country',
'jurisdiction',
'createdAt', 'createdAt',
'_id', '_id',
'updatedAt' 'updatedAt'
@ -237,6 +241,23 @@ export const TaxRate = {
required: false, required: false,
columnWidth: 150 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', name: 'description',
label: 'Description', label: 'Description',
@ -244,6 +265,10 @@ export const TaxRate = {
readOnly: false, readOnly: false,
required: false, required: false,
columnWidth: 200 columnWidth: 200
} },
marketplaceSyncMappingsProperty(
'taxRate',
'eBay tax table key as COUNTRY:JURISDICTION, for example US:GU. Filled when synced.'
)
] ]
} }

View File

@ -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
}
]
}
}

View File

@ -18,6 +18,12 @@ const financeSidebarItems = [
iconKey: 'payment', iconKey: 'payment',
path: '/dashboard/finance/payments' path: '/dashboard/finance/payments'
}, },
{
key: 'paymentpolicies',
label: 'Payment Policies',
iconKey: 'paymentPolicy',
path: '/dashboard/finance/paymentpolicies'
},
{ {
key: 'taxRecords', key: 'taxRecords',
iconKey: 'taxRecord', iconKey: 'taxRecord',

View File

@ -18,6 +18,7 @@ const salesSidebarItems = [
iconKey: 'salesOrder', iconKey: 'salesOrder',
path: '/dashboard/sales/salesorders' path: '/dashboard/sales/salesorders'
}, },
{ type: 'divider' },
{ {
key: 'marketplaces', key: 'marketplaces',
label: 'Marketplaces', label: 'Marketplaces',
@ -29,6 +30,24 @@ const salesSidebarItems = [
label: 'Listings', label: 'Listings',
iconKey: 'listing', iconKey: 'listing',
path: '/dashboard/sales/listings' 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'
} }
] ]

View File

@ -23,6 +23,9 @@ const FinanceOverview = lazy(
const TaxRecords = lazy( const TaxRecords = lazy(
() => import('../components/Dashboard/Finance/TaxRecords.jsx') () => import('../components/Dashboard/Finance/TaxRecords.jsx')
) )
const PaymentPolicies = lazy(
() => import('../components/Dashboard/Finance/PaymentPolicies.jsx')
)
const FinanceRoutes = [ const FinanceRoutes = [
<Route <Route
@ -33,6 +36,11 @@ const FinanceRoutes = [
<Route key='invoices' path='finance/invoices' element={<Invoices />} />, <Route key='invoices' path='finance/invoices' element={<Invoices />} />,
<Route key='payments' path='finance/payments' element={<Payments />} />, <Route key='payments' path='finance/payments' element={<Payments />} />,
<Route key='taxrecords' path='finance/taxrecords' element={<TaxRecords />} />, <Route key='taxrecords' path='finance/taxrecords' element={<TaxRecords />} />,
<Route
key='paymentpolicies'
path='finance/paymentpolicies'
element={<PaymentPolicies />}
/>,
<Route <Route
key='taxrecords-redirect' key='taxrecords-redirect'
path='management/taxrecords' path='management/taxrecords'

View File

@ -14,6 +14,15 @@ const Marketplaces = lazy(
const Listings = lazy( const Listings = lazy(
() => import('../components/Dashboard/Sales/Listings.jsx') () => 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 = [ const SalesRoutes = [
<Route key='overview' path='sales/overview' element={<SalesOverview />} />, <Route key='overview' path='sales/overview' element={<SalesOverview />} />,
@ -28,7 +37,22 @@ const SalesRoutes = [
path='sales/marketplaces' path='sales/marketplaces'
element={<Marketplaces />} element={<Marketplaces />}
/>, />,
<Route key='listings' path='sales/listings' element={<Listings />} /> <Route key='listings' path='sales/listings' element={<Listings />} />,
<Route
key='listingvarients'
path='sales/listingvarients'
element={<ListingVarients />}
/>,
<Route
key='fulfillmentpolicies'
path='sales/fulfillmentpolicies'
element={<FulfillmentPolicies />}
/>,
<Route
key='returnpolicies'
path='sales/returnpolicies'
element={<ReturnPolicies />}
/>
] ]
export default SalesRoutes export default SalesRoutes