diff --git a/assets/icons/partskuicon.svg b/assets/icons/partskuicon.svg
new file mode 100644
index 0000000..0f83ffd
--- /dev/null
+++ b/assets/icons/partskuicon.svg
@@ -0,0 +1,12 @@
+
+
+
diff --git a/assets/icons/productskuicon.svg b/assets/icons/productskuicon.svg
index 6a0ff42..42a3d8a 100644
--- a/assets/icons/productskuicon.svg
+++ b/assets/icons/productskuicon.svg
@@ -2,11 +2,11 @@
diff --git a/src/components/Dashboard/Management/ManagementSidebar.jsx b/src/components/Dashboard/Management/ManagementSidebar.jsx
index e315e40..e0e0b1e 100644
--- a/src/components/Dashboard/Management/ManagementSidebar.jsx
+++ b/src/components/Dashboard/Management/ManagementSidebar.jsx
@@ -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: ,
+ label: 'Part SKUs',
+ path: '/dashboard/management/partskus'
+ },
{
key: 'products',
icon: ,
@@ -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',
diff --git a/src/components/Dashboard/Management/PartSkus.jsx b/src/components/Dashboard/Management/PartSkus.jsx
new file mode 100644
index 0000000..4656ddf
--- /dev/null
+++ b/src/components/Dashboard/Management/PartSkus.jsx
@@ -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:
+ },
+ { type: 'divider' },
+ {
+ label: 'Reload List',
+ key: 'reloadList',
+ icon:
+ }
+ ],
+ onClick: ({ key }) => {
+ if (key === 'reloadList') {
+ tableRef.current?.reload()
+ } else if (key === 'newPartSku') {
+ setNewPartSkuOpen(true)
+ }
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ : }
+ onClick={() =>
+ setViewMode(viewMode === 'cards' ? 'list' : 'cards')
+ }
+ />
+
+
+
+
+
+ {
+ setNewPartSkuOpen(false)
+ }}
+ destroyOnHidden={true}
+ >
+ {
+ setNewPartSkuOpen(false)
+ tableRef.current?.reload()
+ }}
+ reset={newPartSkuOpen}
+ />
+
+ >
+ )
+}
+
+export default PartSkus
diff --git a/src/components/Dashboard/Management/PartSkus/NewPartSku.jsx b/src/components/Dashboard/Management/PartSkus/NewPartSku.jsx
new file mode 100644
index 0000000..c4a08ee
--- /dev/null
+++ b/src/components/Dashboard/Management/PartSkus/NewPartSku.jsx
@@ -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 (
+
+ {({ handleSubmit, submitLoading, objectData, formValid }) => {
+ const steps = [
+ {
+ title: 'Required',
+ key: 'required',
+ content: (
+
+ )
+ },
+ {
+ title: 'Pricing',
+ key: 'pricing',
+ content: (
+
+ )
+ },
+ {
+ title: 'Optional',
+ key: 'optional',
+ content: (
+
+ )
+ },
+ {
+ title: 'Summary',
+ key: 'summary',
+ content: (
+
+ )
+ }
+ ]
+ return (
+ {
+ const result = await handleSubmit()
+ if (result) {
+ onOk()
+ }
+ }}
+ />
+ )
+ }}
+
+ )
+}
+
+NewPartSku.propTypes = {
+ onOk: PropTypes.func.isRequired,
+ reset: PropTypes.bool,
+ defaultValues: PropTypes.object
+}
+
+export default NewPartSku
diff --git a/src/components/Dashboard/Management/PartSkus/PartSkuInfo.jsx b/src/components/Dashboard/Management/PartSkus/PartSkuInfo.jsx
new file mode 100644
index 0000000..80144ef
--- /dev/null
+++ b/src/components/Dashboard/Management/PartSkus/PartSkuInfo.jsx
@@ -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 (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ actionHandlerRef.current.callAction('finishEdit')
+ }}
+ cancelEditing={() => {
+ actionHandlerRef.current.callAction('cancelEdit')
+ }}
+ startEditing={() => {
+ actionHandlerRef.current.callAction('edit')
+ }}
+ editLoading={objectFormState.editLoading}
+ formValid={objectFormState.formValid}
+ disabled={objectFormState.lock?.locked || objectFormState.loading}
+ loading={objectFormState.editLoading}
+ />
+
+
+
+
+
+ }
+ active={collapseState.info}
+ onToggle={(expanded) => updateCollapseState('info', expanded)}
+ collapseKey='info'
+ >
+ {
+ setEditFormState((prev) => ({ ...prev, ...state }))
+ }}
+ >
+ {({ loading, isEditing, objectData }) => (
+
+ )}
+
+
+
+ }
+ active={collapseState.notes}
+ onToggle={(expanded) => updateCollapseState('notes', expanded)}
+ collapseKey='notes'
+ >
+
+
+
+
+ }
+ active={collapseState.auditLogs}
+ onToggle={(expanded) =>
+ updateCollapseState('auditLogs', expanded)
+ }
+ collapseKey='auditLogs'
+ >
+ {objectFormState.loading ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ >
+ )
+}
+
+export default PartSkuInfo
diff --git a/src/components/Dashboard/Management/Parts/PartInfo.jsx b/src/components/Dashboard/Management/Parts/PartInfo.jsx
index 2b1d3a7..3075403 100644
--- a/src/components/Dashboard/Management/Parts/PartInfo.jsx
+++ b/src/components/Dashboard/Management/Parts/PartInfo.jsx
@@ -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' }
]}
@@ -124,33 +133,72 @@ const PartInfo = () => {
loading={objectFormState.loading}
ref={actionHandlerRef}
>
- }
- active={collapseState.info}
- onToggle={(expanded) => updateCollapseState('info', expanded)}
- collapseKey='info'
+ {
+ setEditFormState((prev) => ({ ...prev, ...state }))
+ }}
>
- {
- setEditFormState((prev) => ({ ...prev, ...state }))
- }}
- >
- {({ loading, isEditing, objectData }) => (
+ {({ loading, isEditing, objectData }) => (
+ }
+ active={collapseState.info}
+ onToggle={(expanded) => updateCollapseState('info', expanded)}
+ collapseKey='info'
+ >
- )}
-
+
+ )}
+
+ }
+ active={collapseState.partSkus}
+ onToggle={(expanded) =>
+ updateCollapseState('partSkus', expanded)
+ }
+ collapseKey='partSkus'
+ >
+ {objectFormState.loading ? (
+
+ ) : (
+
+ )}
+ setNewPartSkuOpen(false)}
+ destroyOnClose
+ >
+ {
+ setNewPartSkuOpen(false)
+ partSkusTableRef.current?.reload?.()
+ }}
+ reset={newPartSkuOpen}
+ defaultValues={{
+ part: partId ? { _id: partId } : undefined
+ }}
+ />
+
}
diff --git a/src/components/Dashboard/Management/ProductSkus/NewProductSku.jsx b/src/components/Dashboard/Management/ProductSkus/NewProductSku.jsx
index 6608694..9064b51 100644
--- a/src/components/Dashboard/Management/ProductSkus/NewProductSku.jsx
+++ b/src/components/Dashboard/Management/ProductSkus/NewProductSku.jsx
@@ -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: (
+
+ )
+ },
+ {
+ title: 'Parts',
+ key: 'parts',
+ content: (
+
)
},
@@ -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}
/>
)
}
diff --git a/src/components/Dashboard/Management/ProductSkus/ProductSkuInfo.jsx b/src/components/Dashboard/Management/ProductSkus/ProductSkuInfo.jsx
index 81333ac..82e1e72 100644
--- a/src/components/Dashboard/Management/ProductSkus/ProductSkuInfo.jsx
+++ b/src/components/Dashboard/Management/ProductSkus/ProductSkuInfo.jsx
@@ -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', {
- info: true,
- notes: true,
- auditLogs: true
- })
+ 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 }) => (
-
+ <>
+
+ >
)}
+ }
+ active={collapseState.parts}
+ onToggle={(expanded) => updateCollapseState('parts', expanded)}
+ collapseKey='parts'
+ >
+
+
}
diff --git a/src/components/Dashboard/Management/Products/ProductInfo.jsx b/src/components/Dashboard/Management/Products/ProductInfo.jsx
index c0b8336..23236a5 100644
--- a/src/components/Dashboard/Management/Products/ProductInfo.jsx
+++ b/src/components/Dashboard/Management/Products/ProductInfo.jsx
@@ -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
- }}
- />
-
- }
- active={collapseState.parts}
- onToggle={(expanded) =>
- updateCollapseState('parts', expanded)
- }
- collapseKey='parts'
- >
-
(
+
+)
+
+export default PartSkuIcon
diff --git a/src/database/ObjectModels.js b/src/database/ObjectModels.js
index d50809a..6adf26f 100644
--- a/src/database/ObjectModels.js
+++ b/src/database/ObjectModels.js
@@ -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,
diff --git a/src/database/models/Part.js b/src/database/models/Part.js
index ed960f8..a04b90f 100644
--- a/src/database/models/Part.js
+++ b/src/database/models/Part.js
@@ -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',
diff --git a/src/database/models/PartSku.js b/src/database/models/PartSku.js
new file mode 100644
index 0000000..b6d7104
--- /dev/null
+++ b/src/database/models/PartSku.js
@@ -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
+ }
+ ]
+}
diff --git a/src/database/models/PartStock.js b/src/database/models/PartStock.js
index 52a0de2..2cf2e6d 100644
--- a/src/database/models/PartStock.js
+++ b/src/database/models/PartStock.js
@@ -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
},
diff --git a/src/database/models/Product.js b/src/database/models/Product.js
index 0b8138c..c3fca79 100644
--- a/src/database/models/Product.js
+++ b/src/database/models/Product.js
@@ -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
- )
- }
- }
- ]
}
]
}
diff --git a/src/database/models/ProductSku.js b/src/database/models/ProductSku.js
index 4e3138e..717084c 100644
--- a/src/database/models/ProductSku.js
+++ b/src/database/models/ProductSku.js
@@ -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
+ )
+ }
+ }
+ ]
}
]
}
diff --git a/src/database/models/ProductStock.js b/src/database/models/ProductStock.js
index bce886c..6eae8a0 100644
--- a/src/database/models/ProductStock.js
+++ b/src/database/models/ProductStock.js
@@ -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 }
}
},
{
diff --git a/src/routes/ManagementRoutes.jsx b/src/routes/ManagementRoutes.jsx
index b92fcde..6e110a3 100644
--- a/src/routes/ManagementRoutes.jsx
+++ b/src/routes/ManagementRoutes.jsx
@@ -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={}
/>,
+ } />,
+ }
+ />,
} />,