Implemented Part SKUs.

This commit is contained in:
Tom Butcher 2026-03-07 23:34:07 +00:00
parent cb9e7fcc23
commit 5ba205c6cc
19 changed files with 1141 additions and 296 deletions

View File

@ -0,0 +1,12 @@
<?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(1,0,0,1,-0.5,-4)">
<path d="M59.063,37.932C60.996,39.115 62.286,41.246 62.286,43.675L62.286,59.025C62.286,62.739 59.271,65.754 55.558,65.754L9.442,65.754C5.729,65.754 2.714,62.739 2.714,59.025L2.714,43.675C2.714,41.692 3.574,39.908 4.941,38.676L4.941,23.857C4.941,21.49 6.144,19.403 8.186,18.218L28.762,6.357C30.813,5.136 33.19,5.136 35.263,6.357L55.817,18.218C57.859,19.403 59.063,21.49 59.063,23.857L59.063,37.932ZM10.076,36.947L27.371,36.947L10.076,27.093L10.076,36.947ZM36.574,36.947L53.923,36.947L53.923,27.065L36.574,36.947ZM31.984,33.577L52.082,22.106C51.964,21.992 51.921,21.927 51.713,21.8L33.492,11.283C32.55,10.719 31.453,10.719 30.511,11.283L12.312,21.8C12.083,21.927 12.03,21.997 11.892,22.125L31.984,33.577ZM58.286,43.675C58.286,42.169 57.064,40.947 55.558,40.947L9.442,40.947C7.936,40.947 6.714,42.169 6.714,43.675L6.714,59.025C6.714,60.531 7.936,61.754 9.442,61.754L55.558,61.754C57.064,61.754 58.286,60.531 58.286,59.025L58.286,43.675Z"/>
<g transform="matrix(1.711502,0,0,1.711502,-43.419476,-29.526577)">
<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: 3.3 KiB

View File

@ -2,11 +2,11 @@
<!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.983114,0,0,0.983114,0.872652,-3.62694)">
<path d="M17.414,66.503C16.713,66.492 16.015,66.297 15.368,65.918L3.47,59.042C2.153,58.292 1.364,56.929 1.364,55.39L1.364,41.653C1.364,40.114 2.153,38.751 3.47,37.993L15.368,31.125C15.408,31.101 15.448,31.079 15.488,31.057C15.484,30.981 15.482,30.906 15.482,30.83L15.482,17.094C15.482,15.554 16.271,14.192 17.588,13.433L29.487,6.565C30.819,5.777 32.374,5.777 33.714,6.565L45.605,13.433C46.922,14.192 47.71,15.554 47.71,17.094L47.71,30.83C47.71,30.882 47.71,30.934 47.708,30.986C47.794,31.029 47.879,31.075 47.963,31.125L59.854,37.993C61.171,38.751 61.96,40.114 61.96,41.653L61.96,60.463C61.96,63.797 59.253,66.504 55.919,66.504L17.543,66.503C17.5,66.504 17.457,66.504 17.414,66.503ZM56.328,40.656L56.5,40.558C56.447,40.512 56.431,40.489 56.316,40.42L46.646,34.839C46.14,34.532 45.551,34.532 45.045,34.839L35.382,40.42C35.26,40.489 35.229,40.52 35.145,40.589L35.239,40.643L55.919,40.643C56.057,40.643 56.193,40.647 56.328,40.656ZM27.985,40.643L28.117,40.567L17.928,34.679C17.513,34.548 17.07,34.601 16.678,34.839L7.015,40.42C6.892,40.489 6.862,40.52 6.777,40.589L15.655,45.672C16.137,42.819 18.622,40.643 21.612,40.643L27.985,40.643ZM57.891,46.683C57.891,45.595 57.008,44.711 55.919,44.711L21.612,44.711C20.523,44.711 19.64,45.595 19.64,46.683L19.64,60.463C19.64,61.551 20.523,62.435 21.612,62.435L55.919,62.435C57.008,62.435 57.891,61.551 57.891,60.463L57.891,46.683ZM29.663,36.94L29.663,25.531L19.341,19.62L19.341,30.103C19.341,30.57 19.539,30.987 19.878,31.288L29.663,36.94ZM33.53,36.986C33.721,36.887 33.805,36.833 34.005,36.718L43.047,31.489C43.553,31.19 43.844,30.685 43.844,30.103L43.844,19.574L33.53,25.478L33.53,36.986ZM31.554,22.132L42.251,15.999C42.197,15.953 42.182,15.93 42.067,15.861L32.397,10.279C31.891,9.973 31.302,9.973 30.796,10.279L21.133,15.861C21.011,15.93 20.98,15.96 20.896,16.029L31.554,22.132ZM15.545,61.546L15.545,50.091L5.223,44.18L5.223,54.662C5.223,55.244 5.529,55.75 6.035,56.048L15.261,61.385C15.391,61.462 15.414,61.477 15.545,61.546Z"/>
<g transform="matrix(1.193399,0,0,1.216202,-14.746685,-3.63953)">
<path d="M34.973,52.259C37.641,52.259 39.196,50.992 39.196,49.001C39.196,47.433 38.218,46.581 36.139,46.206L35.167,46.031C34.202,45.857 33.786,45.649 33.786,45.214C33.786,44.778 34.188,44.429 34.979,44.429C35.59,44.429 36.039,44.61 36.354,45.093C36.696,45.609 37.078,45.817 37.681,45.817C38.392,45.81 38.861,45.374 38.861,44.731C38.861,44.51 38.821,44.329 38.74,44.134C38.238,42.927 36.863,42.25 34.946,42.25C32.646,42.25 30.97,43.477 30.97,45.421C30.97,46.923 32.023,47.895 33.947,48.237L34.919,48.411C36.005,48.606 36.381,48.8 36.381,49.256C36.381,49.732 35.858,50.081 35.006,50.081C34.369,50.081 33.806,49.886 33.471,49.41C33.089,48.874 32.727,48.706 32.191,48.706C31.487,48.706 30.97,49.176 30.97,49.859C30.97,50.081 31.017,50.309 31.118,50.53C31.534,51.475 32.794,52.259 34.973,52.259Z" style="fill-rule:nonzero;"/>
<path d="M41.677,52.259C42.582,52.259 43.098,51.723 43.098,50.771L43.098,49.558L43.943,48.686L46.115,51.442C46.577,52.032 46.993,52.259 47.603,52.259C48.381,52.259 48.971,51.663 48.971,50.885C48.971,50.483 48.763,50.054 48.274,49.45L46.182,46.863L48.18,44.731C48.582,44.302 48.729,43.993 48.729,43.564C48.729,42.834 48.126,42.25 47.382,42.25C46.899,42.25 46.531,42.438 46.122,42.894L43.152,46.253L43.098,46.253L43.098,43.739C43.098,42.787 42.582,42.25 41.677,42.25C40.772,42.25 40.256,42.787 40.256,43.739L40.256,50.771C40.256,51.723 40.772,52.259 41.677,52.259Z" style="fill-rule:nonzero;"/>
<path d="M54.14,52.259C56.694,52.259 58.403,50.852 58.403,48.646L58.403,43.739C58.403,42.787 57.887,42.25 56.982,42.25C56.077,42.25 55.561,42.787 55.561,43.739L55.561,48.331C55.561,49.35 55.058,49.906 54.14,49.906C53.221,49.906 52.718,49.35 52.718,48.331L52.718,43.739C52.718,42.787 52.202,42.25 51.297,42.25C50.392,42.25 49.876,42.787 49.876,43.739L49.876,48.646C49.876,50.852 51.585,52.259 54.14,52.259Z" style="fill-rule:nonzero;"/>
<path d="M1.364,59.66L1.364,41.653C1.364,40.114 2.153,38.751 3.47,37.993L15.368,31.125C15.408,31.101 15.448,31.079 15.488,31.057C15.484,30.981 15.482,30.906 15.482,30.83L15.482,17.094C15.482,15.554 16.271,14.192 17.588,13.433L29.487,6.565C30.819,5.777 32.374,5.777 33.714,6.565L45.605,13.433C46.922,14.192 47.71,15.554 47.71,17.094L47.71,30.83C47.71,30.882 47.71,30.934 47.708,30.986C47.794,31.029 47.879,31.075 47.963,31.125L59.854,37.993C61.171,38.751 61.96,40.114 61.96,41.653L61.96,59.66C61.96,63.437 58.893,66.504 55.116,66.504L8.208,66.504C4.431,66.504 1.364,63.437 1.364,59.66ZM50.74,37.202L46.646,34.839C46.14,34.532 45.551,34.532 45.045,34.839L40.954,37.202L50.74,37.202ZM22.294,37.202L17.928,34.679C17.513,34.548 17.07,34.601 16.678,34.839L12.587,37.202L22.294,37.202ZM31.554,22.132L42.251,15.999C42.197,15.953 42.182,15.93 42.067,15.861L32.397,10.279C31.891,9.973 31.302,9.973 30.796,10.279L21.133,15.861C21.011,15.93 20.98,15.96 20.896,16.029L31.554,22.132ZM29.663,36.94L29.663,25.531L19.341,19.62L19.341,30.103C19.341,30.57 19.539,30.987 19.878,31.288L29.663,36.94ZM33.53,36.986C33.721,36.887 33.805,36.833 34.005,36.718L43.047,31.489C43.553,31.19 43.844,30.685 43.844,30.103L43.844,19.574L33.53,25.478L33.53,36.986ZM57.891,44.046C57.891,42.514 56.648,41.271 55.116,41.271L8.208,41.271C6.676,41.271 5.433,42.514 5.433,44.046L5.433,59.66C5.433,61.191 6.676,62.435 8.208,62.435L55.116,62.435C56.648,62.435 57.891,61.191 57.891,59.66L57.891,44.046Z"/>
<g transform="matrix(1.740899,0,0,1.740899,-45.561479,-30.413194)">
<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>

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -2,6 +2,7 @@ import { useLocation } from 'react-router-dom'
import DashboardSidebar from '../common/DashboardSidebar'
import FilamentIcon from '../../Icons/FilamentIcon'
import PartIcon from '../../Icons/PartIcon'
import PartSkuIcon from '../../Icons/PartSkuIcon'
import ProductIcon from '../../Icons/ProductIcon'
import ProductSkuIcon from '../../Icons/ProductSkuIcon'
import VendorIcon from '../../Icons/VendorIcon'
@ -37,6 +38,12 @@ const items = [
label: 'Parts',
path: '/dashboard/management/parts'
},
{
key: 'partSkus',
icon: <PartSkuIcon />,
label: 'Part SKUs',
path: '/dashboard/management/partskus'
},
{
key: 'products',
icon: <ProductIcon />,
@ -180,6 +187,7 @@ if (import.meta.env.MODE === 'development') {
const routeKeyMap = {
'/dashboard/management/filaments': 'filaments',
'/dashboard/management/parts': 'parts',
'/dashboard/management/partskus': 'partSkus',
'/dashboard/management/users': 'users',
'/dashboard/management/apppasswords': 'appPasswords',
'/dashboard/management/products': 'products',

View File

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

View File

@ -0,0 +1,128 @@
import PropTypes from 'prop-types'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
const NewPartSku = ({ onOk, reset, defaultValues }) => {
return (
<NewObjectForm
type='partSku'
reset={reset}
defaultValues={defaultValues}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='partSku'
column={1}
labelWidth={70}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
visibleProperties={{
description: false,
priceMode: false,
cost: false,
costWithTax: false,
costTaxRate: false,
price: false,
priceWithTax: false,
margin: false,
amount: false,
priceTaxRate: false,
vendor: false
}}
/>
)
},
{
title: 'Pricing',
key: 'pricing',
content: (
<ObjectInfo
type='partSku'
column={1}
labelWidth={100}
visibleProperties={{
_id: false,
createdAt: false,
updatedAt: false,
sku: false,
part: false,
name: false,
description: false
}}
bordered={false}
isEditing={true}
objectData={objectData}
/>
)
},
{
title: 'Optional',
key: 'optional',
content: (
<ObjectInfo
type='partSku'
column={1}
labelWidth={100}
visibleProperties={{
description: true
}}
bordered={false}
isEditing={true}
objectData={objectData}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='partSku'
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 Part SKU'
onSubmit={async () => {
const result = await handleSubmit()
if (result) {
onOk()
}
}}
/>
)
}}
</NewObjectForm>
)
}
NewPartSku.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool,
defaultValues: PropTypes.object
}
export default NewPartSku

View File

@ -0,0 +1,194 @@
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 PartSkuInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const partSkuId = new URLSearchParams(location.search).get('partSkuId')
const [collapseState, updateCollapseState] = useCollapseState('PartSkuInfo', {
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='partSku'
id={partSkuId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Part SKU Information' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<UserNotifierToggle
type='partSku'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
<DocumentPrintButton
type='partSku'
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='Part SKU Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) => updateCollapseState('info', expanded)}
collapseKey='info'
>
<ObjectForm
id={partSkuId}
type='partSku'
style={{ height: '100%' }}
ref={objectFormRef}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => (
<ObjectInfo
loading={loading}
isEditing={isEditing}
type='partSku'
objectData={objectData}
/>
)}
</ObjectForm>
</InfoCollapse>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={partSkuId} type='partSku' />
</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': partSkuId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
</>
)
}
export default PartSkuInfo

View File

@ -1,6 +1,6 @@
import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card } from 'antd'
import { Space, Flex, Card, Modal } from 'antd'
import useCollapseState from '../../hooks/useCollapseState'
import NotesPanel from '../../common/NotesPanel'
import InfoCollapse from '../../common/InfoCollapse'
@ -19,15 +19,19 @@ import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
import PartSkuIcon from '../../../Icons/PartSkuIcon.jsx'
import NewPartSku from '../PartSkus/NewPartSku'
const PartInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const partId = new URLSearchParams(location.search).get('partId')
const [newPartSkuOpen, setNewPartSkuOpen] = useState(false)
const partSkusTableRef = useRef()
const [collapseState, updateCollapseState] = useCollapseState('PartInfo', {
info: true,
parts: true,
partSkus: true,
notes: true,
auditLogs: true
})
@ -45,6 +49,10 @@ const PartInfo = () => {
objectFormRef?.current?.fetchObject?.()
return true
},
newPartSku: () => {
setNewPartSkuOpen(true)
return false
},
edit: () => {
objectFormRef?.current?.startEditing?.()
return false
@ -79,6 +87,7 @@ const PartInfo = () => {
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Part Information' },
{ key: 'partSkus', label: 'Part SKUs' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
@ -123,13 +132,6 @@ const PartInfo = () => {
actions={actions}
loading={objectFormState.loading}
ref={actionHandlerRef}
>
<InfoCollapse
title='Part Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) => updateCollapseState('info', expanded)}
collapseKey='info'
>
<ObjectForm
id={partId}
@ -141,16 +143,62 @@ const PartInfo = () => {
}}
>
{({ loading, isEditing, objectData }) => (
<InfoCollapse
title='Part Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) => updateCollapseState('info', expanded)}
collapseKey='info'
>
<ObjectInfo
loading={loading}
isEditing={isEditing}
type='part'
objectData={objectData}
/>
</InfoCollapse>
)}
</ObjectForm>
<InfoCollapse
title='Part SKUs'
icon={<PartSkuIcon />}
active={collapseState.partSkus}
onToggle={(expanded) =>
updateCollapseState('partSkus', expanded)
}
collapseKey='partSkus'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
ref={partSkusTableRef}
type='partSku'
masterFilter={{ part: partId }}
visibleColumns={{ part: false }}
/>
)}
</InfoCollapse>
</ActionHandler>
<Modal
open={newPartSkuOpen}
styles={{ content: { paddingBottom: '24px' } }}
footer={null}
width={600}
onCancel={() => setNewPartSkuOpen(false)}
destroyOnClose
>
<NewPartSku
onOk={() => {
setNewPartSkuOpen(false)
partSkusTableRef.current?.reload?.()
}}
reset={newPartSkuOpen}
defaultValues={{
part: partId ? { _id: partId } : undefined
}}
/>
</Modal>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}

View File

@ -24,6 +24,77 @@ const NewProductSku = ({ onOk, reset, defaultValues }) => {
isEditing={true}
required={true}
objectData={objectData}
visibleProperties={{
description: false,
priceMode: false,
cost: false,
costWithTax: false,
costTaxRate: false,
price: false,
priceWithTax: false,
margin: false,
amount: false,
priceTaxRate: false,
vendor: false,
parts: false
}}
/>
)
},
{
title: 'Pricing',
key: 'pricing',
content: (
<ObjectInfo
type='productSku'
column={1}
labelWidth={100}
visibleProperties={{
_id: false,
createdAt: false,
updatedAt: false,
sku: false,
product: false,
name: false,
description: false,
parts: false
}}
bordered={false}
isEditing={true}
objectData={objectData}
/>
)
},
{
title: 'Parts',
key: 'parts',
content: (
<ObjectInfo
type='productSku'
column={1}
labelWidth={100}
visibleProperties={{
_id: false,
createdAt: false,
updatedAt: false,
sku: false,
product: false,
name: false,
description: false,
priceMode: false,
cost: false,
costWithTax: false,
costTaxRate: false,
price: false,
priceWithTax: false,
margin: false,
amount: false,
priceTaxRate: false,
vendor: false
}}
bordered={false}
isEditing={true}
objectData={objectData}
/>
)
},
@ -40,6 +111,7 @@ const NewProductSku = ({ onOk, reset, defaultValues }) => {
}}
bordered={false}
isEditing={true}
objectData={objectData}
/>
)
},
@ -58,6 +130,7 @@ const NewProductSku = ({ onOk, reset, defaultValues }) => {
labelWidth={100}
bordered={false}
isEditing={false}
objectData={objectData}
/>
)
}

View File

@ -19,17 +19,24 @@ import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
import ObjectProperty from '../../common/ObjectProperty.jsx'
import { getModelProperty } from '../../../../database/ObjectModels.js'
import PartIcon from '../../../Icons/PartIcon.jsx'
const ProductSkuInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const productSkuId = new URLSearchParams(location.search).get('productSkuId')
const [collapseState, updateCollapseState] = useCollapseState('ProductSkuInfo', {
const [collapseState, updateCollapseState] = useCollapseState(
'ProductSkuInfo',
{
info: true,
parts: true,
notes: true,
auditLogs: true
})
}
)
const [objectFormState, setEditFormState] = useState({
isEditing: false,
editLoading: false,
@ -82,6 +89,7 @@ const ProductSkuInfo = () => {
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Product SKU Information' },
{ key: 'parts', label: 'SKU Parts' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
@ -144,16 +152,36 @@ const ProductSkuInfo = () => {
}}
>
{({ loading, isEditing, objectData }) => (
<>
<ObjectInfo
loading={loading}
isEditing={isEditing}
type='productSku'
objectData={objectData}
visibleProperties={{
parts: false
}}
/>
</>
)}
</ObjectForm>
</InfoCollapse>
</ActionHandler>
<InfoCollapse
title='SKU Parts'
icon={<PartIcon />}
active={collapseState.parts}
onToggle={(expanded) => updateCollapseState('parts', expanded)}
collapseKey='parts'
>
<ObjectProperty
{...getModelProperty('productSku', 'parts')}
isEditing={objectFormState.isEditing}
objectData={objectFormState.objectData}
loading={objectFormState.loading}
size='medium'
/>
</InfoCollapse>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}

View File

@ -20,9 +20,6 @@ import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
import ObjectProperty from '../../common/ObjectProperty.jsx'
import { getModelProperty } from '../../../../database/ObjectModels.js'
import PartIcon from '../../../Icons/PartIcon.jsx'
import ProductSkuIcon from '../../../Icons/ProductSkuIcon.jsx'
import NewProductSku from '../ProductSkus/NewProductSku'
@ -33,7 +30,6 @@ const ProductInfo = () => {
const productId = new URLSearchParams(location.search).get('productId')
const [collapseState, updateCollapseState] = useCollapseState('ProductInfo', {
info: true,
parts: true,
productSkus: true,
notes: true,
auditLogs: true
@ -92,7 +88,6 @@ const ProductInfo = () => {
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Product Information' },
{ key: 'parts', label: 'Product Parts' },
{ key: 'productSkus', label: 'Product SKUs' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
@ -164,26 +159,6 @@ const ProductInfo = () => {
isEditing={isEditing}
type='product'
objectData={objectData}
visibleProperties={{
parts: false
}}
/>
</InfoCollapse>
<InfoCollapse
title='Product Parts'
icon={<PartIcon />}
active={collapseState.parts}
onToggle={(expanded) =>
updateCollapseState('parts', expanded)
}
collapseKey='parts'
>
<ObjectProperty
{...getModelProperty('product', 'parts')}
isEditing={isEditing}
objectData={objectData}
loading={loading}
size='medium'
/>
</InfoCollapse>
<InfoCollapse

View File

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

View File

@ -7,6 +7,7 @@ import { Job } from './models/Job'
import { Product } from './models/Product'
import { ProductSku } from './models/ProductSku'
import { Part } from './models/Part.js'
import { PartSku } from './models/PartSku.js'
import { Vendor } from './models/Vendor'
import { Courier } from './models/Courier'
import { CourierService } from './models/CourierService'
@ -48,6 +49,7 @@ export const objectModels = [
Product,
ProductSku,
Part,
PartSku,
Vendor,
Courier,
CourierService,
@ -90,6 +92,7 @@ export {
Product,
ProductSku,
Part,
PartSku,
Vendor,
Courier,
CourierService,

View File

@ -4,6 +4,8 @@ import PartIcon from '../../components/Icons/PartIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
import PlusIcon from '../../components/Icons/PlusIcon'
export const Part = {
name: 'part',
label: 'Part',
@ -55,11 +57,23 @@ export const Part = {
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{ type: 'divider' },
{
name: 'newPartSku',
label: 'New Part SKU',
type: 'button',
icon: PlusIcon,
url: (_id) =>
`/dashboard/management/parts/info?partId=${_id}&action=newPartSku`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
}
}
],
columns: ['name', '_reference', 'product', 'globalPricing', 'createdAt'],
filters: ['name', '_id', 'product', 'globalPricing'],
sorters: ['name', 'email', 'role', 'createdAt', '_id'],
columns: ['name', '_reference', 'createdAt'],
filters: ['name', '_id'],
sorters: ['name', 'createdAt', '_id'],
properties: [
{
name: '_id',
@ -90,148 +104,11 @@ export const Part = {
readOnly: true
},
{
name: 'vendor',
label: 'Vendor',
required: true,
type: 'object',
objectType: 'vendor',
showHyperlink: true,
value: (objectData) => {
if (!objectData?.vendor && objectData?.product?.vendor) {
return objectData?.product?.vendor
} else {
return objectData?.vendor
}
}
name: 'fileName',
label: 'File Name',
required: false,
type: 'text'
},
{
name: 'cost',
label: 'Cost',
columnWidth: 150,
required: true,
type: 'number',
prefix: '£',
min: 0,
step: 0.01
},
{
name: 'costWithTax',
label: 'Cost w/ Tax',
columnWidth: 150,
required: true,
readOnly: true,
type: 'number',
prefix: '£',
min: 0,
step: 0.01,
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
)
} else {
return objectData?.cost || undefined
}
}
},
{
name: 'costTaxRate',
label: 'Cost Tax Rate',
required: true,
type: 'object',
objectType: 'taxRate',
showHyperlink: true
},
{
name: 'price',
label: 'Price',
required: true,
type: 'number',
prefix: '£',
min: 0,
step: 0.1,
readOnly: (objectData) => {
return objectData?.priceMode == 'margin'
},
value: (objectData) => {
if (
objectData?.priceMode == 'margin' &&
objectData?.margin !== undefined &&
objectData?.margin !== null
) {
return (
(objectData?.cost * (1 + objectData?.margin / 100)).toFixed(2) ||
undefined
)
} else {
return objectData?.price || undefined
}
}
},
{
name: 'priceWithTax',
label: 'Price w/ Tax',
columnWidth: 150,
required: true,
readOnly: true,
type: 'number',
prefix: '£',
min: 0,
step: 0.01,
value: (objectData) => {
if (objectData?.priceTaxRate?.rateType == 'percentage') {
return (
(
objectData?.price *
(1 + objectData?.priceTaxRate?.rate / 100)
).toFixed(2) || undefined
)
} else if (objectData?.priceTaxRate?.rateType == 'amount') {
return (
(objectData?.price + objectData?.priceTaxRate?.rate).toFixed(2) ||
undefined
)
} else {
return objectData?.price
}
}
},
{
name: 'priceMode',
label: 'Price Mode',
required: true,
type: 'priceMode'
},
{
name: 'margin',
label: 'Margin',
required: true,
type: 'number',
disabled: (objectData) => {
return objectData.priceMode == 'amount'
},
suffix: '%',
min: 0,
max: 100,
step: 0.01
},
{
name: 'priceTaxRate',
label: 'Price Tax Rate',
required: true,
type: 'object',
objectType: 'taxRate',
showHyperlink: true
},
{
name: 'file',
label: 'File',

View File

@ -0,0 +1,267 @@
import PartSkuIcon from '../../components/Icons/PartSkuIcon'
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 PartSku = {
name: 'partSku',
label: 'Part SKU',
prefix: 'PSU',
icon: PartSkuIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/management/partskus/info?partSkuId=${_id}`
},
{
name: 'reload',
label: 'Reload',
icon: ReloadIcon,
url: (_id) =>
`/dashboard/management/partskus/info?partSkuId=${_id}&action=reload`
},
{
name: 'edit',
label: 'Edit',
row: true,
icon: EditIcon,
url: (_id) =>
`/dashboard/management/partskus/info?partSkuId=${_id}&action=edit`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
}
},
{
name: 'finishEdit',
label: 'Save Edits',
icon: CheckIcon,
url: (_id) =>
`/dashboard/management/partskus/info?partSkuId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'cancelEdit',
label: 'Cancel Edits',
icon: XMarkIcon,
url: (_id) =>
`/dashboard/management/partskus/info?partSkuId=${_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/partskus/info?partSkuId=${_id}&action=delete`
}
],
url: (id) => `/dashboard/management/partskus/info?partSkuId=${id}`,
columns: ['_reference', 'sku', 'part', 'name', 'cost', 'price', 'createdAt', 'updatedAt'],
filters: ['_id', 'sku', 'part', 'part._id', 'name', 'cost', 'price'],
sorters: ['sku', 'part', 'name', 'cost', 'price', 'createdAt', 'updatedAt'],
properties: [
{
name: '_id',
label: 'ID',
type: 'id',
objectType: 'partSku',
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: 'part',
label: 'Part',
type: 'object',
objectType: 'part',
required: true,
showHyperlink: true
},
{
name: 'sku',
label: 'SKU',
required: true,
type: 'text'
},
{
name: 'description',
label: 'Description',
required: false,
type: 'text'
},
{
name: 'priceMode',
label: 'Price Mode',
required: false,
type: 'priceMode'
},
{
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) => {
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
)
} else {
return objectData?.cost || undefined
}
}
},
{
name: 'costTaxRate',
label: 'Cost Tax Rate',
required: false,
type: 'object',
objectType: 'taxRate',
showHyperlink: true
},
{
name: 'price',
label: 'Price',
required: false,
type: 'number',
prefix: '£',
min: 0,
step: 0.1,
readOnly: (objectData) => {
return objectData?.priceMode == 'margin'
},
value: (objectData) => {
if (
objectData?.priceMode == 'margin' &&
objectData?.margin !== undefined &&
objectData?.margin !== null
) {
return (
(objectData?.cost * (1 + objectData?.margin / 100)).toFixed(2) ||
undefined
)
} else {
return objectData?.price || undefined
}
}
},
{
name: 'priceWithTax',
label: 'Price w/ Tax',
required: false,
readOnly: true,
type: 'number',
prefix: '£',
min: 0,
step: 0.01,
value: (objectData) => {
if (objectData?.priceTaxRate?.rateType == 'percentage') {
return (
(
objectData?.price *
(1 + objectData?.priceTaxRate?.rate / 100)
).toFixed(2) || undefined
)
} else if (objectData?.priceTaxRate?.rateType == 'amount') {
return (
(objectData?.price + objectData?.priceTaxRate?.rate).toFixed(2) ||
undefined
)
} else {
return objectData?.price
}
}
},
{
name: 'margin',
label: 'Margin',
required: false,
type: 'number',
disabled: (objectData) => {
return objectData?.priceMode == 'amount'
},
suffix: '%',
min: 0,
max: 100,
step: 0.01
},
{
name: 'amount',
label: 'Amount',
disabled: (objectData) => {
return objectData?.priceMode == 'margin'
},
type: 'number',
required: false,
prefix: '£',
min: 0,
step: 0.1
},
{
name: 'priceTaxRate',
label: 'Price Tax Rate',
required: false,
type: 'object',
objectType: 'taxRate',
showHyperlink: true
},
{
name: 'vendor',
label: 'Vendor',
required: false,
type: 'object',
objectType: 'vendor',
showHyperlink: true
}
]
}

View File

@ -17,14 +17,14 @@ export const PartStock = {
}
],
url: (id) => `/dashboard/inventory/partstocks/info?partStockId=${id}`,
filters: ['_id', 'part', 'startingQuantity', 'currentQuantity'],
sorters: ['part', 'startingQuantity', 'currentQuantity'],
filters: ['_id', 'partSku', 'startingQuantity', 'currentQuantity'],
sorters: ['partSku', 'startingQuantity', 'currentQuantity'],
columns: [
'_reference',
'state',
'startingQuantity',
'currentQuantity',
'part',
'partSku',
'createdAt',
'updatedAt'
],
@ -66,10 +66,10 @@ export const PartStock = {
masterFilter: ['subJob']
},
{
name: 'part',
label: 'Part',
name: 'partSku',
label: 'Part SKU',
type: 'object',
objectType: 'part',
objectType: 'partSku',
required: true,
showHyperlink: true
},

View File

@ -78,12 +78,11 @@ export const Product = {
'name',
'tags',
'vendor',
'price',
'createdAt',
'updatedAt'
],
filters: ['_id', 'name', 'type', 'color', 'cost', 'vendor'],
sorters: ['name', 'createdAt', 'type', 'vendor', 'cost', 'updatedAt'],
filters: ['_id', 'name', 'type', 'color', 'vendor'],
sorters: ['name', 'createdAt', 'type', 'vendor', 'updatedAt'],
properties: [
{
name: '_id',
@ -130,73 +129,6 @@ export const Product = {
label: 'Tags',
required: false,
type: 'tags'
},
{
name: 'priceMode',
label: 'Price Mode',
required: true,
type: 'priceMode'
},
{
name: 'margin',
label: 'Margin',
required: true,
type: 'number',
disabled: (objectData) => {
return objectData.priceMode == 'amount'
},
suffix: '%',
min: 0,
max: 100,
step: 0.01
},
{
name: 'amount',
label: 'Amount',
disabled: (objectData) => {
return objectData.priceMode == 'margin'
},
type: 'number',
required: true,
prefix: '£',
min: 0,
step: 0.1
},
{
name: 'parts',
label: 'Parts',
type: 'objectChildren',
objectType: 'part',
properties: [
{
name: 'part',
label: 'Part',
type: 'object',
objectType: 'part',
required: true,
showHyperlink: true
},
{
name: 'quantity',
label: 'Quantity',
type: 'number',
required: true
}
],
rollups: [
{
name: 'totalQuantity',
label: 'Total',
type: 'number',
property: 'quantity',
value: (objectData) => {
return objectData?.parts?.reduce(
(acc, part) => acc + part.quantity,
0
)
}
}
]
}
]
}

View File

@ -69,9 +69,9 @@ export const ProductSku = {
}
],
url: (id) => `/dashboard/management/productskus/info?productSkuId=${id}`,
columns: ['_reference', 'sku', 'product', 'name', 'createdAt', 'updatedAt'],
filters: ['_id', 'sku', 'product', 'product._id', 'name'],
sorters: ['sku', 'product', 'name', 'createdAt', 'updatedAt'],
columns: ['_reference', 'sku', 'product', 'name', 'cost', 'price', 'createdAt', 'updatedAt'],
filters: ['_id', 'sku', 'product', 'product._id', 'name', 'cost', 'price'],
sorters: ['sku', 'product', 'name', 'cost', 'price', 'createdAt', 'updatedAt'],
properties: [
{
name: '_id',
@ -118,6 +118,186 @@ export const ProductSku = {
label: 'Description',
required: false,
type: 'text'
},
{
name: 'priceMode',
label: 'Price Mode',
required: false,
type: 'priceMode'
},
{
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) => {
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
)
} else {
return objectData?.cost || undefined
}
}
},
{
name: 'costTaxRate',
label: 'Cost Tax Rate',
required: false,
type: 'object',
objectType: 'taxRate',
showHyperlink: true
},
{
name: 'price',
label: 'Price',
required: false,
type: 'number',
prefix: '£',
min: 0,
step: 0.1,
readOnly: (objectData) => {
return objectData?.priceMode == 'margin'
},
value: (objectData) => {
if (
objectData?.priceMode == 'margin' &&
objectData?.margin !== undefined &&
objectData?.margin !== null
) {
return (
(objectData?.cost * (1 + objectData?.margin / 100)).toFixed(2) ||
undefined
)
} else {
return objectData?.price || undefined
}
}
},
{
name: 'priceWithTax',
label: 'Price w/ Tax',
required: false,
readOnly: true,
type: 'number',
prefix: '£',
min: 0,
step: 0.01,
value: (objectData) => {
if (objectData?.priceTaxRate?.rateType == 'percentage') {
return (
(
objectData?.price *
(1 + objectData?.priceTaxRate?.rate / 100)
).toFixed(2) || undefined
)
} else if (objectData?.priceTaxRate?.rateType == 'amount') {
return (
(objectData?.price + objectData?.priceTaxRate?.rate).toFixed(2) ||
undefined
)
} else {
return objectData?.price
}
}
},
{
name: 'margin',
label: 'Margin',
required: false,
type: 'number',
disabled: (objectData) => {
return objectData?.priceMode == 'amount'
},
suffix: '%',
min: 0,
max: 100,
step: 0.01
},
{
name: 'amount',
label: 'Amount',
disabled: (objectData) => {
return objectData?.priceMode == 'margin'
},
type: 'number',
required: false,
prefix: '£',
min: 0,
step: 0.1
},
{
name: 'priceTaxRate',
label: 'Price Tax Rate',
required: false,
type: 'object',
objectType: 'taxRate',
showHyperlink: true
},
{
name: 'vendor',
label: 'Vendor',
required: false,
type: 'object',
objectType: 'vendor',
showHyperlink: true
},
{
name: 'parts',
label: 'Parts',
type: 'objectChildren',
objectType: 'partSku',
properties: [
{
name: 'partSku',
label: 'Part SKU',
type: 'object',
objectType: 'partSku',
required: true,
showHyperlink: true
},
{
name: 'quantity',
label: 'Quantity',
type: 'number',
required: true
}
],
rollups: [
{
name: 'totalQuantity',
label: 'Total',
type: 'number',
property: 'quantity',
value: (objectData) => {
return objectData?.parts?.reduce(
(acc, part) => acc + part.quantity,
0
)
}
}
]
}
]
}

View File

@ -85,13 +85,13 @@ export const ProductStock = {
}
],
url: (id) => `/dashboard/inventory/productstocks/info?productStockId=${id}`,
filters: ['_id', 'product', 'currentQuantity'],
sorters: ['product', 'currentQuantity'],
filters: ['_id', 'productSku', 'currentQuantity'],
sorters: ['productSku', 'currentQuantity'],
columns: [
'_reference',
'state',
'currentQuantity',
'product',
'productSku',
'createdAt',
'updatedAt'
],
@ -130,10 +130,10 @@ export const ProductStock = {
readOnly: true
},
{
name: 'product',
label: 'Product',
name: 'productSku',
label: 'Product SKU',
type: 'object',
objectType: 'product',
objectType: 'productSku',
required: true,
showHyperlink: true
},
@ -151,10 +151,10 @@ export const ProductStock = {
canAddRemove: false,
properties: [
{
name: 'part',
label: 'Part',
name: 'partSku',
label: 'Part SKU',
type: 'object',
objectType: 'part',
objectType: 'partSku',
readOnly: true,
required: true,
showHyperlink: true
@ -167,9 +167,9 @@ export const ProductStock = {
required: true,
showHyperlink: true,
masterFilter: (objectData) => {
const partId = objectData?.part?._id
if (partId == null) return {}
return { 'part._id': partId }
const partSkuId = objectData?.partSku?._id
if (partSkuId == null) return {}
return { 'partSku._id': partSkuId }
}
},
{

View File

@ -5,6 +5,8 @@ const Filaments = lazy(() => import('../components/Dashboard/Management/Filament
const FilamentInfo = lazy(() => import('../components/Dashboard/Management/Filaments/FilamentInfo.jsx'))
const Parts = lazy(() => import('../components/Dashboard/Management/Parts.jsx'))
const PartInfo = lazy(() => import('../components/Dashboard/Management/Parts/PartInfo.jsx'))
const PartSkus = lazy(() => import('../components/Dashboard/Management/PartSkus.jsx'))
const PartSkuInfo = lazy(() => import('../components/Dashboard/Management/PartSkus/PartSkuInfo.jsx'))
const Products = lazy(() => import('../components/Dashboard/Management/Products.jsx'))
const ProductInfo = lazy(() => import('../components/Dashboard/Management/Products/ProductInfo.jsx'))
const ProductSkus = lazy(() => import('../components/Dashboard/Management/ProductSkus.jsx'))
@ -56,6 +58,12 @@ const ManagementRoutes = [
path='management/parts/info'
element={<PartInfo />}
/>,
<Route key='partskus' path='management/partskus' element={<PartSkus />} />,
<Route
key='partskus-info'
path='management/partskus/info'
element={<PartSkuInfo />}
/>,
<Route key='products' path='management/products' element={<Products />} />,
<Route
key='products-info'