Added missing SKUs.

This commit is contained in:
Tom Butcher 2026-03-08 01:07:29 +00:00
parent 5ba205c6cc
commit 6d1946b91a
22 changed files with 1308 additions and 279 deletions

View File

@ -0,0 +1,13 @@
<?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.011907,-0,-0,-0.011907,-0.017623,64)">
<path d="M2168,4709C1894,4666 1703,4555 1616,4389C1580,4321 1580,4184 1616,4116C1686,3983 1795,3902 1990,3838C2374,3713 2898,3806 3075,4030C3139,4112 3155,4156 3155,4253C3155,4349 3139,4393 3075,4475C2930,4659 2521,4765 2168,4709ZM2531,4395C2672,4372 2780,4328 2829,4275C2848,4253 2848,4252 2829,4230C2762,4157 2554,4095 2373,4095C2192,4095 1984,4157 1917,4230C1898,4252 1898,4253 1917,4275C2010,4378 2296,4434 2531,4395Z" style="fill-rule:nonzero;"/>
<path d="M4413.328,0.003L3970,0C2805,0 2804,0 2641,56C2546.293,88.172 2456.943,133.018 2375.17,188.649L752.515,188.649C440.617,188.649 187.395,441.871 187.395,753.769L187.395,2042.949C187.395,2333.828 407.638,2573.672 690.318,2604.678C623.112,2767.055 626.105,2931.521 701,3087L731,3149L707,3196C648,3311 626,3466 653,3579L665,3631L598,3692C381,3894 287,4149 343,4391C403,4658 606,4880 958,5066C1270,5230 1637,5326 2121,5369C2228,5379 2689,5366 2814,5350C3439,5270 3943,5054 4214,4748C4509,4415 4483,4003 4148,3692L4081,3631L4093,3579C4120,3465 4098,3312 4038,3194C4014,3145 4014,3144 4033,3113C4078,3041 4100,2950 4100,2835C4100,2738.782 4090.9,2682.466 4058.642,2608.068L4625.617,2608.068C4937.515,2608.068 5190.736,2354.846 5190.736,2042.949L5190.736,753.769C5190.736,611.423 5137.993,481.298 5051,381.889L5051.021,155.223C5051.021,69.541 4981.4,0.003 4895.817,0.003L4413.328,0.003ZM1063.992,2608.068L1634.782,2608.068C1394.851,2668.367 1190.699,2752.015 1034,2856C966,2901 967,2901 960,2872C944,2809 979,2712 1045,2630C1050.866,2622.695 1057.204,2615.38 1063.992,2608.068ZM1078,3393C1025,3416 976,3438 970,3440C961,3444 960,3436 965,3411C1016,3186 1382,2969 1856,2883C2036,2850 2146,2841 2373,2841C2600,2841 2710,2850 2890,2883C3364,2969 3730,3186 3781,3411C3786,3436 3785,3444 3776,3440C3770,3438 3721,3416 3668,3393C3424,3286 3132,3213 2772,3169C2627,3151 2119,3151 1974,3169C1614,3213 1322,3286 1078,3393ZM4854.788,2042.949C4854.788,2169.432 4752.1,2272.12 4625.617,2272.12L752.515,2272.12C626.032,2272.12 523.344,2169.432 523.344,2042.949L523.344,753.769C523.344,627.286 626.032,524.597 752.515,524.597L4625.617,524.597C4752.1,524.597 4854.788,627.286 4854.788,753.769L4854.788,2042.949ZM2621,5050C1966,5103 1283,4947 897,4656C795,4579 733,4507 685,4411C650,4342 646,4324 646,4253C646,4146 685,4063 783,3960C1103,3621 1884,3416 2631,3476C3229,3524 3721,3702 3963,3960C4061,4063 4100,4146 4100,4253C4100,4324 4096,4342 4061,4411C4013,4507 3951,4579 3849,4656C3560,4873 3138,5009 2621,5050ZM3111.507,2608.068L3680.692,2608.068C3712.673,2642.169 3737.999,2677.689 3757,2715C3782,2766 3797,2857 3783,2881C3775,2896 3769,2895 3733,2870C3569.742,2759.218 3359.738,2670.745 3111.507,2608.068Z"/>
<g transform="matrix(143.743961,-0,-0,-143.743961,-3687.186324,8190.967756)">
<path d="M34.865,52.259C37.413,52.259 38.935,51.026 38.935,49.082C38.935,47.56 37.996,46.722 35.931,46.326L34.939,46.139C33.887,45.938 33.451,45.656 33.451,45.147C33.451,44.577 33.974,44.174 34.865,44.174C35.576,44.174 36.112,44.409 36.428,45.012C36.716,45.482 37.051,45.676 37.587,45.676C38.204,45.669 38.62,45.287 38.62,44.717C38.62,44.516 38.586,44.355 38.519,44.188C38.05,42.961 36.696,42.25 34.845,42.25C32.62,42.25 31.004,43.45 31.004,45.314C31.004,46.789 32.01,47.734 33.934,48.096L34.933,48.284C36.079,48.505 36.488,48.78 36.488,49.316C36.488,49.906 35.864,50.335 34.906,50.335C34.128,50.335 33.478,50.081 33.149,49.477C32.834,48.995 32.512,48.827 32.036,48.827C31.413,48.827 30.97,49.243 30.97,49.859C30.97,50.061 31.011,50.268 31.098,50.469C31.5,51.468 32.767,52.259 34.865,52.259Z" style="fill-rule:nonzero;"/>
<path d="M41.308,52.239C42.086,52.239 42.535,51.777 42.535,50.959L42.535,49.357L43.433,48.418L45.941,51.549C46.336,52.052 46.691,52.246 47.208,52.246C47.878,52.246 48.388,51.73 48.388,51.059C48.388,50.718 48.22,50.349 47.818,49.853L45.344,46.823L47.657,44.382C47.985,44.027 48.113,43.759 48.113,43.397C48.113,42.767 47.596,42.264 46.953,42.264C46.537,42.264 46.229,42.425 45.88,42.814L42.589,46.46L42.535,46.46L42.535,43.551C42.535,42.733 42.086,42.27 41.308,42.27C40.53,42.27 40.075,42.733 40.075,43.551L40.075,50.959C40.075,51.777 40.53,52.239 41.308,52.239Z" style="fill-rule:nonzero;"/>
<path d="M53.583,52.259C56.097,52.259 57.746,50.832 57.746,48.659L57.746,43.551C57.746,42.733 57.297,42.27 56.513,42.27C55.735,42.27 55.286,42.733 55.286,43.551L55.286,48.398C55.286,49.524 54.676,50.195 53.583,50.195C52.484,50.195 51.874,49.524 51.874,48.398L51.874,43.551C51.874,42.733 51.424,42.27 50.647,42.27C49.869,42.27 49.413,42.733 49.413,43.551L49.413,48.659C49.413,50.832 51.062,52.259 53.583,52.259Z" style="fill-rule:nonzero;"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@ -46,6 +46,34 @@ const NewOrderItem = ({ onOk, reset, defaultValues }) => {
/> />
) )
}, },
{
title: 'Optional',
key: 'optional',
content: (
<ObjectInfo
type='orderItem'
column={1}
bordered={false}
isEditing={true}
required={false}
objectData={objectData}
visibleProperties={{
sku: true,
shipment: true,
invoicedAmount: false,
invoicedAmountWithTax: false,
invoicedQuantity: false,
invoicedAmountRemaining: false,
invoicedAmountWithTaxRemaining: false,
invoicedQuantityRemaining: false,
orderedAt: false,
receivedAt: false,
syncAmount: false
}}
/>
)
},
{ {
title: 'Pricing', title: 'Pricing',
key: 'pricing', key: 'pricing',
@ -67,32 +95,6 @@ const NewOrderItem = ({ onOk, reset, defaultValues }) => {
/> />
) )
}, },
{
title: 'Optional',
key: 'optional',
content: (
<ObjectInfo
type='orderItem'
column={1}
bordered={false}
isEditing={true}
required={false}
objectData={objectData}
visibleProperties={{
shipment: true,
invoicedAmount: false,
invoicedAmountWithTax: false,
invoicedQuantity: false,
invoicedAmountRemaining: false,
invoicedAmountWithTaxRemaining: false,
invoicedQuantityRemaining: false,
orderedAt: false,
receivedAt: false,
syncAmount: false
}}
/>
)
},
{ {
title: 'Summary', title: 'Summary',
key: 'summary', key: 'summary',

View File

@ -0,0 +1,104 @@
import { useState, useRef } from 'react'
import { Button, Flex, Space, Modal, Dropdown } from 'antd'
import NewFilamentSku from './FilamentSkus/NewFilamentSku'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import ObjectTable from '../common/ObjectTable'
import ListIcon from '../../Icons/ListIcon'
import GridIcon from '../../Icons/GridIcon'
import useViewMode from '../hooks/useViewMode'
import ColumnViewButton from '../common/ColumnViewButton'
import ExportListButton from '../common/ExportListButton'
const FilamentSkus = () => {
const tableRef = useRef()
const [newFilamentSkuOpen, setNewFilamentSkuOpen] = useState(false)
const [viewMode, setViewMode] = useViewMode('filamentSkus')
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('filamentSku')
const actionItems = {
items: [
{
label: 'New Filament SKU',
key: 'newFilamentSku',
icon: <PlusIcon />
},
{ type: 'divider' },
{
label: 'Reload List',
key: 'reloadList',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reloadList') {
tableRef.current?.reload()
} else if (key === 'newFilamentSku') {
setNewFilamentSkuOpen(true)
}
}
}
return (
<>
<Flex vertical={'true'} gap='large'>
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<ColumnViewButton
type='filamentSku'
loading={false}
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
<ExportListButton objectType='filamentSku' />
</Space>
<Space>
<Button
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
onClick={() =>
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
}
/>
</Space>
</Flex>
<ObjectTable
ref={tableRef}
visibleColumns={columnVisibility}
type='filamentSku'
cards={viewMode === 'cards'}
/>
</Flex>
<Modal
open={newFilamentSkuOpen}
styles={{ content: { paddingBottom: '24px' } }}
footer={null}
width={600}
onCancel={() => {
setNewFilamentSkuOpen(false)
}}
destroyOnHidden={true}
>
<NewFilamentSku
onOk={() => {
setNewFilamentSkuOpen(false)
tableRef.current?.reload()
}}
reset={newFilamentSkuOpen}
/>
</Modal>
</>
)
}
export default FilamentSkus

View File

@ -0,0 +1,197 @@
import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card } from 'antd'
import useCollapseState from '../../hooks/useCollapseState'
import NotesPanel from '../../common/NotesPanel'
import InfoCollapse from '../../common/InfoCollapse'
import ObjectInfo from '../../common/ObjectInfo'
import ViewButton from '../../common/ViewButton'
import ObjectForm from '../../common/ObjectForm'
import EditButtons from '../../common/EditButtons'
import LockIndicator from '../../common/LockIndicator.jsx'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
const FilamentSkuInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const filamentSkuId = new URLSearchParams(location.search).get('filamentSkuId')
const [collapseState, updateCollapseState] = useCollapseState(
'FilamentSkuInfo',
{
info: true,
notes: true,
auditLogs: true
}
)
const [objectFormState, setEditFormState] = useState({
isEditing: false,
editLoading: false,
formValid: false,
lock: null,
loading: false,
objectData: {}
})
const actions = {
reload: () => {
objectFormRef?.current?.fetchObject?.()
return true
},
edit: () => {
objectFormRef?.current?.startEditing?.()
return false
},
cancelEdit: () => {
objectFormRef?.current?.cancelEditing?.()
return true
},
finishEdit: () => {
objectFormRef?.current?.handleUpdate?.()
return true
},
delete: () => {
objectFormRef?.current?.handleDelete?.()
return true
}
}
return (
<>
<Flex
gap='large'
vertical='true'
style={{ maxHeight: '100%', minHeight: 0 }}
>
<Flex justify={'space-between'}>
<Space size='middle'>
<Space size='small'>
<ObjectActions
type='filamentSku'
id={filamentSkuId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Filament SKU Information' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<UserNotifierToggle
type='filamentSku'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
<DocumentPrintButton
type='filamentSku'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</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.lock?.locked || objectFormState.loading}
loading={objectFormState.editLoading}
/>
</Space>
</Flex>
<ScrollBox>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
loading={objectFormState.loading}
ref={actionHandlerRef}
>
<InfoCollapse
title='Filament SKU Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) => updateCollapseState('info', expanded)}
collapseKey='info'
>
<ObjectForm
id={filamentSkuId}
type='filamentSku'
style={{ height: '100%' }}
ref={objectFormRef}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => (
<ObjectInfo
loading={loading}
isEditing={isEditing}
type='filamentSku'
objectData={objectData}
/>
)}
</ObjectForm>
</InfoCollapse>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={filamentSkuId} type='filamentSku' />
</Card>
</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._id': filamentSkuId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
</>
)
}
export default FilamentSkuInfo

View File

@ -0,0 +1,123 @@
import PropTypes from 'prop-types'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
const NewFilamentSku = ({ onOk, reset, defaultValues }) => {
return (
<NewObjectForm
type='filamentSku'
reset={reset}
defaultValues={defaultValues}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='filamentSku'
column={1}
labelWidth={70}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
visibleProperties={{
description: false,
cost: false,
costWithTax: false,
costTaxRate: false,
vendor: false
}}
/>
)
},
{
title: 'Color & Cost',
key: 'colorCost',
content: (
<ObjectInfo
type='filamentSku'
column={1}
labelWidth={100}
visibleProperties={{
_id: false,
createdAt: false,
updatedAt: false,
barcode: false,
filament: false,
name: false,
description: false
}}
bordered={false}
isEditing={true}
objectData={objectData}
/>
)
},
{
title: 'Optional',
key: 'optional',
content: (
<ObjectInfo
type='filamentSku'
column={1}
labelWidth={100}
visibleProperties={{
barcode: true,
description: true
}}
bordered={false}
isEditing={true}
objectData={objectData}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='filamentSku'
column={1}
visibleProperties={{
createdAt: false,
updatedAt: false,
_id: false
}}
labelWidth={100}
bordered={false}
isEditing={false}
objectData={objectData}
/>
)
}
]
return (
<WizardView
steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Filament SKU'
onSubmit={async () => {
const result = await handleSubmit()
if (result) {
onOk()
}
}}
/>
)
}}
</NewObjectForm>
)
}
NewFilamentSku.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool,
defaultValues: PropTypes.object
}
export default NewFilamentSku

View File

@ -1,6 +1,6 @@
import { useRef, useState } from 'react' import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import { Space, Flex, Card } from 'antd' import { Space, Flex, Card, Modal } from 'antd'
import { LoadingOutlined } from '@ant-design/icons' import { LoadingOutlined } from '@ant-design/icons'
import loglevel from 'loglevel' import loglevel from 'loglevel'
import config from '../../../../config' import config from '../../../../config'
@ -20,9 +20,11 @@ import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx' import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import FilamentIcon from '../../../Icons/FilamentIcon.jsx' import FilamentIcon from '../../../Icons/FilamentIcon.jsx'
import FilamentSkuIcon from '../../../Icons/FilamentSkuIcon.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import ScrollBox from '../../common/ScrollBox.jsx' import ScrollBox from '../../common/ScrollBox.jsx'
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
import NewFilamentSku from '../FilamentSkus/NewFilamentSku'
const log = loglevel.getLogger('FilamentInfo') const log = loglevel.getLogger('FilamentInfo')
log.setLevel(config.logLevel) log.setLevel(config.logLevel)
@ -32,10 +34,13 @@ const FilamentInfo = () => {
const objectFormRef = useRef(null) const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null) const actionHandlerRef = useRef(null)
const filamentId = new URLSearchParams(location.search).get('filamentId') const filamentId = new URLSearchParams(location.search).get('filamentId')
const [newFilamentSkuOpen, setNewFilamentSkuOpen] = useState(false)
const filamentSkusTableRef = useRef()
const [collapseState, updateCollapseState] = useCollapseState( const [collapseState, updateCollapseState] = useCollapseState(
'FilamentInfo', 'FilamentInfo',
{ {
info: true, info: true,
filamentSkus: true,
stocks: true, stocks: true,
notes: true, notes: true,
auditLogs: true auditLogs: true
@ -56,6 +61,10 @@ const FilamentInfo = () => {
objectFormRef?.current?.fetchObject?.() objectFormRef?.current?.fetchObject?.()
return true return true
}, },
newFilamentSku: () => {
setNewFilamentSkuOpen(true)
return false
},
edit: () => { edit: () => {
objectFormRef?.current?.startEditing?.() objectFormRef?.current?.startEditing?.()
return false return false
@ -93,6 +102,7 @@ const FilamentInfo = () => {
disabled={objectFormState.loading} disabled={objectFormState.loading}
items={[ items={[
{ key: 'info', label: 'Filament Information' }, { key: 'info', label: 'Filament Information' },
{ key: 'filamentSkus', label: 'Filament SKUs' },
{ key: 'stocks', label: 'Filament Stocks' }, { key: 'stocks', label: 'Filament Stocks' },
{ key: 'notes', label: 'Notes' }, { key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' } { key: 'auditLogs', label: 'Audit Logs' }
@ -171,6 +181,27 @@ const FilamentInfo = () => {
</InfoCollapse> </InfoCollapse>
</ActionHandler> </ActionHandler>
<InfoCollapse
title='Filament SKUs'
icon={<FilamentSkuIcon />}
active={collapseState.filamentSkus}
onToggle={(expanded) =>
updateCollapseState('filamentSkus', expanded)
}
collapseKey='filamentSkus'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
ref={filamentSkusTableRef}
type='filamentSku'
masterFilter={{ filament: filamentId }}
visibleColumns={{ filament: false }}
/>
)}
</InfoCollapse>
<InfoCollapse <InfoCollapse
title='Filament Stocks' title='Filament Stocks'
icon={<FilamentIcon />} icon={<FilamentIcon />}
@ -183,16 +214,36 @@ const FilamentInfo = () => {
) : ( ) : (
<ObjectTable <ObjectTable
type='filamentStock' type='filamentStock'
masterFilter={{ 'filament._id': filamentId }} masterFilter={{ 'filamentSku.filament._id': filamentId }}
visibleColumns={{ visibleColumns={{
filament: false, filamentSku: false,
'filament._id': false, 'filamentSku.filament._id': false,
startingWeight: false startingWeight: false
}} }}
/> />
)} )}
</InfoCollapse> </InfoCollapse>
<Modal
open={newFilamentSkuOpen}
styles={{ content: { paddingBottom: '24px' } }}
footer={null}
width={600}
onCancel={() => setNewFilamentSkuOpen(false)}
destroyOnClose
>
<NewFilamentSku
onOk={() => {
setNewFilamentSkuOpen(false)
filamentSkusTableRef.current?.reload?.()
}}
reset={newFilamentSkuOpen}
defaultValues={{
filament: filamentId ? { _id: filamentId } : undefined
}}
/>
</Modal>
<InfoCollapse <InfoCollapse
title='Notes' title='Notes'
icon={<NoteIcon />} icon={<NoteIcon />}

View File

@ -1,6 +1,7 @@
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import DashboardSidebar from '../common/DashboardSidebar' import DashboardSidebar from '../common/DashboardSidebar'
import FilamentIcon from '../../Icons/FilamentIcon' import FilamentIcon from '../../Icons/FilamentIcon'
import FilamentSkuIcon from '../../Icons/FilamentSkuIcon'
import PartIcon from '../../Icons/PartIcon' import PartIcon from '../../Icons/PartIcon'
import PartSkuIcon from '../../Icons/PartSkuIcon' import PartSkuIcon from '../../Icons/PartSkuIcon'
import ProductIcon from '../../Icons/ProductIcon' import ProductIcon from '../../Icons/ProductIcon'
@ -32,6 +33,12 @@ const items = [
label: 'Filaments', label: 'Filaments',
path: '/dashboard/management/filaments' path: '/dashboard/management/filaments'
}, },
{
key: 'filamentSkus',
icon: <FilamentSkuIcon />,
label: 'Filament SKUs',
path: '/dashboard/management/filamentskus'
},
{ {
key: 'parts', key: 'parts',
icon: <PartIcon />, icon: <PartIcon />,
@ -186,6 +193,7 @@ if (import.meta.env.MODE === 'development') {
const routeKeyMap = { const routeKeyMap = {
'/dashboard/management/filaments': 'filaments', '/dashboard/management/filaments': 'filaments',
'/dashboard/management/filamentskus': 'filamentSkus',
'/dashboard/management/parts': 'parts', '/dashboard/management/parts': 'parts',
'/dashboard/management/partskus': 'partSkus', '/dashboard/management/partskus': 'partSkus',
'/dashboard/management/users': 'users', '/dashboard/management/users': 'users',

View File

@ -52,7 +52,7 @@ const NewPartSku = ({ onOk, reset, defaultValues }) => {
_id: false, _id: false,
createdAt: false, createdAt: false,
updatedAt: false, updatedAt: false,
sku: false, barcode: false,
part: false, part: false,
name: false, name: false,
description: false description: false
@ -72,6 +72,7 @@ const NewPartSku = ({ onOk, reset, defaultValues }) => {
column={1} column={1}
labelWidth={100} labelWidth={100}
visibleProperties={{ visibleProperties={{
barcode: true,
description: true description: true
}} }}
bordered={false} bordered={false}

View File

@ -53,7 +53,7 @@ const NewProductSku = ({ onOk, reset, defaultValues }) => {
_id: false, _id: false,
createdAt: false, createdAt: false,
updatedAt: false, updatedAt: false,
sku: false, barcode: false,
product: false, product: false,
name: false, name: false,
description: false, description: false,
@ -77,7 +77,7 @@ const NewProductSku = ({ onOk, reset, defaultValues }) => {
_id: false, _id: false,
createdAt: false, createdAt: false,
updatedAt: false, updatedAt: false,
sku: false, barcode: false,
product: false, product: false,
name: false, name: false,
description: false, description: false,
@ -107,6 +107,7 @@ const NewProductSku = ({ onOk, reset, defaultValues }) => {
column={1} column={1}
labelWidth={100} labelWidth={100}
visibleProperties={{ visibleProperties={{
barcode: true,
description: true description: true
}} }}
bordered={false} bordered={false}

View File

@ -521,11 +521,15 @@ const ObjectForm = forwardRef(
onEdit(allFormValues) onEdit(allFormValues)
} }
// Calculate computed values based on current form data // Recompute derived fields from the full current form snapshot so
const currentFormData = { // toggles like overridePrice/overrideCost are preserved while typing.
...(serverObjectData.current || {}), const currentFormData = mergeWith(
...changedValues {},
} serverObjectData.current || {},
objectData || {},
allFormValues,
arrayReplaceCustomizer
)
const computedEntries = calculateComputedValues( const computedEntries = calculateComputedValues(
currentFormData, currentFormData,
model model

View File

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

View File

@ -1,6 +1,7 @@
import { Printer } from './models/Printer.js' import { Printer } from './models/Printer.js'
import { Host } from './models/Host.js' import { Host } from './models/Host.js'
import { Filament } from './models/Filament.js' import { Filament } from './models/Filament.js'
import { FilamentSku } from './models/FilamentSku.js'
import { Spool } from './models/Spool' import { Spool } from './models/Spool'
import { GCodeFile } from './models/GCodeFile' import { GCodeFile } from './models/GCodeFile'
import { Job } from './models/Job' import { Job } from './models/Job'
@ -43,6 +44,7 @@ export const objectModels = [
Printer, Printer,
Host, Host,
Filament, Filament,
FilamentSku,
Spool, Spool,
GCodeFile, GCodeFile,
Job, Job,
@ -86,6 +88,7 @@ export {
Printer, Printer,
Host, Host,
Filament, Filament,
FilamentSku,
Spool, Spool,
GCodeFile, GCodeFile,
Job, Job,

View File

@ -4,6 +4,7 @@ import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon' import ReloadIcon from '../../components/Icons/ReloadIcon'
import CheckIcon from '../../components/Icons/CheckIcon' import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon' import XMarkIcon from '../../components/Icons/XMarkIcon'
import PlusIcon from '../../components/Icons/PlusIcon'
export const Filament = { export const Filament = {
name: 'filament', name: 'filament',
@ -56,31 +57,33 @@ export const Filament = {
visible: (objectData) => { visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true return objectData?._isEditing && objectData?._isEditing == true
} }
},
{ type: 'divider' },
{
name: 'newFilamentSku',
label: 'New Filament SKU',
type: 'button',
icon: PlusIcon,
url: (_id) =>
`/dashboard/management/filaments/info?filamentId=${_id}&action=newFilamentSku`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
}
} }
], ],
columns: [ columns: [
'_reference', '_reference',
'name', 'name',
'type', 'type',
'color',
'vendor',
'cost',
'density', 'density',
'diameter', 'diameter',
'cost',
'createdAt', 'createdAt',
'updatedAt' 'updatedAt'
], ],
filters: ['_id', 'name', 'type', 'color', 'cost', 'vendor'], filters: ['_id', 'name', 'type'],
sorters: [ sorters: ['name', 'createdAt', 'type', 'updatedAt'],
'name', group: ['diameter', 'type'],
'createdAt',
'type',
'vendor',
'cost',
'updatedAt',
'createdAt'
],
group: ['diameter', 'type', 'vendor'],
properties: [ properties: [
{ {
name: '_id', name: '_id',
@ -109,14 +112,6 @@ export const Filament = {
type: 'dateTime', type: 'dateTime',
readOnly: true readOnly: true
}, },
{
name: 'vendor',
label: 'Vendor',
required: true,
type: 'object',
objectType: 'vendor',
showHyperlink: true
},
{ {
name: 'type', name: 'type',
label: 'Material', label: 'Material',
@ -124,53 +119,6 @@ export const Filament = {
columnWidth: 150, columnWidth: 150,
type: 'material' type: 'material'
}, },
{
name: 'cost',
label: 'Cost',
columnWidth: 150,
required: true,
type: 'number',
prefix: '£'
},
{
name: 'costWithTax',
label: 'Cost w/ Tax',
columnWidth: 150,
required: true,
readOnly: true,
type: 'number',
prefix: '£',
value: (objectData) => {
if (objectData?.costTaxRate?.rateType == 'percentage') {
return (
(
objectData?.cost *
(1 + objectData?.costTaxRate?.rate / 100)
).toFixed(2) || undefined
)
} else if (objectData?.costTaxRate?.rateType == 'amount') {
return (
(objectData?.cost + objectData?.costTaxRate?.rate).toFixed(2) ||
undefined
)
}
}
},
{
name: 'costTaxRate',
label: 'Cost Tax Rate',
required: true,
type: 'object',
objectType: 'taxRate',
showHyperlink: true
},
{
name: 'color',
label: 'Color',
columnWidth: 150,
required: true,
type: 'color'
},
{ {
name: 'diameter', name: 'diameter',
label: 'Diameter', label: 'Diameter',
@ -195,6 +143,47 @@ export const Filament = {
type: 'number', type: 'number',
suffix: 'g' suffix: 'g'
}, },
{
name: 'cost',
label: 'Cost',
required: false,
columnWidth: 120,
type: 'number',
prefix: '£',
min: 0,
step: 0.01
},
{
name: 'costWithTax',
label: 'Cost w/ Tax',
required: false,
readOnly: true,
type: 'number',
prefix: '£',
min: 0,
step: 0.01,
value: (objectData) => {
const cost = objectData?.cost
if (!cost) return undefined
if (objectData?.costTaxRate?.rateType == 'percentage') {
return (
(cost * (1 + objectData?.costTaxRate?.rate / 100)).toFixed(2) ||
undefined
)
} else if (objectData?.costTaxRate?.rateType == 'amount') {
return (cost + objectData?.costTaxRate?.rate).toFixed(2) || undefined
}
return cost
}
},
{
name: 'costTaxRate',
label: 'Cost Tax Rate',
required: false,
type: 'object',
objectType: 'taxRate',
showHyperlink: true
},
{ {
name: 'url', name: 'url',
label: 'Link', label: 'Link',

View File

@ -0,0 +1,184 @@
import FilamentSkuIcon from '../../components/Icons/FilamentSkuIcon'
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 ReloadIcon from '../../components/Icons/ReloadIcon'
export const FilamentSku = {
name: 'filamentSku',
label: 'Filament SKU',
prefix: 'FSU',
icon: FilamentSkuIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/management/filamentskus/info?filamentSkuId=${_id}`
},
{
name: 'reload',
label: 'Reload',
icon: ReloadIcon,
url: (_id) =>
`/dashboard/management/filamentskus/info?filamentSkuId=${_id}&action=reload`
},
{
name: 'edit',
label: 'Edit',
row: true,
icon: EditIcon,
url: (_id) =>
`/dashboard/management/filamentskus/info?filamentSkuId=${_id}&action=edit`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
}
},
{
name: 'finishEdit',
label: 'Save Edits',
icon: CheckIcon,
url: (_id) =>
`/dashboard/management/filamentskus/info?filamentSkuId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'cancelEdit',
label: 'Cancel Edits',
icon: XMarkIcon,
url: (_id) =>
`/dashboard/management/filamentskus/info?filamentSkuId=${_id}&action=cancelEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{ type: 'divider' },
{
name: 'delete',
label: 'Delete',
icon: BinIcon,
danger: true,
url: (_id) =>
`/dashboard/management/filamentskus/info?filamentSkuId=${_id}&action=delete`
}
],
url: (id) => `/dashboard/management/filamentskus/info?filamentSkuId=${id}`,
columns: ['_reference', 'barcode', 'filament', 'name', 'color', 'overrideCost', 'cost', 'createdAt', 'updatedAt'],
filters: ['_id', 'barcode', 'filament', 'filament._id', 'name', 'color', 'cost'],
sorters: ['barcode', 'filament', 'name', 'color', 'cost', 'createdAt', 'updatedAt'],
properties: [
{
name: '_id',
label: 'ID',
type: 'id',
objectType: 'filamentSku',
showCopy: true,
readOnly: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
{
name: 'name',
label: 'Name',
required: true,
type: 'text'
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true
},
{
name: 'filament',
label: 'Filament',
type: 'object',
objectType: 'filament',
required: true,
showHyperlink: true
},
{
name: 'barcode',
label: 'Barcode',
required: false,
type: 'text'
},
{
name: 'description',
label: 'Description',
required: false,
type: 'text'
},
{
name: 'color',
label: 'Color',
required: true,
type: 'color'
},
{
name: 'overrideCost',
label: 'Override Cost',
required: false,
type: 'bool',
value: (objectData) => objectData?.overrideCost ?? false
},
{
name: 'cost',
label: 'Cost',
required: false,
type: 'number',
prefix: '£',
min: 0,
step: 0.01,
disabled: (objectData) => !objectData?.overrideCost,
value: (objectData) =>
objectData?.overrideCost ? objectData?.cost : undefined
},
{
name: 'costWithTax',
label: 'Cost w/ Tax',
required: false,
readOnly: true,
type: 'number',
prefix: '£',
min: 0,
step: 0.01,
disabled: (objectData) => !objectData?.overrideCost,
value: (objectData) => {
if (!objectData?.overrideCost) return undefined
const cost = objectData?.cost
const taxRate = objectData?.costTaxRate
if (!cost) return undefined
if (taxRate?.rateType == 'percentage') {
return (
(cost * (1 + taxRate?.rate / 100)).toFixed(2) || undefined
)
} else if (taxRate?.rateType == 'amount') {
return (cost + taxRate?.rate).toFixed(2) || undefined
}
return cost
}
},
{
name: 'costTaxRate',
label: 'Cost Tax Rate',
required: false,
type: 'object',
objectType: 'taxRate',
showHyperlink: true,
disabled: (objectData) => !objectData?.overrideCost,
value: (objectData) =>
objectData?.overrideCost ? objectData?.costTaxRate : undefined
}
]
}

View File

@ -22,13 +22,13 @@ export const FilamentStock = {
'state', 'state',
'currentWeight', 'currentWeight',
'startingWeight', 'startingWeight',
'filament', 'filamentSku',
'createdAt', 'createdAt',
'updatedAt' 'updatedAt'
], ],
filters: ['_id'], filters: ['_id'],
sorters: ['createdAt', 'updatedAt'], sorters: ['createdAt', 'updatedAt'],
group: ['filament'], group: ['filamentSku'],
properties: [ properties: [
{ {
name: '_id', name: '_id',
@ -58,10 +58,10 @@ export const FilamentStock = {
readOnly: true readOnly: true
}, },
{ {
name: 'filament', name: 'filamentSku',
label: 'Filament', label: 'Filament SKU',
type: 'object', type: 'object',
objectType: 'filament', objectType: 'filamentSku',
readOnly: true, readOnly: true,
initial: true, initial: true,
required: true, required: true,
@ -93,7 +93,7 @@ export const FilamentStock = {
required: true, required: true,
columnWidth: 300, columnWidth: 300,
difference: (objectData) => { difference: (objectData) => {
return objectData?.filament?.emptySpoolWeight return objectData?.filamentSku?.filament?.emptySpoolWeight
} }
} }
], ],

View File

@ -71,7 +71,7 @@ export const GCodeFile = {
columns: [ columns: [
'name', 'name',
'_reference', '_reference',
'filament', 'filamentSku',
'gcodeFileInfo.estimatedPrintingTimeNormalMode', 'gcodeFileInfo.estimatedPrintingTimeNormalMode',
'gcodeFileInfo.sparseInfillDensity', 'gcodeFileInfo.sparseInfillDensity',
'gcodeFileInfo.sparseInfillPattern', 'gcodeFileInfo.sparseInfillPattern',
@ -81,7 +81,7 @@ export const GCodeFile = {
], ],
filters: ['_id', 'name', 'updatedAt'], filters: ['_id', 'name', 'updatedAt'],
sorters: ['name', 'createdAt', 'updatedAt'], sorters: ['name', 'createdAt', 'updatedAt'],
group: ['filament'], group: ['filamentSku'],
properties: [ properties: [
{ {
name: '_id', name: '_id',
@ -125,11 +125,11 @@ export const GCodeFile = {
filter: ['.gcode', '.g'] filter: ['.gcode', '.g']
}, },
{ {
name: 'filament', name: 'filamentSku',
label: 'Filament', label: 'Filament SKU',
type: 'object', type: 'object',
value: null, value: null,
objectType: 'filament', objectType: 'filamentSku',
required: true, required: true,
showHyperlink: true showHyperlink: true
}, },
@ -139,10 +139,11 @@ export const GCodeFile = {
type: 'number', type: 'number',
roundNumber: 2, roundNumber: 2,
value: (objectData) => { value: (objectData) => {
return ( const fs = objectData?.filamentSku
objectData?.file?.metaData?.filamentUsedG * const costPerKg =
(objectData?.filament?.cost / 1000) fs?.overrideCost ? fs?.cost : fs?.filament?.cost
) if (!costPerKg || !objectData?.file?.metaData?.filamentUsedG) return undefined
return objectData.file.metaData.filamentUsedG * (costPerKg / 1000)
}, },
readOnly: true, readOnly: true,
prefix: '£' prefix: '£'

View File

@ -78,7 +78,7 @@ export const OrderItem = {
} }
], ],
group: [], group: [],
filters: ['itemType', 'item', 'order'], filters: ['itemType', 'item', 'sku', 'order'],
sorters: ['createdAt', 'updatedAt', 'itemAmount', 'quantity'], sorters: ['createdAt', 'updatedAt', 'itemAmount', 'quantity'],
columns: [ columns: [
'_reference', '_reference',
@ -86,6 +86,7 @@ export const OrderItem = {
'state', 'state',
'itemType', 'itemType',
'item', 'item',
'sku',
'itemAmount', 'itemAmount',
'quantity', 'quantity',
'totalAmount', 'totalAmount',
@ -141,7 +142,7 @@ export const OrderItem = {
type: 'text', type: 'text',
readOnly: true, readOnly: true,
value: (objectData) => { value: (objectData) => {
return objectData?.item?.name return objectData?.sku?.name ?? objectData?.item?.name
} }
}, },
{ {
@ -213,6 +214,35 @@ export const OrderItem = {
showHyperlink: true, showHyperlink: true,
columnWidth: 300 columnWidth: 300
}, },
{
name: 'sku',
label: 'SKU',
type: 'object',
objectType: (objectData) => {
if (objectData?.itemType === 'filament') return 'filamentSku'
if (objectData?.itemType === 'part') return 'partSku'
if (objectData?.itemType === 'product') return 'productSku'
return undefined
},
required: false,
showHyperlink: true,
columnWidth: 300,
visible: (objectData) =>
['filament', 'part', 'product'].includes(objectData?.itemType),
masterFilter: (objectData) => {
console.log(objectData)
if (objectData?.itemType === 'filament' && objectData?.item?._id) {
return { filament: objectData.item._id }
}
if (objectData?.itemType === 'part' && objectData?.item?._id) {
return { part: objectData.item._id }
}
if (objectData?.itemType === 'product' && objectData?.item?._id) {
return { product: objectData.item._id }
}
return undefined
}
},
{ {
name: 'syncAmount', name: 'syncAmount',
label: 'Sync Amount', label: 'Sync Amount',
@ -239,11 +269,24 @@ export const OrderItem = {
}, },
columnWidth: 150, columnWidth: 150,
value: (objectData) => { value: (objectData) => {
if (objectData?.item?.cost && objectData?.syncAmount == 'itemCost') { const sku = objectData?.sku
return objectData?.item?.cost || undefined const item = objectData?.item
if (objectData?.syncAmount == 'itemCost') {
const cost =
sku && sku.overrideCost ? sku.cost : (item?.cost ?? sku?.cost)
return cost ?? objectData?.itemAmount
} }
if (objectData?.item?.price && objectData?.syncAmount == 'itemPrice') { if (objectData?.syncAmount == 'itemPrice') {
return objectData?.item?.price || undefined if (sku && sku.overridePrice) {
return sku.price ?? objectData?.itemAmount
}
const priceMode = item?.priceMode ?? sku?.priceMode
const margin = item?.margin ?? sku?.margin
const cost = item?.cost ?? sku?.cost
if (priceMode == 'margin' && margin != null && cost != null) {
return cost * (1 + margin / 100)
}
return item?.price ?? sku?.price ?? objectData?.itemAmount
} }
return objectData?.itemAmount || undefined return objectData?.itemAmount || undefined
} }
@ -283,19 +326,19 @@ export const OrderItem = {
objectType: 'taxRate', objectType: 'taxRate',
showHyperlink: true, showHyperlink: true,
value: (objectData) => { value: (objectData) => {
if ( const sku = objectData?.sku
objectData?.item?.costTaxRate?._id && const item = objectData?.item
objectData?.syncAmount == 'itemCost' if (objectData?.syncAmount == 'itemCost') {
) { const source = sku && sku.overrideCost ? sku : item
return objectData?.item?.costTaxRate || undefined return source?.costTaxRate ?? sku?.costTaxRate ?? objectData?.taxRate
} else if (
objectData?.item?.priceTaxRate?._id &&
objectData?.syncAmount == 'itemPrice'
) {
return objectData?.item?.priceTaxRate || undefined
} else {
return objectData?.taxRate || undefined
} }
if (objectData?.syncAmount == 'itemPrice') {
const source = sku && sku.overridePrice ? sku : item
return (
source?.priceTaxRate ?? sku?.priceTaxRate ?? objectData?.taxRate
)
}
return objectData?.taxRate || undefined
}, },
readOnly: (objectData) => { readOnly: (objectData) => {
return objectData?.syncAmount != null return objectData?.syncAmount != null

View File

@ -71,7 +71,7 @@ export const Part = {
} }
} }
], ],
columns: ['name', '_reference', 'createdAt'], columns: ['name', '_reference', 'cost', 'price', 'createdAt'],
filters: ['name', '_id'], filters: ['name', '_id'],
sorters: ['name', 'createdAt', '_id'], sorters: ['name', 'createdAt', '_id'],
properties: [ properties: [
@ -116,6 +116,127 @@ export const Part = {
value: null, value: null,
required: false, required: false,
showHyperlink: true showHyperlink: true
},
{
name: 'cost',
label: 'Cost',
required: false,
type: 'number',
prefix: '£',
min: 0,
step: 0.01
},
{
name: 'costWithTax',
label: 'Cost w/ Tax',
required: false,
readOnly: true,
type: 'number',
prefix: '£',
min: 0,
step: 0.01,
value: (objectData) => {
const cost = objectData?.cost
if (!cost) return undefined
if (objectData?.costTaxRate?.rateType == 'percentage') {
return (
(cost * (1 + objectData?.costTaxRate?.rate / 100)).toFixed(2) ||
undefined
)
} else if (objectData?.costTaxRate?.rateType == 'amount') {
return (cost + objectData?.costTaxRate?.rate).toFixed(2) || undefined
}
return cost
}
},
{
name: 'costTaxRate',
label: 'Cost Tax Rate',
required: false,
type: 'object',
objectType: 'taxRate',
showHyperlink: true
},
{
name: 'priceMode',
label: 'Price Mode',
required: false,
type: 'priceMode'
},
{
name: 'price',
label: 'Price',
required: false,
type: 'number',
prefix: '£',
min: 0,
step: 0.1,
readOnly: (objectData) => objectData?.priceMode == 'margin',
value: (objectData) => {
if (
objectData?.priceMode == 'margin' &&
objectData?.margin !== undefined &&
objectData?.margin !== null &&
objectData?.cost != null
) {
return (
(objectData.cost * (1 + objectData.margin / 100)).toFixed(2) ||
undefined
)
}
return objectData?.price
}
},
{
name: 'margin',
label: 'Margin',
required: false,
type: 'number',
disabled: (objectData) => objectData?.priceMode == 'amount',
suffix: '%',
min: 0,
max: 100,
step: 0.01
},
{
name: 'priceWithTax',
label: 'Price w/ Tax',
required: false,
readOnly: true,
type: 'number',
prefix: '£',
min: 0,
step: 0.01,
value: (objectData) => {
let price
if (
objectData?.priceMode == 'margin' &&
objectData?.margin != null &&
objectData?.cost != null
) {
price = objectData.cost * (1 + objectData.margin / 100)
} else {
price = objectData?.price
}
if (!price) return undefined
if (objectData?.priceTaxRate?.rateType == 'percentage') {
return (
(price * (1 + objectData?.priceTaxRate?.rate / 100)).toFixed(2) ||
undefined
)
} else if (objectData?.priceTaxRate?.rateType == 'amount') {
return (price + objectData?.priceTaxRate?.rate).toFixed(2) || undefined
}
return price
}
},
{
name: 'priceTaxRate',
label: 'Price Tax Rate',
required: false,
type: 'object',
objectType: 'taxRate',
showHyperlink: true
} }
] ]
} }

View File

@ -69,9 +69,20 @@ export const PartSku = {
} }
], ],
url: (id) => `/dashboard/management/partskus/info?partSkuId=${id}`, url: (id) => `/dashboard/management/partskus/info?partSkuId=${id}`,
columns: ['_reference', 'sku', 'part', 'name', 'cost', 'price', 'createdAt', 'updatedAt'], columns: [
filters: ['_id', 'sku', 'part', 'part._id', 'name', 'cost', 'price'], '_reference',
sorters: ['sku', 'part', 'name', 'cost', 'price', 'createdAt', 'updatedAt'], 'barcode',
'part',
'name',
'overrideCost',
'cost',
'overridePrice',
'price',
'createdAt',
'updatedAt'
],
filters: ['_id', 'barcode', 'part', 'part._id', 'name', 'cost', 'price'],
sorters: ['barcode', 'part', 'name', 'cost', 'price', 'createdAt', 'updatedAt'],
properties: [ properties: [
{ {
name: '_id', name: '_id',
@ -108,9 +119,9 @@ export const PartSku = {
showHyperlink: true showHyperlink: true
}, },
{ {
name: 'sku', name: 'barcode',
label: 'SKU', label: 'Barcode',
required: true, required: false,
type: 'text' type: 'text'
}, },
{ {
@ -125,6 +136,20 @@ export const PartSku = {
required: false, required: false,
type: 'priceMode' type: 'priceMode'
}, },
{
name: 'overrideCost',
label: 'Override Cost',
required: false,
type: 'bool',
value: (objectData) => objectData?.overrideCost ?? false
},
{
name: 'overridePrice',
label: 'Override Price',
required: false,
type: 'bool',
value: (objectData) => objectData?.overridePrice ?? false
},
{ {
name: 'cost', name: 'cost',
label: 'Cost', label: 'Cost',
@ -132,7 +157,10 @@ export const PartSku = {
type: 'number', type: 'number',
prefix: '£', prefix: '£',
min: 0, min: 0,
step: 0.01 step: 0.01,
disabled: (objectData) => !objectData?.overrideCost,
value: (objectData) =>
objectData?.overrideCost ? objectData?.cost : undefined
}, },
{ {
name: 'costWithTax', name: 'costWithTax',
@ -143,22 +171,18 @@ export const PartSku = {
prefix: '£', prefix: '£',
min: 0, min: 0,
step: 0.01, step: 0.01,
disabled: (objectData) => !objectData?.overrideCost,
value: (objectData) => { value: (objectData) => {
if (objectData?.costTaxRate?.rateType == 'percentage') { if (!objectData?.overrideCost) return undefined
return ( const cost = objectData?.cost
( const taxRate = objectData?.costTaxRate
objectData?.cost * if (!cost) return undefined
(1 + objectData?.costTaxRate?.rate / 100) if (taxRate?.rateType == 'percentage') {
).toFixed(2) || undefined return (cost * (1 + taxRate?.rate / 100)).toFixed(2) || undefined
) } else if (taxRate?.rateType == 'amount') {
} else if (objectData?.costTaxRate?.rateType == 'amount') { return (cost + taxRate?.rate).toFixed(2) || undefined
return (
(objectData?.cost + objectData?.costTaxRate?.rate).toFixed(2) ||
undefined
)
} else {
return objectData?.cost || undefined
} }
return cost
} }
}, },
{ {
@ -167,7 +191,10 @@ export const PartSku = {
required: false, required: false,
type: 'object', type: 'object',
objectType: 'taxRate', objectType: 'taxRate',
showHyperlink: true showHyperlink: true,
disabled: (objectData) => !objectData?.overrideCost,
value: (objectData) =>
objectData?.overrideCost ? objectData?.costTaxRate : undefined
}, },
{ {
name: 'price', name: 'price',
@ -177,22 +204,27 @@ export const PartSku = {
prefix: '£', prefix: '£',
min: 0, min: 0,
step: 0.1, step: 0.1,
readOnly: (objectData) => { disabled: (objectData) => !objectData?.overridePrice,
return objectData?.priceMode == 'margin' readOnly: (objectData) =>
}, objectData?.overridePrice && objectData?.priceMode == 'margin',
value: (objectData) => { value: (objectData) => {
if (!objectData?.overridePrice) return undefined
const priceMode = objectData?.priceMode ?? objectData?.part?.priceMode
const cost = objectData?.overrideCost
? objectData?.cost
: objectData?.part?.cost
const margin = objectData?.margin ?? objectData?.part?.margin
if ( if (
objectData?.priceMode == 'margin' && priceMode == 'margin' &&
objectData?.margin !== undefined && margin !== undefined &&
objectData?.margin !== null margin !== null &&
cost != null
) { ) {
return ( return (
(objectData?.cost * (1 + objectData?.margin / 100)).toFixed(2) || (cost * (1 + margin / 100)).toFixed(2) || undefined
undefined
) )
} else {
return objectData?.price || undefined
} }
return objectData?.price
} }
}, },
{ {
@ -204,22 +236,32 @@ export const PartSku = {
prefix: '£', prefix: '£',
min: 0, min: 0,
step: 0.01, step: 0.01,
disabled: (objectData) => !objectData?.overridePrice,
value: (objectData) => { value: (objectData) => {
if (objectData?.priceTaxRate?.rateType == 'percentage') { if (!objectData?.overridePrice) return undefined
return ( let price
( const priceMode = objectData?.priceMode ?? objectData?.part?.priceMode
objectData?.price * const cost = objectData?.overrideCost
(1 + objectData?.priceTaxRate?.rate / 100) ? objectData?.cost
).toFixed(2) || undefined : objectData?.part?.cost
) const margin = objectData?.margin ?? objectData?.part?.margin
} else if (objectData?.priceTaxRate?.rateType == 'amount') { if (
return ( priceMode == 'margin' &&
(objectData?.price + objectData?.priceTaxRate?.rate).toFixed(2) || margin != null &&
undefined cost != null
) ) {
price = cost * (1 + margin / 100)
} else { } else {
return objectData?.price price = objectData?.price
} }
if (price == null) return undefined
const taxRate = objectData?.priceTaxRate ?? objectData?.part?.priceTaxRate
if (taxRate?.rateType == 'percentage') {
return (price * (1 + taxRate?.rate / 100)).toFixed(2) || undefined
} else if (taxRate?.rateType == 'amount') {
return (price + taxRate?.rate).toFixed(2) || undefined
}
return price
} }
}, },
{ {
@ -227,25 +269,14 @@ export const PartSku = {
label: 'Margin', label: 'Margin',
required: false, required: false,
type: 'number', type: 'number',
disabled: (objectData) => { disabled: (objectData) =>
return objectData?.priceMode == 'amount' !objectData?.overridePrice || objectData?.priceMode == 'amount',
},
suffix: '%', suffix: '%',
min: 0, min: 0,
max: 100, max: 100,
step: 0.01 step: 0.01,
}, value: (objectData) =>
{ objectData?.overridePrice ? objectData?.margin : undefined
name: 'amount',
label: 'Amount',
disabled: (objectData) => {
return objectData?.priceMode == 'margin'
},
type: 'number',
required: false,
prefix: '£',
min: 0,
step: 0.1
}, },
{ {
name: 'priceTaxRate', name: 'priceTaxRate',
@ -253,15 +284,10 @@ export const PartSku = {
required: false, required: false,
type: 'object', type: 'object',
objectType: 'taxRate', objectType: 'taxRate',
showHyperlink: true showHyperlink: true,
}, disabled: (objectData) => !objectData?.overridePrice,
{ value: (objectData) =>
name: 'vendor', objectData?.overridePrice ? objectData?.priceTaxRate : undefined
label: 'Vendor',
required: false,
type: 'object',
objectType: 'vendor',
showHyperlink: true
} }
] ]
} }

View File

@ -78,6 +78,8 @@ export const Product = {
'name', 'name',
'tags', 'tags',
'vendor', 'vendor',
'cost',
'price',
'createdAt', 'createdAt',
'updatedAt' 'updatedAt'
], ],
@ -129,6 +131,127 @@ export const Product = {
label: 'Tags', label: 'Tags',
required: false, required: false,
type: 'tags' type: 'tags'
},
{
name: 'cost',
label: 'Cost',
required: false,
type: 'number',
prefix: '£',
min: 0,
step: 0.01
},
{
name: 'costWithTax',
label: 'Cost w/ Tax',
required: false,
readOnly: true,
type: 'number',
prefix: '£',
min: 0,
step: 0.01,
value: (objectData) => {
const cost = objectData?.cost
if (!cost) return undefined
if (objectData?.costTaxRate?.rateType == 'percentage') {
return (
(cost * (1 + objectData?.costTaxRate?.rate / 100)).toFixed(2) ||
undefined
)
} else if (objectData?.costTaxRate?.rateType == 'amount') {
return (cost + objectData?.costTaxRate?.rate).toFixed(2) || undefined
}
return cost
}
},
{
name: 'costTaxRate',
label: 'Cost Tax Rate',
required: false,
type: 'object',
objectType: 'taxRate',
showHyperlink: true
},
{
name: 'priceMode',
label: 'Price Mode',
required: false,
type: 'priceMode'
},
{
name: 'price',
label: 'Price',
required: false,
type: 'number',
prefix: '£',
min: 0,
step: 0.1,
readOnly: (objectData) => objectData?.priceMode == 'margin',
value: (objectData) => {
if (
objectData?.priceMode == 'margin' &&
objectData?.margin !== undefined &&
objectData?.margin !== null &&
objectData?.cost != null
) {
return (
(objectData.cost * (1 + objectData.margin / 100)).toFixed(2) ||
undefined
)
}
return objectData?.price
}
},
{
name: 'margin',
label: 'Margin',
required: false,
type: 'number',
disabled: (objectData) => objectData?.priceMode == 'amount',
suffix: '%',
min: 0,
max: 100,
step: 0.01
},
{
name: 'priceWithTax',
label: 'Price w/ Tax',
required: false,
readOnly: true,
type: 'number',
prefix: '£',
min: 0,
step: 0.01,
value: (objectData) => {
let price
if (
objectData?.priceMode == 'margin' &&
objectData?.margin != null &&
objectData?.cost != null
) {
price = objectData.cost * (1 + objectData.margin / 100)
} else {
price = objectData?.price
}
if (!price) return undefined
if (objectData?.priceTaxRate?.rateType == 'percentage') {
return (
(price * (1 + objectData?.priceTaxRate?.rate / 100)).toFixed(2) ||
undefined
)
} else if (objectData?.priceTaxRate?.rateType == 'amount') {
return (price + objectData?.priceTaxRate?.rate).toFixed(2) || undefined
}
return price
}
},
{
name: 'priceTaxRate',
label: 'Price Tax Rate',
required: false,
type: 'object',
objectType: 'taxRate',
showHyperlink: true
} }
] ]
} }

View File

@ -69,9 +69,9 @@ export const ProductSku = {
} }
], ],
url: (id) => `/dashboard/management/productskus/info?productSkuId=${id}`, url: (id) => `/dashboard/management/productskus/info?productSkuId=${id}`,
columns: ['_reference', 'sku', 'product', 'name', 'cost', 'price', 'createdAt', 'updatedAt'], columns: ['_reference', 'barcode', 'product', 'name', 'overrideCost', 'cost', 'overridePrice', 'price', 'createdAt', 'updatedAt'],
filters: ['_id', 'sku', 'product', 'product._id', 'name', 'cost', 'price'], filters: ['_id', 'barcode', 'product', 'product._id', 'name', 'cost', 'price'],
sorters: ['sku', 'product', 'name', 'cost', 'price', 'createdAt', 'updatedAt'], sorters: ['barcode', 'product', 'name', 'cost', 'price', 'createdAt', 'updatedAt'],
properties: [ properties: [
{ {
name: '_id', name: '_id',
@ -108,9 +108,9 @@ export const ProductSku = {
showHyperlink: true showHyperlink: true
}, },
{ {
name: 'sku', name: 'barcode',
label: 'SKU', label: 'Barcode',
required: true, required: false,
type: 'text' type: 'text'
}, },
{ {
@ -125,6 +125,20 @@ export const ProductSku = {
required: false, required: false,
type: 'priceMode' type: 'priceMode'
}, },
{
name: 'overrideCost',
label: 'Override Cost',
required: false,
type: 'bool',
value: (objectData) => objectData?.overrideCost ?? false
},
{
name: 'overridePrice',
label: 'Override Price',
required: false,
type: 'bool',
value: (objectData) => objectData?.overridePrice ?? false
},
{ {
name: 'cost', name: 'cost',
label: 'Cost', label: 'Cost',
@ -132,7 +146,10 @@ export const ProductSku = {
type: 'number', type: 'number',
prefix: '£', prefix: '£',
min: 0, min: 0,
step: 0.01 step: 0.01,
disabled: (objectData) => !objectData?.overrideCost,
value: (objectData) =>
objectData?.overrideCost ? objectData?.cost : undefined
}, },
{ {
name: 'costWithTax', name: 'costWithTax',
@ -143,22 +160,20 @@ export const ProductSku = {
prefix: '£', prefix: '£',
min: 0, min: 0,
step: 0.01, step: 0.01,
disabled: (objectData) => !objectData?.overrideCost,
value: (objectData) => { value: (objectData) => {
if (objectData?.costTaxRate?.rateType == 'percentage') { if (!objectData?.overrideCost) return undefined
const cost = objectData?.cost
const taxRate = objectData?.costTaxRate
if (!cost) return undefined
if (taxRate?.rateType == 'percentage') {
return ( return (
( (cost * (1 + taxRate?.rate / 100)).toFixed(2) || undefined
objectData?.cost *
(1 + objectData?.costTaxRate?.rate / 100)
).toFixed(2) || undefined
) )
} else if (objectData?.costTaxRate?.rateType == 'amount') { } else if (taxRate?.rateType == 'amount') {
return ( return (cost + taxRate?.rate).toFixed(2) || undefined
(objectData?.cost + objectData?.costTaxRate?.rate).toFixed(2) ||
undefined
)
} else {
return objectData?.cost || undefined
} }
return cost
} }
}, },
{ {
@ -167,7 +182,10 @@ export const ProductSku = {
required: false, required: false,
type: 'object', type: 'object',
objectType: 'taxRate', objectType: 'taxRate',
showHyperlink: true showHyperlink: true,
disabled: (objectData) => !objectData?.overrideCost,
value: (objectData) =>
objectData?.overrideCost ? objectData?.costTaxRate : undefined
}, },
{ {
name: 'price', name: 'price',
@ -177,22 +195,27 @@ export const ProductSku = {
prefix: '£', prefix: '£',
min: 0, min: 0,
step: 0.1, step: 0.1,
readOnly: (objectData) => { disabled: (objectData) => !objectData?.overridePrice,
return objectData?.priceMode == 'margin' readOnly: (objectData) =>
}, objectData?.overridePrice && objectData?.priceMode == 'margin',
value: (objectData) => { value: (objectData) => {
if (!objectData?.overridePrice) return undefined
const priceMode = objectData?.priceMode ?? objectData?.product?.priceMode
const cost = objectData?.overrideCost
? objectData?.cost
: objectData?.product?.cost
const margin = objectData?.margin ?? objectData?.product?.margin
if ( if (
objectData?.priceMode == 'margin' && priceMode == 'margin' &&
objectData?.margin !== undefined && margin !== undefined &&
objectData?.margin !== null margin !== null &&
cost != null
) { ) {
return ( return (
(objectData?.cost * (1 + objectData?.margin / 100)).toFixed(2) || (cost * (1 + margin / 100)).toFixed(2) || undefined
undefined
) )
} else {
return objectData?.price || undefined
} }
return objectData?.price
} }
}, },
{ {
@ -204,22 +227,34 @@ export const ProductSku = {
prefix: '£', prefix: '£',
min: 0, min: 0,
step: 0.01, step: 0.01,
disabled: (objectData) => !objectData?.overridePrice,
value: (objectData) => { value: (objectData) => {
if (objectData?.priceTaxRate?.rateType == 'percentage') { if (!objectData?.overridePrice) return undefined
return ( let price
( const priceMode = objectData?.priceMode ?? objectData?.product?.priceMode
objectData?.price * const cost = objectData?.overrideCost
(1 + objectData?.priceTaxRate?.rate / 100) ? objectData?.cost
).toFixed(2) || undefined : objectData?.product?.cost
) const margin = objectData?.margin ?? objectData?.product?.margin
} else if (objectData?.priceTaxRate?.rateType == 'amount') { if (
return ( priceMode == 'margin' &&
(objectData?.price + objectData?.priceTaxRate?.rate).toFixed(2) || margin != null &&
undefined cost != null
) ) {
price = cost * (1 + margin / 100)
} else { } else {
return objectData?.price price = objectData?.price
} }
if (price == null) return undefined
const taxRate = objectData?.priceTaxRate ?? objectData?.product?.priceTaxRate
if (taxRate?.rateType == 'percentage') {
return (
(price * (1 + taxRate?.rate / 100)).toFixed(2) || undefined
)
} else if (taxRate?.rateType == 'amount') {
return (price + taxRate?.rate).toFixed(2) || undefined
}
return price
} }
}, },
{ {
@ -227,25 +262,14 @@ export const ProductSku = {
label: 'Margin', label: 'Margin',
required: false, required: false,
type: 'number', type: 'number',
disabled: (objectData) => { disabled: (objectData) =>
return objectData?.priceMode == 'amount' !objectData?.overridePrice || objectData?.priceMode == 'amount',
},
suffix: '%', suffix: '%',
min: 0, min: 0,
max: 100, max: 100,
step: 0.01 step: 0.01,
}, value: (objectData) =>
{ objectData?.overridePrice ? objectData?.margin : undefined
name: 'amount',
label: 'Amount',
disabled: (objectData) => {
return objectData?.priceMode == 'margin'
},
type: 'number',
required: false,
prefix: '£',
min: 0,
step: 0.1
}, },
{ {
name: 'priceTaxRate', name: 'priceTaxRate',
@ -253,15 +277,10 @@ export const ProductSku = {
required: false, required: false,
type: 'object', type: 'object',
objectType: 'taxRate', objectType: 'taxRate',
showHyperlink: true showHyperlink: true,
}, disabled: (objectData) => !objectData?.overridePrice,
{ value: (objectData) =>
name: 'vendor', objectData?.overridePrice ? objectData?.priceTaxRate : undefined
label: 'Vendor',
required: false,
type: 'object',
objectType: 'vendor',
showHyperlink: true
}, },
{ {
name: 'parts', name: 'parts',

View File

@ -3,6 +3,8 @@ import { Route } from 'react-router-dom'
const Filaments = lazy(() => import('../components/Dashboard/Management/Filaments')) const Filaments = lazy(() => import('../components/Dashboard/Management/Filaments'))
const FilamentInfo = lazy(() => import('../components/Dashboard/Management/Filaments/FilamentInfo.jsx')) const FilamentInfo = lazy(() => import('../components/Dashboard/Management/Filaments/FilamentInfo.jsx'))
const FilamentSkus = lazy(() => import('../components/Dashboard/Management/FilamentSkus.jsx'))
const FilamentSkuInfo = lazy(() => import('../components/Dashboard/Management/FilamentSkus/FilamentSkuInfo.jsx'))
const Parts = lazy(() => import('../components/Dashboard/Management/Parts.jsx')) const Parts = lazy(() => import('../components/Dashboard/Management/Parts.jsx'))
const PartInfo = lazy(() => import('../components/Dashboard/Management/Parts/PartInfo.jsx')) const PartInfo = lazy(() => import('../components/Dashboard/Management/Parts/PartInfo.jsx'))
const PartSkus = lazy(() => import('../components/Dashboard/Management/PartSkus.jsx')) const PartSkus = lazy(() => import('../components/Dashboard/Management/PartSkus.jsx'))
@ -52,6 +54,12 @@ const ManagementRoutes = [
path='management/filaments/info' path='management/filaments/info'
element={<FilamentInfo />} element={<FilamentInfo />}
/>, />,
<Route key='filamentskus' path='management/filamentskus' element={<FilamentSkus />} />,
<Route
key='filamentskus-info'
path='management/filamentskus/info'
element={<FilamentSkuInfo />}
/>,
<Route key='parts' path='management/parts' element={<Parts />} />, <Route key='parts' path='management/parts' element={<Parts />} />,
<Route <Route
key='parts-info' key='parts-info'