diff --git a/src/components/Dashboard/Inventory/ProductStocks.jsx b/src/components/Dashboard/Inventory/ProductStocks.jsx
new file mode 100644
index 0000000..974f8ff
--- /dev/null
+++ b/src/components/Dashboard/Inventory/ProductStocks.jsx
@@ -0,0 +1,107 @@
+// src/components/Dashboard/Inventory/ProductStocks.jsx
+// ProductStocks - tracks assembled products consisting of part stocks
+
+import { useState, useRef } from 'react'
+
+import { Button, Flex, Space, Modal, Dropdown } from 'antd'
+
+import NewProductStock from './ProductStocks/NewProductStock'
+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 ProductStocks = () => {
+ const tableRef = useRef()
+
+ const [newProductStockOpen, setNewProductStockOpen] = useState(false)
+
+ const [viewMode, setViewMode] = useViewMode('productStocks')
+
+ const [columnVisibility, setColumnVisibility] =
+ useColumnVisibility('productStock')
+
+ const actionItems = {
+ items: [
+ {
+ label: 'New Product Stock',
+ key: 'newProductStock',
+ icon:
+ },
+ { type: 'divider' },
+ {
+ label: 'Reload List',
+ key: 'reloadList',
+ icon:
+ }
+ ],
+ onClick: ({ key }) => {
+ if (key === 'reloadList') {
+ tableRef.current?.reload()
+ } else if (key === 'newProductStock') {
+ setNewProductStockOpen(true)
+ }
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ : }
+ onClick={() =>
+ setViewMode(viewMode === 'cards' ? 'list' : 'cards')
+ }
+ />
+
+
+
+
+
+ {
+ setNewProductStockOpen(false)
+ }}
+ destroyOnHidden={true}
+ >
+ {
+ setNewProductStockOpen(false)
+ tableRef.current?.reload()
+ }}
+ reset={newProductStockOpen}
+ />
+
+ >
+ )
+}
+
+export default ProductStocks
diff --git a/src/components/Dashboard/Inventory/ProductStocks/NewProductStock.jsx b/src/components/Dashboard/Inventory/ProductStocks/NewProductStock.jsx
new file mode 100644
index 0000000..4e40425
--- /dev/null
+++ b/src/components/Dashboard/Inventory/ProductStocks/NewProductStock.jsx
@@ -0,0 +1,77 @@
+import PropTypes from 'prop-types'
+import ObjectInfo from '../../common/ObjectInfo'
+import NewObjectForm from '../../common/NewObjectForm'
+import WizardView from '../../common/WizardView'
+
+const NewProductStock = ({ onOk, reset, defaultValues }) => {
+ 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()
+ }
+ }}
+ />
+ )
+ }}
+
+ )
+}
+
+NewProductStock.propTypes = {
+ onOk: PropTypes.func.isRequired,
+ reset: PropTypes.bool,
+ defaultValues: PropTypes.object
+}
+
+export default NewProductStock
diff --git a/src/components/Dashboard/Inventory/ProductStocks/ProductStockInfo.jsx b/src/components/Dashboard/Inventory/ProductStocks/ProductStockInfo.jsx
new file mode 100644
index 0000000..8eb262d
--- /dev/null
+++ b/src/components/Dashboard/Inventory/ProductStocks/ProductStockInfo.jsx
@@ -0,0 +1,237 @@
+import { useRef, useState } from 'react'
+import { useLocation } from 'react-router-dom'
+import { Space, Flex, Card } from 'antd'
+import { LoadingOutlined } from '@ant-design/icons'
+import loglevel from 'loglevel'
+import config from '../../../../config.js'
+import useCollapseState from '../../hooks/useCollapseState.jsx'
+import NotesPanel from '../../common/NotesPanel.jsx'
+import InfoCollapse from '../../common/InfoCollapse.jsx'
+import ObjectInfo from '../../common/ObjectInfo.jsx'
+import ObjectProperty from '../../common/ObjectProperty.jsx'
+import ViewButton from '../../common/ViewButton.jsx'
+import { getModelProperty } from '../../../../database/ObjectModels.js'
+import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
+import NoteIcon from '../../../Icons/NoteIcon.jsx'
+import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
+import PartStockIcon from '../../../Icons/PartStockIcon.jsx'
+import ObjectForm from '../../common/ObjectForm.jsx'
+import EditButtons from '../../common/EditButtons.jsx'
+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 log = loglevel.getLogger('ProductStockInfo')
+log.setLevel(config.logLevel)
+
+const ProductStockInfo = () => {
+ const location = useLocation()
+ const objectFormRef = useRef(null)
+ const actionHandlerRef = useRef(null)
+ const productStockId = new URLSearchParams(location.search).get(
+ 'productStockId'
+ )
+ const [collapseState, updateCollapseState] = useCollapseState(
+ 'ProductStockInfo',
+ {
+ info: true,
+ partStocks: true,
+ notes: true,
+ auditLogs: true
+ }
+ )
+
+ const [objectFormState, setEditFormState] = useState({
+ isEditing: false,
+ editLoading: false,
+ formValid: false,
+ locked: false,
+ loading: false,
+ objectData: {}
+ })
+
+ const actions = {
+ reload: () => {
+ objectFormRef?.current.handleFetchObject()
+ return true
+ },
+ edit: () => {
+ objectFormRef?.current.startEditing()
+ return false
+ },
+ cancelEdit: () => {
+ objectFormRef?.current.cancelEditing()
+ return true
+ },
+ finishEdit: () => {
+ objectFormRef?.current.handleUpdate()
+ 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}
+ />
+
+
+
+
+
+
+ {
+ setEditFormState((prev) => ({ ...prev, ...state }))
+ }}
+ >
+ {({ loading, isEditing, objectData }) => (
+
+ }
+ active={collapseState.info}
+ onToggle={(expanded) =>
+ updateCollapseState('info', expanded)
+ }
+ collapseKey='info'
+ >
+ }
+ isEditing={isEditing}
+ type='productStock'
+ objectData={objectData}
+ labelWidth='175px'
+ visibleProperties={{ partStocks: false }}
+ />
+
+ }
+ active={collapseState.partStocks}
+ onToggle={(expanded) =>
+ updateCollapseState('partStocks', expanded)
+ }
+ collapseKey='partStocks'
+ >
+
+
+
+ )}
+
+
+
+ }
+ active={collapseState.notes}
+ onToggle={(expanded) => updateCollapseState('notes', expanded)}
+ collapseKey='notes'
+ >
+
+
+
+
+
+ }
+ active={collapseState.auditLogs}
+ onToggle={(expanded) =>
+ updateCollapseState('auditLogs', expanded)
+ }
+ collapseKey='auditLogs'
+ >
+ {objectFormState.loading ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ >
+ )
+}
+
+export default ProductStockInfo
diff --git a/src/components/Dashboard/common/ObjectChildTable.jsx b/src/components/Dashboard/common/ObjectChildTable.jsx
index 6c9d898..96c0d10 100644
--- a/src/components/Dashboard/common/ObjectChildTable.jsx
+++ b/src/components/Dashboard/common/ObjectChildTable.jsx
@@ -51,6 +51,7 @@ const ObjectChildTable = ({
properties = [],
columns = [],
visibleColumns = {},
+ canAddRemove = true,
objectData = null,
scrollHeight = 240,
size = 'small',
@@ -154,56 +155,57 @@ const ObjectChildTable = ({
}
}))
- const deleteColumn = isEditing
- ? {
- title: '',
- key: 'delete',
- width: 10,
- fixed: 'right',
- render: (_text, record, index) => {
- if (record?.isSkeleton) {
- return null
+ const deleteColumn =
+ isEditing && canAddRemove
+ ? {
+ title: '',
+ key: 'delete',
+ width: 10,
+ fixed: 'right',
+ render: (_text, record, index) => {
+ if (record?.isSkeleton) {
+ return null
+ }
+ return (
+ }
+ onClick={(e) => {
+ e.stopPropagation()
+ const currentItems = Array.isArray(itemsSource)
+ ? itemsSource
+ : []
+
+ // Use record's unique identifier if available, otherwise use index
+ let newItems
+ if (typeof rowKey === 'string' && record[rowKey] != null) {
+ // Use the unique key to find and remove the item
+ newItems = currentItems.filter(
+ (item) => item[rowKey] !== record[rowKey]
+ )
+ } else if (typeof rowKey === 'function') {
+ // If rowKey is a function, find the item by comparing the resolved keys
+ const recordKey = rowKey(record, index)
+ newItems = currentItems.filter((item, i) => {
+ const itemKey = rowKey(item, i)
+ return itemKey !== recordKey
+ })
+ } else {
+ // Fallback to index-based removal
+ newItems = currentItems.filter((_, i) => i !== index)
+ }
+
+ if (typeof onChange === 'function') {
+ onChange(newItems)
+ }
+ }}
+ />
+ )
}
- return (
- }
- onClick={(e) => {
- e.stopPropagation()
- const currentItems = Array.isArray(itemsSource)
- ? itemsSource
- : []
-
- // Use record's unique identifier if available, otherwise use index
- let newItems
- if (typeof rowKey === 'string' && record[rowKey] != null) {
- // Use the unique key to find and remove the item
- newItems = currentItems.filter(
- (item) => item[rowKey] !== record[rowKey]
- )
- } else if (typeof rowKey === 'function') {
- // If rowKey is a function, find the item by comparing the resolved keys
- const recordKey = rowKey(record, index)
- newItems = currentItems.filter((item, i) => {
- const itemKey = rowKey(item, i)
- return itemKey !== recordKey
- })
- } else {
- // Fallback to index-based removal
- newItems = currentItems.filter((_, i) => i !== index)
- }
-
- if (typeof onChange === 'function') {
- onChange(newItems)
- }
- }}
- />
- )
}
- }
- : null
+ : null
return [
...propertyColumns,
@@ -335,17 +337,18 @@ const ObjectChildTable = ({
}
}
})
- const blankDeleteColumn = isEditing
- ? {
- title: '',
- key: 'delete',
- width: 40,
- fixed: 'right',
- render: () => {
- return
+ const blankDeleteColumn =
+ isEditing && canAddRemove
+ ? {
+ title: '',
+ key: 'delete',
+ width: 40,
+ fixed: 'right',
+ render: () => {
+ return
+ }
}
- }
- : null
+ : null
return [
...propertyColumns,
...(blankDeleteColumn ? [blankDeleteColumn] : [])
@@ -410,7 +413,7 @@ const ObjectChildTable = ({
dataSource={rollupDataSource}
showHeader={false}
columns={rollupColumns}
- loading={loading}
+ loading={{ spinning: loading, indicator: <>> }}
pagination={false}
size={size}
rowKey={resolvedRowKey}
@@ -449,12 +452,16 @@ const ObjectChildTable = ({
- }
- onClick={handleAddItem}
- />
+
+ {canAddRemove && (
+ }
+ onClick={handleAddItem}
+ />
+ )}
+
{tableComponent}
@@ -487,7 +494,8 @@ ObjectChildTable.propTypes = {
onChange: PropTypes.func,
maxWidth: PropTypes.string,
rollups: PropTypes.arrayOf(PropTypes.object),
- objectData: PropTypes.object
+ objectData: PropTypes.object,
+ canAddRemove: PropTypes.bool
}
export default ObjectChildTable
diff --git a/src/components/Dashboard/common/ObjectProperty.jsx b/src/components/Dashboard/common/ObjectProperty.jsx
index e63072c..0d8e4d0 100644
--- a/src/components/Dashboard/common/ObjectProperty.jsx
+++ b/src/components/Dashboard/common/ObjectProperty.jsx
@@ -91,6 +91,7 @@ const ObjectProperty = ({
options = [],
roundNumber = false,
fixedNumber = false,
+ canAddRemove = true,
showHyperlink,
showSince,
properties = [],
@@ -415,6 +416,7 @@ const ObjectProperty = ({
loading={loading}
rollups={rollups}
size={size}
+ canAddRemove={canAddRemove}
/>
)
}
@@ -804,6 +806,7 @@ const ObjectProperty = ({
isEditing={true}
rollups={rollups}
size={size}
+ canAddRemove={canAddRemove}
{...inputProps}
/>
)
@@ -868,7 +871,8 @@ ObjectProperty.propTypes = {
options: PropTypes.array,
showSince: PropTypes.bool,
loading: PropTypes.bool,
- rollups: PropTypes.arrayOf(PropTypes.object)
+ rollups: PropTypes.arrayOf(PropTypes.object),
+ canAddRemove: PropTypes.bool
}
export default ObjectProperty
diff --git a/src/components/Dashboard/common/ObjectSelect.jsx b/src/components/Dashboard/common/ObjectSelect.jsx
index 5877301..8872dd1 100644
--- a/src/components/Dashboard/common/ObjectSelect.jsx
+++ b/src/components/Dashboard/common/ObjectSelect.jsx
@@ -55,6 +55,11 @@ const ObjectSelect = ({
const prevValueRef = useRef(value)
const isInternalChangeRef = useRef(false)
+ useEffect(() => {
+ console.log('type', type)
+ console.log('value', value)
+ }, [value, type])
+
// Normalize a value to an identity string so we can detect in-place _id updates
const getValueIdentity = useCallback((val) => {
if (val && typeof val === 'object') {
diff --git a/src/database/models/ProductStock.js b/src/database/models/ProductStock.js
index cef7cb0..540e05e 100644
--- a/src/database/models/ProductStock.js
+++ b/src/database/models/ProductStock.js
@@ -2,7 +2,7 @@ import ProductStockIcon from '../../components/Icons/ProductStockIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const ProductStock = {
- name: 'productstock',
+ name: 'productStock',
label: 'Product Stock',
prefix: 'PDS',
icon: ProductStockIcon,
@@ -14,8 +14,107 @@ export const ProductStock = {
row: true,
icon: InfoCircleIcon,
url: (_id) =>
- `/dashboard/management/productstocks/info?productStockId=${_id}`
+ `/dashboard/inventory/productstocks/info?productStockId=${_id}`
}
],
- url: (id) => `/dashboard/management/productstocks/info?productStockId=${id}`
+ url: (id) => `/dashboard/inventory/productstocks/info?productStockId=${id}`,
+ filters: ['_id', 'product', 'currentQuantity'],
+ sorters: ['product', 'currentQuantity'],
+ columns: [
+ '_reference',
+ 'state',
+ 'currentQuantity',
+ 'product',
+ 'createdAt',
+ 'updatedAt'
+ ],
+ properties: [
+ {
+ name: '_id',
+ label: 'ID',
+ type: 'id',
+ objectType: 'productStock',
+ showCopy: true,
+ readOnly: true
+ },
+ {
+ name: 'createdAt',
+ label: 'Created At',
+ type: 'dateTime',
+ readOnly: true
+ },
+ {
+ name: 'state',
+ label: 'State',
+ type: 'state',
+ readOnly: true,
+ columnWidth: 120
+ },
+ {
+ name: 'updatedAt',
+ label: 'Updated At',
+ type: 'dateTime',
+ readOnly: true
+ },
+ {
+ name: 'product',
+ label: 'Product',
+ type: 'object',
+ objectType: 'product',
+ required: true,
+ showHyperlink: true
+ },
+ {
+ name: 'currentQuantity',
+ label: 'Current Quantity',
+ type: 'number',
+ columnWidth: 200,
+ required: true
+ },
+ {
+ name: 'partStocks',
+ label: 'Part Stocks',
+ type: 'objectChildren',
+ canAddRemove: false,
+ properties: [
+ {
+ name: 'part',
+ label: 'Part',
+ type: 'object',
+ objectType: 'part',
+ readOnly: true,
+ required: true,
+ showHyperlink: true
+ },
+ {
+ name: 'partStock',
+ label: 'Part Stock',
+ type: 'object',
+ objectType: 'partStock',
+ required: true,
+ showHyperlink: true,
+ masterFilter: (objectData) => {
+ const partId = objectData?.part?._id
+ if (partId == null) return {}
+ return { 'part._id': partId }
+ }
+ },
+ {
+ name: 'quantity',
+ label: 'Quantity',
+ type: 'number',
+ required: true
+ }
+ ]
+ }
+ ],
+ stats: [
+ {
+ name: 'totalCurrentQuantity.sum',
+ label: 'Total Current Quantity',
+ type: 'number',
+ roundNumber: 2,
+ cardWidth: 200
+ }
+ ]
}
diff --git a/src/routes/InventoryRoutes.jsx b/src/routes/InventoryRoutes.jsx
index fb85b9a..b99b9ea 100644
--- a/src/routes/InventoryRoutes.jsx
+++ b/src/routes/InventoryRoutes.jsx
@@ -11,6 +11,15 @@ const FilamentStockInfo = lazy(
const PartStocks = lazy(
() => import('../components/Dashboard/Inventory/PartStocks.jsx')
)
+const ProductStocks = lazy(
+ () => import('../components/Dashboard/Inventory/ProductStocks.jsx')
+)
+const ProductStockInfo = lazy(
+ () =>
+ import(
+ '../components/Dashboard/Inventory/ProductStocks/ProductStockInfo.jsx'
+ )
+)
const PartStockInfo = lazy(
() => import('../components/Dashboard/Inventory/PartStocks/PartStockInfo.jsx')
)
@@ -73,6 +82,16 @@ const InventoryRoutes = [
path='inventory/partstocks/info'
element={}
/>,
+ }
+ />,
+ }
+ />,