diff --git a/assets/icons/productcategoryicon.svg b/assets/icons/productcategoryicon.svg
new file mode 100644
index 0000000..825fee8
--- /dev/null
+++ b/assets/icons/productcategoryicon.svg
@@ -0,0 +1,16 @@
+
+
+
diff --git a/src/components/Dashboard/Inventory/StockTransfers.jsx b/src/components/Dashboard/Inventory/StockTransfers.jsx
index 4d2d9dd..5a6a646 100644
--- a/src/components/Dashboard/Inventory/StockTransfers.jsx
+++ b/src/components/Dashboard/Inventory/StockTransfers.jsx
@@ -90,7 +90,7 @@ const StockTransfers = () => {
open={newOpen}
styles={{ content: { paddingBottom: '24px' } }}
footer={null}
- width={960}
+ width={740}
onCancel={() => {
setNewOpen(false)
}}
diff --git a/src/components/Dashboard/Inventory/StockTransfers/NewStockTransfer.jsx b/src/components/Dashboard/Inventory/StockTransfers/NewStockTransfer.jsx
index cbf8f6b..675da7d 100644
--- a/src/components/Dashboard/Inventory/StockTransfers/NewStockTransfer.jsx
+++ b/src/components/Dashboard/Inventory/StockTransfers/NewStockTransfer.jsx
@@ -1,17 +1,40 @@
import PropTypes from 'prop-types'
+import dayjs from 'dayjs'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
+const defaultTransferName = () =>
+ `Transfer ${dayjs().format('YYYY-MM-DD HH:mm:ss')}`
+
const NewStockTransfer = ({ onOk, reset }) => {
return (
- {({ handleSubmit, submitLoading, objectData }) => {
+ {({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
+ {
+ title: 'Required',
+ key: 'required',
+ content: (
+
+ )
+ },
{
title: 'Summary',
key: 'summary',
@@ -20,6 +43,7 @@ const NewStockTransfer = ({ onOk, reset }) => {
type='stockTransfer'
column={1}
bordered={false}
+ labelWidth={80}
visibleProperties={{
_id: false,
createdAt: false,
@@ -38,7 +62,7 @@ const NewStockTransfer = ({ onOk, reset }) => {
{
const result = await handleSubmit()
diff --git a/src/components/Dashboard/Management/Hosts/HostInfo.jsx b/src/components/Dashboard/Management/Hosts/HostInfo.jsx
index aeb5fab..5d74d32 100644
--- a/src/components/Dashboard/Management/Hosts/HostInfo.jsx
+++ b/src/components/Dashboard/Management/Hosts/HostInfo.jsx
@@ -68,6 +68,10 @@ const HostInfo = () => {
finishEdit: () => {
objectFormRef?.current.handleUpdate()
return true
+ },
+ delete: () => {
+ objectFormRef?.current?.handleDelete?.()
+ return true
}
}
diff --git a/src/components/Dashboard/Management/ManagementSidebar.jsx b/src/components/Dashboard/Management/ManagementSidebar.jsx
index 96f8609..6881be2 100644
--- a/src/components/Dashboard/Management/ManagementSidebar.jsx
+++ b/src/components/Dashboard/Management/ManagementSidebar.jsx
@@ -5,6 +5,7 @@ import FilamentSkuIcon from '../../Icons/FilamentSkuIcon'
import PartIcon from '../../Icons/PartIcon'
import PartSkuIcon from '../../Icons/PartSkuIcon'
import ProductIcon from '../../Icons/ProductIcon'
+import ProductCategoryIcon from '../../Icons/ProductCategoryIcon'
import ProductSkuIcon from '../../Icons/ProductSkuIcon'
import VendorIcon from '../../Icons/VendorIcon'
import MaterialIcon from '../../Icons/MaterialIcon'
@@ -57,6 +58,12 @@ const items = [
label: 'Products',
path: '/dashboard/management/products'
},
+ {
+ key: 'productCategories',
+ icon: ,
+ label: 'Product Categories',
+ path: '/dashboard/management/productcategories'
+ },
{
key: 'productSkus',
icon: ,
@@ -199,6 +206,7 @@ const routeKeyMap = {
'/dashboard/management/users': 'users',
'/dashboard/management/apppasswords': 'appPasswords',
'/dashboard/management/products': 'products',
+ '/dashboard/management/productcategories': 'productCategories',
'/dashboard/management/productskus': 'productSkus',
'/dashboard/management/vendors': 'vendors',
'/dashboard/management/couriers': 'couriers',
diff --git a/src/components/Dashboard/Management/ProductCategories.jsx b/src/components/Dashboard/Management/ProductCategories.jsx
new file mode 100644
index 0000000..4a4c8fd
--- /dev/null
+++ b/src/components/Dashboard/Management/ProductCategories.jsx
@@ -0,0 +1,109 @@
+import { useRef, useState } from 'react'
+import { Button, Flex, Space, Modal, Dropdown } from 'antd'
+
+import NewProductCategory from './ProductCategories/NewProductCategory'
+
+import useColumnVisibility from '../hooks/useColumnVisibility'
+import ColumnViewButton from '../common/ColumnViewButton'
+import ObjectTable from '../common/ObjectTable'
+import PlusIcon from '../../Icons/PlusIcon'
+import ReloadIcon from '../../Icons/ReloadIcon'
+import ObjectTableViewButton from '../common/ObjectTableViewButton'
+import FilterSidebarButton from '../common/FilterSidebarButton'
+import useViewMode from '../hooks/useViewMode'
+import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility'
+import ExportListButton from '../common/ExportListButton'
+
+const ProductCategories = () => {
+ const [newProductCategoryOpen, setNewProductCategoryOpen] = useState(false)
+ const tableRef = useRef()
+
+ const [viewMode, setViewMode] = useViewMode('productCategory')
+
+ const [columnVisibility, setColumnVisibility] =
+ useColumnVisibility('productCategory')
+
+ const [showFilterSidebar, setShowFilterSidebar] =
+ useFilterSidebarVisibility('ProductCategories')
+
+ const actionItems = {
+ items: [
+ {
+ label: 'New Product Category',
+ key: 'newProductCategory',
+ icon:
+ },
+ { type: 'divider' },
+ {
+ label: 'Reload List',
+ key: 'reloadList',
+ icon:
+ }
+ ],
+ onClick: ({ key }) => {
+ if (key === 'reloadList') {
+ tableRef.current?.reload()
+ } else if (key === 'newProductCategory') {
+ setNewProductCategoryOpen(true)
+ }
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ setShowFilterSidebar(!showFilterSidebar)}
+ />
+
+
+
+
+
+
+ {
+ setNewProductCategoryOpen(false)
+ }}
+ >
+ {
+ setNewProductCategoryOpen(false)
+ tableRef.current?.reload()
+ }}
+ reset={newProductCategoryOpen}
+ />
+
+
+ >
+ )
+}
+
+export default ProductCategories
diff --git a/src/components/Dashboard/Management/ProductCategories/NewProductCategory.jsx b/src/components/Dashboard/Management/ProductCategories/NewProductCategory.jsx
new file mode 100644
index 0000000..71c2141
--- /dev/null
+++ b/src/components/Dashboard/Management/ProductCategories/NewProductCategory.jsx
@@ -0,0 +1,68 @@
+import PropTypes from 'prop-types'
+import ObjectInfo from '../../common/ObjectInfo'
+import NewObjectForm from '../../common/NewObjectForm'
+import WizardView from '../../common/WizardView'
+
+const NewProductCategory = ({ onOk }) => {
+ return (
+
+ {({ handleSubmit, submitLoading, objectData, formValid }) => {
+ const steps = [
+ {
+ title: 'Required',
+ key: 'required',
+ content: (
+
+ )
+ },
+ {
+ title: 'Summary',
+ key: 'summary',
+ content: (
+
+ )
+ }
+ ]
+ return (
+ {
+ const result = await handleSubmit()
+ if (result) {
+ onOk()
+ }
+ }}
+ />
+ )
+ }}
+
+ )
+}
+
+NewProductCategory.propTypes = {
+ onOk: PropTypes.func.isRequired,
+ reset: PropTypes.bool
+}
+
+export default NewProductCategory
diff --git a/src/components/Dashboard/Management/ProductCategories/ProductCategoryInfo.jsx b/src/components/Dashboard/Management/ProductCategories/ProductCategoryInfo.jsx
new file mode 100644
index 0000000..98841d0
--- /dev/null
+++ b/src/components/Dashboard/Management/ProductCategories/ProductCategoryInfo.jsx
@@ -0,0 +1,195 @@
+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 InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
+import NoteIcon from '../../../Icons/NoteIcon.jsx'
+import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
+import ObjectForm from '../../common/ObjectForm'
+import EditButtons from '../../common/EditButtons'
+import LockIndicator from '../../common/LockIndicator.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 ProductCategoryInfo = () => {
+ const location = useLocation()
+ const objectFormRef = useRef(null)
+ const actionHandlerRef = useRef(null)
+ const productCategoryId = new URLSearchParams(location.search).get(
+ 'productCategoryId'
+ )
+ const [collapseState, updateCollapseState] = useCollapseState(
+ 'ProductCategoryInfo',
+ {
+ info: true,
+ notes: true,
+ auditLogs: true
+ }
+ )
+ const [objectFormState, setEditFormState] = useState({
+ isEditing: false,
+ editLoading: false,
+ formValid: false,
+ lock: null,
+ loading: false,
+ objectData: {}
+ })
+
+ const actions = {
+ 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 ProductCategoryInfo
diff --git a/src/components/Dashboard/Management/Users/UserInfo.jsx b/src/components/Dashboard/Management/Users/UserInfo.jsx
index 6850247..941de3b 100644
--- a/src/components/Dashboard/Management/Users/UserInfo.jsx
+++ b/src/components/Dashboard/Management/Users/UserInfo.jsx
@@ -22,6 +22,9 @@ import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
import NewAppPassword from '../AppPasswords/NewAppPassword.jsx'
+import ObjectProperty from '../../common/ObjectProperty.jsx'
+import { getModelProperty } from '../../../../database/ObjectModels.js'
+import { useMediaQuery } from 'react-responsive'
const UserInfo = () => {
const location = useLocation()
@@ -44,6 +47,7 @@ const UserInfo = () => {
loading: false,
objectData: {}
})
+ const isMobile = useMediaQuery({ maxWidth: 768 })
const actions = {
newAppPassword: () => {
@@ -147,13 +151,34 @@ const UserInfo = () => {
}}
>
{({ loading, isEditing, objectData }) => (
- }
- isEditing={isEditing}
- type='user'
- objectData={objectData}
- />
+
+
+
+
+
+
+ }
+ isEditing={isEditing}
+ type='user'
+ objectData={objectData}
+ visibleProperties={{
+ profileImage: false
+ }}
+ />
+
)}
diff --git a/src/components/Dashboard/Production/GCodeFiles/NewGCodeFile.jsx b/src/components/Dashboard/Production/GCodeFiles/NewGCodeFile.jsx
index 1776aa9..0b6d71e 100644
--- a/src/components/Dashboard/Production/GCodeFiles/NewGCodeFile.jsx
+++ b/src/components/Dashboard/Production/GCodeFiles/NewGCodeFile.jsx
@@ -18,7 +18,7 @@ const NewGCodeFile = ({ onOk, defaultValues }) => {
const steps = [
{
title: 'Upload',
- key: 'uplaod',
+ key: 'upload',
content: (
{
isEditing={true}
required={true}
objectData={objectData}
- visibleProperties={{ name: false, filament: false }}
+ visibleProperties={{ file: true }}
showLabels={false}
/>
)
@@ -57,6 +57,8 @@ const NewGCodeFile = ({ onOk, defaultValues }) => {
bordered={false}
visibleProperties={{
_id: false,
+ _reference: false,
+ parts: false,
createdAt: false,
updatedAt: false,
startedAt: false
diff --git a/src/components/Dashboard/common/DashboardBreadcrumb.jsx b/src/components/Dashboard/common/DashboardBreadcrumb.jsx
index dd293b9..73e96b5 100644
--- a/src/components/Dashboard/common/DashboardBreadcrumb.jsx
+++ b/src/components/Dashboard/common/DashboardBreadcrumb.jsx
@@ -12,6 +12,7 @@ const breadcrumbNameMap = {
developer: 'Developer',
finance: 'Finance',
sales: 'Sales',
+ productcategories: 'Product Categories',
overview: 'Overview',
info: 'Info',
design: 'Design',
diff --git a/src/components/Dashboard/common/FileList.jsx b/src/components/Dashboard/common/FileList.jsx
index 5bcf97a..a0b2bb8 100644
--- a/src/components/Dashboard/common/FileList.jsx
+++ b/src/components/Dashboard/common/FileList.jsx
@@ -23,6 +23,7 @@ const FileList = ({
showInfo = true,
showDownload = true,
defaultPreviewOpen = false,
+ minimal = false,
card = true
}) => {
const { fetchFileContent, flushFile } = useContext(ApiServerContext)
@@ -78,7 +79,7 @@ const FileList = ({
{file.extension}
- {showDownload && (
+ {showDownload && !minimal && (
}
size='small'
@@ -86,7 +87,7 @@ const FileList = ({
onClick={() => handleDownload(file)}
/>
)}
- {showPreview && (
+ {showPreview && !minimal && (
: }
size='small'
@@ -100,7 +101,7 @@ const FileList = ({
}}
/>
)}
- {showInfo && (
+ {showInfo && !minimal && (
}
size='small'
@@ -110,7 +111,7 @@ const FileList = ({
}}
/>
)}
- {editing && (
+ {editing && !minimal && (
}
size='small'
@@ -120,7 +121,7 @@ const FileList = ({
)}
- {previewOpen ? (
+ {previewOpen && !minimal ? (
<>
@@ -174,7 +175,8 @@ FileList.propTypes = {
showInfo: PropTypes.bool,
showDownload: PropTypes.bool,
defaultPreviewOpen: PropTypes.bool,
- card: PropTypes.bool
+ card: PropTypes.bool,
+ minimal: PropTypes.bool
}
export default FileList
diff --git a/src/components/Dashboard/common/FileUpload.jsx b/src/components/Dashboard/common/FileUpload.jsx
index fcd3c0d..ebbb048 100644
--- a/src/components/Dashboard/common/FileUpload.jsx
+++ b/src/components/Dashboard/common/FileUpload.jsx
@@ -2,7 +2,14 @@ import { Upload, Button, Flex, Typography, Space, Progress, Card } from 'antd'
import PropTypes from 'prop-types'
import { ApiServerContext } from '../context/ApiServerContext'
import UploadIcon from '../../Icons/UploadIcon'
-import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
+import {
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useRef,
+ useState
+} from 'react'
import ObjectSelect from './ObjectSelect'
import FileList from './FileList'
import PlusIcon from '../../Icons/PlusIcon'
@@ -51,7 +58,9 @@ const FileUpload = ({
// Update currentFiles when value prop changes
useEffect(() => {
setCurrentFiles((prev) => {
- if (getFileIdentity(prev, multiple) === getFileIdentity(value, multiple)) {
+ if (
+ getFileIdentity(prev, multiple) === getFileIdentity(value, multiple)
+ ) {
return prev
}
@@ -128,7 +137,7 @@ const FileUpload = ({
return (
{hasNoItems && uploading == false ? (
-
+
{
const [submitLoading, setSubmitLoading] = useState(false)
const [formValid, setFormValid] = useState(false)
const [form] = Form.useForm()
+ const validationRunRef = useRef(0)
const formUpdateValues = Form.useWatch([], form)
const { showSuccess, showError: showMessageError } = useMessageContext()
const { createObject, showError } = useContext(ApiServerContext)
+ const validateForm = useCallback(() => {
+ const validationRun = ++validationRunRef.current
+ let cancelled = false
+ const timeoutId = setTimeout(() => {
+ form
+ .validateFields({ validateOnly: true })
+ .then(() => {
+ if (!cancelled && validationRun === validationRunRef.current) {
+ setFormValid(true)
+ }
+ })
+ .catch(() => {
+ if (!cancelled && validationRun === validationRunRef.current) {
+ setFormValid(false)
+ }
+ })
+ }, 0)
+
+ return () => {
+ cancelled = true
+ clearTimeout(timeoutId)
+ }
+ }, [form])
+
// Get the model definition for this object type
const model = getModelByName(type)
@@ -141,21 +166,15 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
const computedValuesObject = buildObjectFromEntries(computedEntries)
const initialFormData = merge({}, defaultValues, computedValuesObject)
form.setFieldsValue(initialFormData)
- form
- .validateFields({ validateOnly: true })
- .then(() => setFormValid(true))
- .catch(() => setFormValid(false))
setObjectData((prev) => merge({}, prev, initialFormData))
+ return validateForm()
}
- }, [form, defaultValues, calculateComputedValues, model])
+ }, [form, defaultValues, calculateComputedValues, model, validateForm])
// Validate form on change
useEffect(() => {
- form
- .validateFields({ validateOnly: true })
- .then(() => setFormValid(true))
- .catch(() => setFormValid(false))
- }, [form, formUpdateValues])
+ return validateForm()
+ }, [validateForm, formUpdateValues])
const handleSubmit = async () => {
try {
@@ -217,7 +236,7 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
}}
>
{children({
- submitLoading: submitLoading,
+ submitLoading,
handleSubmit,
form,
formValid,
diff --git a/src/components/Dashboard/common/NoteItem.jsx b/src/components/Dashboard/common/NoteItem.jsx
index 771d62e..f8907d3 100644
--- a/src/components/Dashboard/common/NoteItem.jsx
+++ b/src/components/Dashboard/common/NoteItem.jsx
@@ -103,11 +103,10 @@ const NoteItem = ({
if (isExpanded == true) {
subscribeToObjectTypeUpdatesRef.current = subscribeToObjectTypeUpdates(
'note',
- (noteData) => {
- if (noteData.parent._id == note._id) {
- if (isExpanded == true) {
- handleNoteExpand()
- }
+ { 'parent._id': note._id },
+ () => {
+ if (isExpanded == true) {
+ handleNoteExpand()
}
}
)
diff --git a/src/components/Dashboard/common/NotesPanel.jsx b/src/components/Dashboard/common/NotesPanel.jsx
index 1e53a44..66c7689 100644
--- a/src/components/Dashboard/common/NotesPanel.jsx
+++ b/src/components/Dashboard/common/NotesPanel.jsx
@@ -95,11 +95,8 @@ const NotesPanel = ({ _id, type }) => {
if (connected == true && subscribeToObjectTypeUpdatesRef.current == null) {
subscribeToObjectTypeUpdatesRef.current = subscribeToObjectTypeUpdates(
'note',
- (noteData) => {
- if (noteData.parent._id == _id) {
- handleReloadData()
- }
- }
+ { 'parent._id': _id },
+ () => handleReloadData()
)
}
return () => {
diff --git a/src/components/Dashboard/common/ObjectProperty.jsx b/src/components/Dashboard/common/ObjectProperty.jsx
index 72635ce..1984602 100644
--- a/src/components/Dashboard/common/ObjectProperty.jsx
+++ b/src/components/Dashboard/common/ObjectProperty.jsx
@@ -99,6 +99,14 @@ const ObjectProperty = ({
value = value(objectData)
}
+ if (max && typeof max == 'function' && objectData) {
+ max = max(objectData)
+ }
+
+ if (min && typeof min == 'function' && objectData) {
+ min = min(objectData)
+ }
+
if (objectType && typeof objectType == 'function' && objectData) {
objectType = objectType(objectData)
}
diff --git a/src/components/Dashboard/common/ObjectTable.jsx b/src/components/Dashboard/common/ObjectTable.jsx
index 9954853..26cf3fe 100644
--- a/src/components/Dashboard/common/ObjectTable.jsx
+++ b/src/components/Dashboard/common/ObjectTable.jsx
@@ -446,6 +446,14 @@ const ObjectTable = forwardRef(
[reload]
)
+ const subscriptionFilter = useMemo(() => {
+ const active = {}
+ Object.entries(sidebarFilter).forEach(([k, v]) => {
+ if (v !== '' && v !== undefined) active[k] = v
+ })
+ return { ...active, ...masterFilter }
+ }, [sidebarFilter, masterFilter])
+
// Subscribe to real-time updates for all items
useEffect(() => {
if (pages.length > 0 && connected == true) {
@@ -517,16 +525,28 @@ const ObjectTable = forwardRef(
}, [connected])
useEffect(() => {
- if (
- connected == true &&
- subscribeToObjectTypeUpdatesRef.current == null
- ) {
- subscribeToObjectTypeUpdatesRef.current = subscribeToObjectTypeUpdates(
+ if (connected == true) {
+ const unsubscribe = subscribeToObjectTypeUpdates(
type,
+ subscriptionFilter,
newEventHandler
)
+ subscribeToObjectTypeUpdatesRef.current = unsubscribe
+
+ return () => {
+ if (unsubscribe) unsubscribe()
+ if (subscribeToObjectTypeUpdatesRef.current === unsubscribe) {
+ subscribeToObjectTypeUpdatesRef.current = null
+ }
+ }
}
- }, [type, subscribeToObjectTypeUpdates, connected, newEventHandler])
+ }, [
+ type,
+ subscriptionFilter,
+ subscribeToObjectTypeUpdates,
+ connected,
+ newEventHandler
+ ])
const updateData = useCallback(
(id, updatedData) => {
@@ -663,11 +683,13 @@ const ObjectTable = forwardRef(
}
})
+ console.log('filters--', filters)
+
setSidebarFilter(next)
setPages([])
setLoading(true)
loadPage(initialPage, getActiveFilter(next), {
- field: sorter.field,
+ field: sorter.columnKey,
order: sorter.order
})
}
@@ -858,29 +880,42 @@ const ObjectTable = forwardRef(
style={{ overflowY: 'auto', maxHeight: adjustedScrollHeight }}
ref={cardsContainerRef}
>
- {tableData.map((record) => (
-
-
-
-
-
-
-
-
-
- ))}
+ {tableData.map((record) => {
+ if (record?._id == undefined) {
+ return null
+ }
+ return (
+
+
+
+
+
+
+
+
+
+ )
+ })}
)
}
diff --git a/src/components/Dashboard/common/StateTag.jsx b/src/components/Dashboard/common/StateTag.jsx
index 238ddd0..89220a7 100644
--- a/src/components/Dashboard/common/StateTag.jsx
+++ b/src/components/Dashboard/common/StateTag.jsx
@@ -136,6 +136,10 @@ const StateTag = ({ state, showBadge = true, style = {} }) => {
status = 'cyan'
text = 'Ordered'
break
+ case 'posted':
+ status = 'magenta'
+ text = 'Posted'
+ break
case 'received':
status = 'success'
text = 'Received'
diff --git a/src/components/Dashboard/context/ApiServerContext.jsx b/src/components/Dashboard/context/ApiServerContext.jsx
index 4e07c82..0a31c07 100644
--- a/src/components/Dashboard/context/ApiServerContext.jsx
+++ b/src/components/Dashboard/context/ApiServerContext.jsx
@@ -17,6 +17,7 @@ import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import config from '../../../config'
import loglevel from 'loglevel'
+import { getModelByName } from '../../../database/ObjectModels'
const logger = loglevel.getLogger('ApiServerContext')
logger.setLevel(config.logLevel)
@@ -25,6 +26,35 @@ const SPOTLIGHT_CACHE_TTL_MS = 10_000
const spotlightCache = new Map()
const runningSpotlightFetches = new Map()
+const stableStringify = (value) => {
+ if (Array.isArray(value)) {
+ return `[${value.map(stableStringify).join(',')}]`
+ }
+
+ if (value && typeof value === 'object') {
+ return `{${Object.keys(value)
+ .sort()
+ .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
+ .join(',')}}`
+ }
+
+ return JSON.stringify(value)
+}
+
+const getObjectTypeSubscriptionKey = (objectType, filter = {}) =>
+ `${objectType}:${stableStringify(filter || {})}`
+
+const getObjectTypeSubscriptionArgs = (filterOrCallback, callback) => {
+ if (typeof filterOrCallback === 'function') {
+ return { filter: {}, callback: filterOrCallback }
+ }
+
+ return { filter: filterOrCallback || {}, callback }
+}
+
+const getObjectEndpoint = (type) =>
+ getModelByName(type)?.endpoint || `${type.toLowerCase()}s`
+
const ApiServerContext = createContext()
const ApiServerProvider = ({ children }) => {
@@ -316,12 +346,16 @@ const ApiServerProvider = ({ children }) => {
const handleObjectNew = async (data) => {
logger.debug('Notifying object new:', data)
const objectType = data.objectType || 'unknown'
+ const callbacksRefKey = getObjectTypeSubscriptionKey(
+ objectType,
+ data.filter || {}
+ )
- if (objectType && subscribedCallbacksRef.current.has(objectType)) {
- const callbacks = subscribedCallbacksRef.current.get(objectType)
+ if (objectType && subscribedCallbacksRef.current.has(callbacksRefKey)) {
+ const callbacks = subscribedCallbacksRef.current.get(callbacksRefKey)
logger.debug(
`Calling ${callbacks.length} callbacks for type:`,
- objectType
+ callbacksRefKey
)
callbacks.forEach((callback) => {
try {
@@ -332,7 +366,7 @@ const ApiServerProvider = ({ children }) => {
})
} else {
logger.debug(
- `No callbacks found for object: ${objectType}, subscribed callbacks:`,
+ `No callbacks found for object: ${callbacksRefKey}, subscribed callbacks:`,
Array.from(subscribedCallbacksRef.current.keys())
)
}
@@ -341,12 +375,16 @@ const ApiServerProvider = ({ children }) => {
const handleObjectDelete = async (data) => {
logger.debug('Notifying object delete:', data)
const objectType = data.objectType || 'unknown'
+ const callbacksRefKey = getObjectTypeSubscriptionKey(
+ objectType,
+ data.filter || {}
+ )
- if (objectType && subscribedCallbacksRef.current.has(objectType)) {
- const callbacks = subscribedCallbacksRef.current.get(objectType)
+ if (objectType && subscribedCallbacksRef.current.has(callbacksRefKey)) {
+ const callbacks = subscribedCallbacksRef.current.get(callbacksRefKey)
logger.debug(
`Calling ${callbacks.length} callbacks for type:`,
- objectType
+ callbacksRefKey
)
callbacks.forEach((callback) => {
try {
@@ -357,7 +395,7 @@ const ApiServerProvider = ({ children }) => {
})
} else {
logger.debug(
- `No callbacks found for object: ${objectType}, subscribed callbacks:`,
+ `No callbacks found for object: ${callbacksRefKey}, subscribed callbacks:`,
Array.from(subscribedCallbacksRef.current.keys())
)
}
@@ -389,24 +427,29 @@ const ApiServerProvider = ({ children }) => {
}
}, [])
- const offObjectTypeUpdatesEvent = useCallback((objectType, callback) => {
- if (socketRef.current && socketRef.current.connected == true) {
- // Remove callback from the subscribed callbacks map
- if (subscribedCallbacksRef.current.has(objectType)) {
- const callbacks = subscribedCallbacksRef.current
- .get(objectType)
- .filter((cb) => cb !== callback)
- if (callbacks.length === 0) {
- subscribedCallbacksRef.current.delete(objectType)
- socketRef.current.emit('unsubscribeObjectTypeUpdate', {
- objectType: objectType
- })
- } else {
- subscribedCallbacksRef.current.set(objectType, callbacks)
+ const offObjectTypeUpdatesEvent = useCallback(
+ (objectType, filter, callback) => {
+ if (socketRef.current && socketRef.current.connected == true) {
+ const callbacksRefKey = getObjectTypeSubscriptionKey(objectType, filter)
+ // Remove callback from the subscribed callbacks map
+ if (subscribedCallbacksRef.current.has(callbacksRefKey)) {
+ const callbacks = subscribedCallbacksRef.current
+ .get(callbacksRefKey)
+ .filter((cb) => cb !== callback)
+ if (callbacks.length === 0) {
+ subscribedCallbacksRef.current.delete(callbacksRefKey)
+ socketRef.current.emit('unsubscribeObjectTypeUpdate', {
+ objectType: objectType,
+ filter: filter
+ })
+ } else {
+ subscribedCallbacksRef.current.set(callbacksRefKey, callbacks)
+ }
}
}
- }
- }, [])
+ },
+ []
+ )
const subscribeToObjectUpdates = useCallback(
(id, objectType, callback) => {
@@ -471,31 +514,46 @@ const ApiServerProvider = ({ children }) => {
}, [connected, userProfile?._id, subscribeToObjectUpdates, setUserProfile])
const subscribeToObjectTypeUpdates = useCallback(
- (objectType, callback) => {
- logger.debug('Subscribing to type updates:', objectType)
+ (objectType, filterOrCallback = {}, maybeCallback) => {
+ const { filter, callback } = getObjectTypeSubscriptionArgs(
+ filterOrCallback,
+ maybeCallback
+ )
+ const callbacksRefKey = getObjectTypeSubscriptionKey(objectType, filter)
+
+ logger.debug('Subscribing to type updates:', objectType, filter)
if (socketRef.current && socketRef.current.connected == true) {
// Add callback to the subscribed callbacks map immediately
- if (!subscribedCallbacksRef.current.has(objectType)) {
- subscribedCallbacksRef.current.set(objectType, [])
+ if (!subscribedCallbacksRef.current.has(callbacksRefKey)) {
+ subscribedCallbacksRef.current.set(callbacksRefKey, [])
}
- subscribedCallbacksRef.current.get(objectType).push(callback)
- logger.debug(
- `Added callback for type ${objectType}, total callbacks: ${subscribedCallbacksRef.current.get(objectType).length}`
- )
- socketRef.current.emit(
- 'subscribeToObjectTypeUpdate',
- { objectType: objectType },
- (result) => {
- if (result.success) {
- logger.info('Subscribed to objectType:', objectType)
+ const callbacksLength =
+ subscribedCallbacksRef.current.get(callbacksRefKey).length
+
+ if (callbacksLength <= 0) {
+ socketRef.current.emit(
+ 'subscribeToObjectTypeUpdate',
+ { objectType: objectType, filter: filter },
+ (result) => {
+ if (result.success) {
+ logger.info('Subscribed to objectType:', objectType, filter)
+ }
}
- }
+ )
+ }
+
+ subscribedCallbacksRef.current.get(callbacksRefKey).push(callback)
+ logger.debug(
+ `Added callback for type ${callbacksRefKey}, total callbacks: ${callbacksLength + 1}`
+ )
+ logger.debug(
+ 'Registered type event listener for object:',
+ callbacksRefKey
)
- logger.debug('Registered type event listener for object:', objectType)
// Return cleanup function
- return () => offObjectTypeUpdatesEvent(objectType, callback)
+ return () => offObjectTypeUpdatesEvent(objectType, filter, callback)
}
},
[offObjectTypeUpdatesEvent]
@@ -708,7 +766,7 @@ const ApiServerProvider = ({ children }) => {
// Generalized fetchObject function
const fetchObject = async (id, type) => {
- const fetchUrl = `${config.backendUrl}/${type}s/${id}`
+ const fetchUrl = `${config.backendUrl}/${getObjectEndpoint(type)}/${id}`
setFetchLoading(true)
logger.debug('Fetching from ' + fetchUrl)
try {
@@ -738,21 +796,37 @@ const ApiServerProvider = ({ children }) => {
sorter = {},
onDataChange
} = params
+
+ let newFilter = { ...filter }
+
+ if (filter != null && Object.keys(filter).length > 0) {
+ const model = getModelByName(type)
+
+ for (const key of Object.keys(filter)) {
+ const property = model?.properties?.find((p) => p.name === key)
+ if (property && property.type === 'object') {
+ const value = filter[key]
+ newFilter[`${key}._id`] = value?._id ?? value
+ delete newFilter[key]
+ }
+ }
+ }
+
logger.debug('Fetching table data from:', type, {
page,
limit,
- filter,
+ newFilter,
sorter
})
try {
const response = await axios.get(
- `${config.backendUrl}/${type.toLowerCase()}s`,
+ `${config.backendUrl}/${getObjectEndpoint(type)}`,
{
params: {
page,
limit,
- ...filter,
+ ...newFilter,
sort: sorter.field,
order: sorter.order
},
@@ -799,7 +873,7 @@ const ApiServerProvider = ({ children }) => {
try {
const response = await axios.get(
- `${config.backendUrl}/${type.toLowerCase()}s/properties`,
+ `${config.backendUrl}/${getObjectEndpoint(type)}/properties`,
{
params: {
...Object.keys(filter).reduce((acc, key) => {
@@ -833,7 +907,7 @@ const ApiServerProvider = ({ children }) => {
// Update filament information
const updateObject = async (id, type, value) => {
- const updateUrl = `${config.backendUrl}/${type.toLowerCase()}s/${id}`
+ const updateUrl = `${config.backendUrl}/${getObjectEndpoint(type)}/${id}`
logger.debug('Updating info for ' + id)
try {
const response = await axios.put(updateUrl, value, {
@@ -855,7 +929,7 @@ const ApiServerProvider = ({ children }) => {
// Update multiple objects
const updateMultipleObjects = async (type, objects) => {
- const updateUrl = `${config.backendUrl}/${type.toLowerCase()}s`
+ const updateUrl = `${config.backendUrl}/${getObjectEndpoint(type)}`
logger.debug('Updating multiple objects for ' + type)
try {
const response = await axios.put(updateUrl, objects, {
@@ -877,7 +951,7 @@ const ApiServerProvider = ({ children }) => {
// Update filament information
const deleteObject = async (id, type) => {
- const deleteUrl = `${config.backendUrl}/${type.toLowerCase()}s/${id}`
+ const deleteUrl = `${config.backendUrl}/${getObjectEndpoint(type)}/${id}`
logger.debug('Deleting object ID: ' + id)
try {
const response = await axios.delete(deleteUrl, {
@@ -899,7 +973,7 @@ const ApiServerProvider = ({ children }) => {
// Update filament information
const createObject = async (type, value) => {
- const createUrl = `${config.backendUrl}/${type.toLowerCase()}s`
+ const createUrl = `${config.backendUrl}/${getObjectEndpoint(type)}`
logger.debug('Creating object...')
try {
const response = await axios.post(createUrl, value, {
@@ -920,7 +994,7 @@ const ApiServerProvider = ({ children }) => {
// Call a function on an object
const sendObjectFunction = async (id, type, functionName, value = {}) => {
- const url = `${config.backendUrl}/${type.toLowerCase()}s/${id}/${functionName}`
+ const url = `${config.backendUrl}/${getObjectEndpoint(type)}/${id}/${functionName}`
logger.debug(`Calling object function ${functionName} for ${id} at ${url}`)
try {
const response = await axios.post(url, value, {
@@ -940,7 +1014,7 @@ const ApiServerProvider = ({ children }) => {
}
const getObjectFunction = async (id, type, functionName, params = {}) => {
- const url = `${config.backendUrl}/${type.toLowerCase()}s/${id}/${functionName}`
+ const url = `${config.backendUrl}/${getObjectEndpoint(type)}/${id}/${functionName}`
logger.debug(`Fetching object function ${functionName} for ${id} at ${url}`)
try {
const response = await axios.get(url, {
@@ -1165,7 +1239,7 @@ const ApiServerProvider = ({ children }) => {
if (objectType === 'history') {
statsUrl = `${config.backendUrl}/stats/history`
} else {
- statsUrl = `${config.backendUrl}/${objectType.toLowerCase()}s/stats`
+ statsUrl = `${config.backendUrl}/${getObjectEndpoint(objectType)}/stats`
}
const response = await axios.get(statsUrl, {
@@ -1190,7 +1264,7 @@ const ApiServerProvider = ({ children }) => {
const encodedStartDate = encodeURIComponent(startDate.toISOString())
const encodedEndDate = encodeURIComponent(endDate.toISOString())
try {
- const historyUrl = `${config.backendUrl}/${objectType.toLowerCase()}s/history?from=${encodedStartDate}&to=${encodedEndDate}`
+ const historyUrl = `${config.backendUrl}/${getObjectEndpoint(objectType)}/history?from=${encodedStartDate}&to=${encodedEndDate}`
const response = await axios.get(historyUrl, {
headers: {
diff --git a/src/components/Icons/ProductCategoryIcon.jsx b/src/components/Icons/ProductCategoryIcon.jsx
new file mode 100644
index 0000000..a6af0f2
--- /dev/null
+++ b/src/components/Icons/ProductCategoryIcon.jsx
@@ -0,0 +1,8 @@
+import Icon from '@ant-design/icons'
+import CustomIconSvg from '../../../assets/icons/productcategoryicon.svg?react'
+
+const ProductCategoryIcon = (props) => (
+
+)
+
+export default ProductCategoryIcon
diff --git a/src/database/ObjectModels.js b/src/database/ObjectModels.js
index a66b856..ed4bc9b 100644
--- a/src/database/ObjectModels.js
+++ b/src/database/ObjectModels.js
@@ -7,6 +7,7 @@ import { Spool } from './models/Spool'
import { GCodeFile } from './models/GCodeFile'
import { Job } from './models/Job'
import { Product } from './models/Product'
+import { ProductCategory } from './models/ProductCategory'
import { ProductSku } from './models/ProductSku'
import { Part } from './models/Part.js'
import { PartSku } from './models/PartSku.js'
@@ -56,6 +57,7 @@ export const objectModels = [
GCodeFile,
Job,
Product,
+ ProductCategory,
ProductSku,
Part,
PartSku,
@@ -106,6 +108,7 @@ export {
GCodeFile,
Job,
Product,
+ ProductCategory,
ProductSku,
Part,
PartSku,
diff --git a/src/database/models/File.js b/src/database/models/File.js
index 44e225e..ffa7190 100644
--- a/src/database/models/File.js
+++ b/src/database/models/File.js
@@ -70,7 +70,15 @@ export const File = {
}
],
url: (id) => `/dashboard/management/files/info?fileId=${id}`,
- columns: ['_reference', 'name', 'type', 'size', 'temp', 'createdAt'],
+ columns: [
+ '_reference',
+ 'name',
+ 'type',
+ 'size',
+ 'temp',
+ 'createdAt',
+ 'updatedAt'
+ ],
filters: ['name', '_id', 'type', 'temp'],
sorters: ['name', 'type', 'size', 'createdAt', 'temp'],
group: ['type'],
@@ -121,7 +129,7 @@ export const File = {
type: 'text',
readOnly: true,
required: true,
- columnWidth: 120
+ columnWidth: 190
},
{
name: 'size',
diff --git a/src/database/models/GCodeFile.js b/src/database/models/GCodeFile.js
index 2d2beac..99fcc15 100644
--- a/src/database/models/GCodeFile.js
+++ b/src/database/models/GCodeFile.js
@@ -82,8 +82,23 @@ export const GCodeFile = {
'gcodeFileInfo.hotPlateTemp',
'updatedAt'
],
- filters: ['_id', 'name', 'filament', 'filament._id', 'filamentSku', 'cost', 'updatedAt'],
- sorters: ['name', 'filament', 'filamentSku', 'cost', 'createdAt', 'updatedAt'],
+ filters: [
+ '_id',
+ 'name',
+ 'filament',
+ 'filament._id',
+ 'filamentSku',
+ 'cost',
+ 'updatedAt'
+ ],
+ sorters: [
+ 'name',
+ 'filament',
+ 'filamentSku',
+ 'cost',
+ 'createdAt',
+ 'updatedAt'
+ ],
group: ['filament', 'filamentSku'],
properties: [
{
@@ -255,6 +270,7 @@ export const GCodeFile = {
label: 'Parts',
type: 'objectChildren',
objectType: 'part',
+ size: 'medium',
properties: [
{
name: 'part',
diff --git a/src/database/models/Host.js b/src/database/models/Host.js
index 09f263f..85b80fe 100644
--- a/src/database/models/Host.js
+++ b/src/database/models/Host.js
@@ -3,6 +3,7 @@ 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 OTPIcon from '../../components/Icons/OTPIcon'
export const Host = {
@@ -60,6 +61,14 @@ export const Host = {
icon: OTPIcon,
url: (_id) =>
`/dashboard/management/hosts/info?hostId=${_id}&action=hostOTP`
+ },
+ {
+ name: 'delete',
+ label: 'Delete',
+ icon: BinIcon,
+ danger: true,
+ url: (_id) =>
+ `/dashboard/management/hosts/info?hostId=${_id}&action=delete`
}
],
columns: ['_reference', 'name', 'state', 'tags', 'connectedAt'],
diff --git a/src/database/models/Product.js b/src/database/models/Product.js
index 89fd6dc..7107f78 100644
--- a/src/database/models/Product.js
+++ b/src/database/models/Product.js
@@ -68,6 +68,7 @@ export const Product = {
columns: [
'_reference',
'name',
+ 'productCategory',
'tags',
'vendor',
'cost',
@@ -77,8 +78,30 @@ export const Product = {
'createdAt',
'updatedAt'
],
- filters: ['_id', 'name', 'type', 'color', 'vendor', 'cost', 'costWithTax', 'price', 'priceWithTax'],
- sorters: ['name', 'createdAt', 'type', 'vendor', 'cost', 'costWithTax', 'price', 'priceWithTax', 'updatedAt'],
+ filters: [
+ '_id',
+ 'name',
+ 'productCategory',
+ 'type',
+ 'color',
+ 'vendor',
+ 'cost',
+ 'costWithTax',
+ 'price',
+ 'priceWithTax'
+ ],
+ sorters: [
+ 'name',
+ 'productCategory',
+ 'createdAt',
+ 'type',
+ 'vendor',
+ 'cost',
+ 'costWithTax',
+ 'price',
+ 'priceWithTax',
+ 'updatedAt'
+ ],
properties: [
{
name: '_id',
@@ -120,6 +143,15 @@ export const Product = {
readOnly: true,
columnWidth: 175
},
+ {
+ name: 'productCategory',
+ label: 'Product Category',
+ required: true,
+ type: 'object',
+ objectType: 'productCategory',
+ showHyperlink: true,
+ columnWidth: 200
+ },
{
name: 'vendor',
label: 'Vendor',
diff --git a/src/database/models/ProductCategory.js b/src/database/models/ProductCategory.js
new file mode 100644
index 0000000..9445da4
--- /dev/null
+++ b/src/database/models/ProductCategory.js
@@ -0,0 +1,113 @@
+import ProductCategoryIcon from '../../components/Icons/ProductCategoryIcon'
+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'
+
+export const ProductCategory = {
+ name: 'productCategory',
+ label: 'Product Category',
+ prefix: 'PCG',
+ endpoint: 'productcategories',
+ icon: ProductCategoryIcon,
+ actions: [
+ {
+ name: 'info',
+ label: 'Info',
+ default: true,
+ row: true,
+ icon: InfoCircleIcon,
+ url: (_id) =>
+ `/dashboard/management/productcategories/info?productCategoryId=${_id}`
+ },
+ {
+ name: 'edit',
+ label: 'Edit',
+ row: true,
+ icon: EditIcon,
+ url: (_id) =>
+ `/dashboard/management/productcategories/info?productCategoryId=${_id}&action=edit`,
+ visible: (objectData) => {
+ return !(objectData?._isEditing && objectData?._isEditing == true)
+ }
+ },
+ {
+ name: 'finishEdit',
+ label: 'Save Edits',
+ icon: CheckIcon,
+ url: (_id) =>
+ `/dashboard/management/productcategories/info?productCategoryId=${_id}&action=finishEdit`,
+ visible: (objectData) => {
+ return objectData?._isEditing && objectData?._isEditing == true
+ }
+ },
+ {
+ name: 'cancelEdit',
+ label: 'Cancel Edits',
+ icon: XMarkIcon,
+ url: (_id) =>
+ `/dashboard/management/productcategories/info?productCategoryId=${_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/productcategories/info?productCategoryId=${_id}&action=delete`
+ }
+ ],
+ url: (id) =>
+ `/dashboard/management/productcategories/info?productCategoryId=${id}`,
+ columns: ['_reference', 'name', 'createdAt', 'updatedAt'],
+ filters: ['_id', 'name'],
+ sorters: ['name', 'createdAt', 'updatedAt', '_id'],
+ properties: [
+ {
+ name: '_id',
+ label: 'ID',
+ columnFixed: 'left',
+ type: 'id',
+ objectType: 'productCategory',
+ showCopy: true,
+ readOnly: true,
+ columnWidth: 140
+ },
+ {
+ name: 'createdAt',
+ label: 'Created At',
+ type: 'dateTime',
+ readOnly: true,
+ columnWidth: 175
+ },
+ {
+ name: '_reference',
+ label: 'Reference',
+ type: 'reference',
+ columnFixed: 'left',
+ objectType: 'productCategory',
+ showCopy: true,
+ readOnly: true
+ },
+ {
+ name: 'name',
+ label: 'Name',
+ columnFixed: 'left',
+ required: true,
+ type: 'text',
+ columnWidth: 200
+ },
+ {
+ name: 'updatedAt',
+ label: 'Updated At',
+ type: 'dateTime',
+ readOnly: true,
+ columnWidth: 175
+ }
+ ]
+}
diff --git a/src/database/models/PurchaseOrder.js b/src/database/models/PurchaseOrder.js
index 41cd8de..80d669a 100644
--- a/src/database/models/PurchaseOrder.js
+++ b/src/database/models/PurchaseOrder.js
@@ -172,8 +172,26 @@ export const PurchaseOrder = {
}
],
group: ['vendor'],
- filters: ['vendor'],
- sorters: ['createdAt', 'state', 'updatedAt'],
+ filters: [
+ 'vendor',
+ 'totalAmount',
+ 'totalAmountWithTax',
+ 'totalTaxAmount',
+ 'shippingAmount',
+ 'shippingAmountWithTax',
+ 'grandTotalAmount'
+ ],
+ sorters: [
+ 'createdAt',
+ 'state',
+ 'updatedAt',
+ 'totalAmount',
+ 'totalAmountWithTax',
+ 'totalTaxAmount',
+ 'shippingAmount',
+ 'shippingAmountWithTax',
+ 'grandTotalAmount'
+ ],
columns: [
'_reference',
'state',
@@ -260,7 +278,7 @@ export const PurchaseOrder = {
prefix: '£',
roundNumber: 2,
readOnly: true,
- columnWidth: 175
+ columnWidth: 185
},
{
name: 'completedAt',
@@ -275,7 +293,7 @@ export const PurchaseOrder = {
type: 'number',
prefix: '£',
readOnly: true,
- columnWidth: 175,
+ columnWidth: 215,
roundNumber: 2
},
{
@@ -285,7 +303,7 @@ export const PurchaseOrder = {
prefix: '£',
roundNumber: 2,
readOnly: true,
- columnWidth: 150
+ columnWidth: 190
},
{
name: 'shippingAmountWithTax',
@@ -294,7 +312,7 @@ export const PurchaseOrder = {
prefix: '£',
readOnly: true,
roundNumber: 2,
- columnWidth: 200
+ columnWidth: 240
},
{
name: 'totalAmount',
@@ -303,7 +321,7 @@ export const PurchaseOrder = {
prefix: '£',
roundNumber: 2,
readOnly: true,
- columnWidth: 150
+ columnWidth: 170
},
{
name: 'grandTotalAmount',
@@ -311,7 +329,7 @@ export const PurchaseOrder = {
type: 'number',
prefix: '£',
roundNumber: 2,
- columnWidth: 175,
+ columnWidth: 215,
readOnly: true
}
],
diff --git a/src/database/models/SalesOrder.js b/src/database/models/SalesOrder.js
index 3b1dd60..b44ffb6 100644
--- a/src/database/models/SalesOrder.js
+++ b/src/database/models/SalesOrder.js
@@ -170,7 +170,7 @@ export const SalesOrder = {
}
}
],
- group: ['client', 'marketplace'],
+ group: ['client'],
filters: ['client', 'marketplace'],
sorters: ['createdAt', 'state', 'updatedAt'],
columns: [
diff --git a/src/database/models/StockTransfer.js b/src/database/models/StockTransfer.js
index b6e08e4..fb9205c 100644
--- a/src/database/models/StockTransfer.js
+++ b/src/database/models/StockTransfer.js
@@ -85,9 +85,9 @@ export const StockTransfer = {
}
],
url: (id) => `/dashboard/inventory/stocktransfers/info?stockTransferId=${id}`,
- filters: ['_id', 'state'],
- sorters: ['createdAt', 'postedAt'],
- columns: ['_reference', 'state', 'postedAt', 'createdAt', 'updatedAt'],
+ filters: ['_id', 'name', 'state'],
+ sorters: ['name', 'createdAt', 'postedAt'],
+ columns: ['_reference', 'name', 'state', 'postedAt', 'createdAt', 'updatedAt'],
properties: [
{
name: '_id',
@@ -114,6 +114,14 @@ export const StockTransfer = {
showCopy: true,
readOnly: true
},
+ {
+ name: 'name',
+ label: 'Name',
+ type: 'text',
+ required: true,
+ columnWidth: 220,
+ columnFixed: 'left'
+ },
{
name: 'state',
label: 'State',
@@ -177,6 +185,38 @@ export const StockTransfer = {
suffix: (row) =>
row?.fromStockType === 'filamentStock' ? 'g net' : null
},
+ {
+ name: 'available',
+ label: 'Available',
+ type: 'number',
+ readOnly: true,
+ columnWidth: 140,
+ value: (row) => {
+ if (row?.fromStockType === 'filamentStock') {
+ return row?.fromStock?.currentWeight?.net ?? 0
+ } else {
+ return row?.fromStock?.currentQuantity ?? 0
+ }
+ },
+ suffix: (row) =>
+ row?.fromStockType === 'filamentStock' ? 'g net' : null
+ },
+ {
+ name: 'remaining',
+ label: 'Remaining',
+ type: 'number',
+ readOnly: true,
+ columnWidth: 140,
+ value: (row) => {
+ const quantity = row?.quantity ?? 0
+ if (row?.fromStockType === 'filamentStock') {
+ return (row?.fromStock?.currentWeight?.net ?? 0) - quantity
+ }
+ return (row?.fromStock?.currentQuantity ?? 0) - quantity
+ },
+ suffix: (row) =>
+ row?.fromStockType === 'filamentStock' ? 'g net' : null
+ },
{
name: 'toStockLocation',
label: 'To location',
diff --git a/src/routes/ManagementRoutes.jsx b/src/routes/ManagementRoutes.jsx
index 895538f..5836544 100644
--- a/src/routes/ManagementRoutes.jsx
+++ b/src/routes/ManagementRoutes.jsx
@@ -11,6 +11,8 @@ const PartSkus = lazy(() => import('../components/Dashboard/Management/PartSkus.
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 ProductCategories = lazy(() => import('../components/Dashboard/Management/ProductCategories.jsx'))
+const ProductCategoryInfo = lazy(() => import('../components/Dashboard/Management/ProductCategories/ProductCategoryInfo.jsx'))
const ProductSkus = lazy(() => import('../components/Dashboard/Management/ProductSkus.jsx'))
const ProductSkuInfo = lazy(() => import('../components/Dashboard/Management/ProductSkus/ProductSkuInfo.jsx'))
const Vendors = lazy(() => import('../components/Dashboard/Management/Vendors'))
@@ -79,6 +81,16 @@ const ManagementRoutes = [
path='management/products/info'
element={}
/>,
+ }
+ />,
+ }
+ />,
} />,