diff --git a/src/components/Dashboard/Inventory/ProductStocks/NewProductStock.jsx b/src/components/Dashboard/Inventory/ProductStocks/NewProductStock.jsx
index d5906af2..2a18c96a 100644
--- a/src/components/Dashboard/Inventory/ProductStocks/NewProductStock.jsx
+++ b/src/components/Dashboard/Inventory/ProductStocks/NewProductStock.jsx
@@ -24,7 +24,7 @@ const NewProductStock = ({ onOk, reset, defaultValues }) => {
required={true}
objectData={objectData}
visibleProperties={{
- partStocks: false
+ partStockList: false
}}
/>
)
@@ -42,7 +42,7 @@ const NewProductStock = ({ onOk, reset, defaultValues }) => {
_reference: false,
createdAt: false,
updatedAt: false,
- partStocks: false
+ partStockList: false
}}
isEditing={false}
objectData={objectData}
diff --git a/src/components/Dashboard/Inventory/ProductStocks/ProductStockInfo.jsx b/src/components/Dashboard/Inventory/ProductStocks/ProductStockInfo.jsx
index be497362..fe2b0492 100644
--- a/src/components/Dashboard/Inventory/ProductStocks/ProductStockInfo.jsx
+++ b/src/components/Dashboard/Inventory/ProductStocks/ProductStockInfo.jsx
@@ -10,7 +10,10 @@ 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, getModelByName } from '../../../../database/ObjectModels.js'
+import {
+ getModelProperty,
+ getModelByName
+} from '../../../../database/ObjectModels.js'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
@@ -54,7 +57,7 @@ const ProductStockInfo = () => {
'ProductStockInfo',
{
info: true,
- partStocks: true,
+ partStockList: true,
events: true,
history: true,
notes: true,
@@ -97,7 +100,7 @@ const ProductStockInfo = () => {
finishEdit: () => {
objectFormRef?.current.handleUpdate()
return true
- },
+ }
}
return (
@@ -123,7 +126,7 @@ const ProductStockInfo = () => {
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Product Stock Information' },
- { key: 'partStocks', label: 'Part Stocks' },
+ { key: 'partStockList', label: 'Part Stock List' },
{ key: 'events', label: 'Product Stock Events' },
{ key: 'history', label: 'Product Stock History' },
{ key: 'notes', label: 'Notes' },
@@ -207,20 +210,20 @@ const ProductStockInfo = () => {
type='productStock'
objectData={objectData}
labelWidth='175px'
- visibleProperties={{ partStocks: false }}
+ visibleProperties={{ partStockList: false }}
/>
}
- active={collapseState.partStocks}
+ active={collapseState.partStockList}
onToggle={(expanded) =>
- updateCollapseState('partStocks', expanded)
+ updateCollapseState('partStockList', expanded)
}
- collapseKey='partStocks'
+ collapseKey='partStockList'
>
{
) : (
)}
@@ -281,7 +287,7 @@ const ProductStockInfo = () => {
indicator={}
>
-
+
@@ -299,10 +305,8 @@ const ProductStockInfo = () => {
diff --git a/src/components/Dashboard/common/NewObjectForm.jsx b/src/components/Dashboard/common/NewObjectForm.jsx
index 0b11e1fc..66bac8f0 100644
--- a/src/components/Dashboard/common/NewObjectForm.jsx
+++ b/src/components/Dashboard/common/NewObjectForm.jsx
@@ -3,10 +3,9 @@ import { Form } from 'antd'
import { ApiServerContext } from '../context/ApiServerContext'
import { useMessageContext } from '../context/MessageContext'
import PropTypes from 'prop-types'
-import merge from 'lodash/merge'
-import mergeWith from 'lodash/mergeWith'
import set from 'lodash/set'
import { getModelByName } from '../../../database/ObjectModels'
+import { mergeFormData } from '../utils/Utils'
const buildObjectFromEntries = (entries = []) => {
return entries.reduce((acc, entry) => {
@@ -20,7 +19,7 @@ const buildObjectFromEntries = (entries = []) => {
}
const applyComputedEntries = (base, entries = []) => {
- const result = merge({}, base || {})
+ const result = mergeFormData(base || {})
entries.forEach((entry) => {
const { namePath, value } = entry || {}
if (!Array.isArray(namePath) || value === undefined) return
@@ -29,15 +28,6 @@ const applyComputedEntries = (base, entries = []) => {
return result
}
-const arrayReplaceCustomizer = (objValue, srcValue, key) => {
- if (Array.isArray(srcValue)) {
- return srcValue
- }
- if (key === 'permissions' && srcValue !== undefined) {
- return srcValue
- }
-}
-
/**
* NewObjectForm is a reusable form component for creating new objects.
* It handles form validation, submission, and error handling logic.
@@ -188,9 +178,7 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
computedEntries
)
form.setFieldsValue(initialFormData)
- setObjectData((prev) =>
- mergeWith({}, prev, initialFormData, arrayReplaceCustomizer)
- )
+ setObjectData((prev) => mergeFormData(prev, initialFormData))
return validateForm()
}
}, [form, defaultValues, calculateComputedValues, model, validateForm])
@@ -233,12 +221,7 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
style={style}
onValuesChange={(_changedValues, allFormValues) => {
// Calculate computed values based on current form data
- const currentFormData = mergeWith(
- {},
- objectData || {},
- allFormValues,
- arrayReplaceCustomizer
- )
+ const currentFormData = mergeFormData(objectData || {}, allFormValues)
const computedEntries = calculateComputedValues(currentFormData, model)
if (Array.isArray(computedEntries) && computedEntries.length > 0) {
@@ -259,9 +242,7 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
}
const allValues = applyComputedEntries(allFormValues, computedEntries)
- setObjectData((prev) => {
- return mergeWith({}, prev, allValues, arrayReplaceCustomizer)
- })
+ setObjectData((prev) => mergeFormData(prev, allValues))
}}
>
{children({
diff --git a/src/components/Dashboard/common/ObjectForm.jsx b/src/components/Dashboard/common/ObjectForm.jsx
index 8c91ac7b..084f49d6 100644
--- a/src/components/Dashboard/common/ObjectForm.jsx
+++ b/src/components/Dashboard/common/ObjectForm.jsx
@@ -11,22 +11,12 @@ import { Form } from 'antd'
import { ApiServerContext } from '../context/ApiServerContext'
import { AuthContext } from '../context/AuthContext'
import { useMessageContext } from '../context/MessageContext'
-import merge from 'lodash/merge'
-import mergeWith from 'lodash/mergeWith'
import set from 'lodash/set'
import { getModelByName } from '../../../database/ObjectModels'
import { useLocation, useNavigate } from 'react-router-dom'
import PropTypes from 'prop-types'
import { useActions } from '../context/ActionsContext'
-
-const arrayReplaceCustomizer = (objValue, srcValue, key) => {
- if (Array.isArray(srcValue)) {
- return srcValue
- }
- if (key === 'permissions' && srcValue !== undefined) {
- return srcValue
- }
-}
+import { mergeFormData } from '../utils/Utils'
const getUserId = (user) => user?._id || user
@@ -82,7 +72,7 @@ const buildObjectFromEntries = (entries = []) => {
// merge-replacing arrays would wipe stored child-row fields (shipment, amount,
// etc.) while leaving calculated columns intact.
const applyComputedEntries = (base, entries = []) => {
- const result = merge({}, base || {})
+ const result = mergeFormData(base || {})
entries.forEach((entry) => {
const { namePath, value } = entry || {}
if (!Array.isArray(namePath) || value === undefined) return
@@ -91,6 +81,21 @@ const applyComputedEntries = (base, entries = []) => {
return result
}
+const getObjectChildrenNames = (modelDefinition) =>
+ (modelDefinition?.properties || [])
+ .filter((property) => property?.type === 'objectChildren' && property?.name)
+ .map((property) => property.name)
+
+const copyObjectChildren = (target, source, modelDefinition) => {
+ if (!target || !source) return target
+ getObjectChildrenNames(modelDefinition).forEach((name) => {
+ if (Object.prototype.hasOwnProperty.call(source, name)) {
+ target[name] = source[name]
+ }
+ })
+ return target
+}
+
const getEditDisabled = (model, objectData, userProfile) => {
if (model?.readOnly === true) {
return true
@@ -261,14 +266,16 @@ const ObjectForm = forwardRef(
// Function to calculate computed values from model properties
const calculateComputedValues = useCallback(
- (currentData, modelDefinition) => {
+ (currentData, modelDefinition, options = {}) => {
if (!modelDefinition || !Array.isArray(modelDefinition.properties)) {
return []
}
+ const skipObjectChildrenValue = options.skipObjectChildrenValue === true
+
// Clone currentData to allow sequential updates
// We use this working copy to calculate subsequent dependent values
- const workingData = merge({}, currentData)
+ const workingData = mergeFormData(currentData)
const normalizedPath = (name, parentPath = []) => {
if (Array.isArray(name)) {
@@ -307,21 +314,25 @@ const ObjectForm = forwardRef(
: getValueAtPath(workingData, parentPath)
if (property.value && typeof property.value === 'function') {
- try {
- const computedValue = property.value(scopeData || {})
- if (computedValue !== undefined) {
- computedEntries.push({
- namePath: propertyPath,
- value: computedValue
- })
- // Update workingData so subsequent properties can use this value
- set(workingData, propertyPath, computedValue)
+ const skipValue =
+ skipObjectChildrenValue && property.type === 'objectChildren'
+ if (!skipValue) {
+ try {
+ const computedValue = property.value(scopeData || {})
+ if (computedValue !== undefined) {
+ computedEntries.push({
+ namePath: propertyPath,
+ value: computedValue
+ })
+ // Update workingData so subsequent properties can use this value
+ set(workingData, propertyPath, computedValue)
+ }
+ } catch (error) {
+ console.warn(
+ `Error calculating value for property ${property.name}:`,
+ error
+ )
}
- } catch (error) {
- console.warn(
- `Error calculating value for property ${property.name}:`,
- error
- )
}
}
@@ -478,62 +489,81 @@ const ObjectForm = forwardRef(
}
}, []) // Empty dependency array - only run on mount/unmount
- const handleFetchObject = useCallback(async () => {
- const objectKey = `${type}:${id}`
+ const handleFetchObject = useCallback(
+ async (options = {}) => {
+ const objectKey = `${type}:${id}`
+ const skipObjectChildrenValue = options.skipObjectChildrenValue === true
- try {
- setFetchLoading(true)
- onStateChangeRef.current({ loading: true })
- const data = await fetchObject(id, type)
- const initialActivities = await fetchObjectActivities(id, type)
+ try {
+ setFetchLoading(true)
+ onStateChangeRef.current({ loading: true })
+ const data = await fetchObject(id, type)
+ const initialActivities = await fetchObjectActivities(id, type)
- if (fetchedObjectRef.current !== objectKey) {
- return
+ if (fetchedObjectRef.current !== objectKey) {
+ return
+ }
+
+ setActivities(initialActivities)
+
+ if (
+ isEditingRef.current &&
+ getBeingEditedByOther(initialActivities, userProfile?._id)
+ ) {
+ releaseEditingState()
+ }
+
+ serverObjectData.current = data
+
+ // Calculate and set computed values on initial load
+ const computedEntries = calculateComputedValues(data, model, {
+ skipObjectChildrenValue
+ })
+ const initialFormData = applyComputedEntries(data, computedEntries)
+ if (skipObjectChildrenValue) {
+ copyObjectChildren(initialFormData, data, model)
+ }
+ setObjectData({
+ ...initialFormData,
+ _isEditing: isEditingRef.current
+ })
+ form.setFieldsValue(initialFormData)
+ setFetchLoading(false)
+ onStateChangeRef.current({ loading: false })
+ } catch (err) {
+ console.error(err)
+ showError(
+ `Failed to fetch ${getModelMessageLabel(type).toLowerCase()} info.`
+ )
}
-
- setActivities(initialActivities)
-
- if (
- isEditingRef.current &&
- getBeingEditedByOther(initialActivities, userProfile?._id)
- ) {
- releaseEditingState()
- }
-
- serverObjectData.current = data
-
- // Calculate and set computed values on initial load
- const computedEntries = calculateComputedValues(data, model)
- const initialFormData = applyComputedEntries(data, computedEntries)
- setObjectData({ ...initialFormData, _isEditing: isEditingRef.current })
- form.setFieldsValue(initialFormData)
- setFetchLoading(false)
- onStateChangeRef.current({ loading: false })
- } catch (err) {
- console.error(err)
- showError(
- `Failed to fetch ${getModelMessageLabel(type).toLowerCase()} info.`
- )
- }
- }, [
- fetchObject,
- fetchObjectActivities,
- id,
- type,
- form,
- showError,
- calculateComputedValues,
- model,
- userProfile?._id,
- releaseEditingState
- ])
+ },
+ [
+ fetchObject,
+ fetchObjectActivities,
+ id,
+ type,
+ form,
+ showError,
+ calculateComputedValues,
+ model,
+ userProfile?._id,
+ releaseEditingState
+ ]
+ )
// Update event handler
- const updateObjectEventHandler = useCallback((value) => {
- setObjectData((prev) =>
- mergeWith({}, prev, value, arrayReplaceCustomizer)
- )
- }, [])
+ const updateObjectEventHandler = useCallback(
+ (value) => {
+ setObjectData((prev) => {
+ const next = mergeFormData(prev, value)
+ if (!isEditingRef.current) {
+ copyObjectChildren(next, value, model)
+ }
+ return next
+ })
+ },
+ [model]
+ )
useEffect(() => {
notifyActivityState(activities)
@@ -728,6 +758,7 @@ const ObjectForm = forwardRef(
const handleUpdate = async () => {
let error
+ let savedSuccessfully = false
const value = await form.validateFields().catch((err) => {
error = err
})
@@ -746,16 +777,29 @@ const ObjectForm = forwardRef(
console.log('THERE IS AN ERROR')
error = updatedObject
} else {
+ savedSuccessfully = true
setIsEditing(false)
isEditingRef.current = false
onStateChangeRef.current({ isEditing: isEditingRef.current })
+ serverObjectData.current = updatedObject
+ const computedEntries = calculateComputedValues(
+ updatedObject,
+ model,
+ {
+ skipObjectChildrenValue: true
+ }
+ )
+ const nextObjectData = applyComputedEntries(
+ updatedObject,
+ computedEntries
+ )
+ copyObjectChildren(nextObjectData, updatedObject, model)
setObjectData({
- ...objectData,
- ...currentFormData,
- ...updatedObject,
- _isEditing: isEditingRef.current
+ ...nextObjectData,
+ _isEditing: false
})
+ form.setFieldsValue(nextObjectData)
setObjectActivity(id, type, 'viewing')
showSuccess(`${getModelMessageLabel(type)} edited successfully!`)
}
@@ -768,7 +812,7 @@ const ObjectForm = forwardRef(
)
}
- handleFetchObject()
+ handleFetchObject({ skipObjectChildrenValue: savedSuccessfully })
setEditLoading(false)
onStateChangeRef.current({ editLoading: false })
}
@@ -799,14 +843,18 @@ const ObjectForm = forwardRef(
onEdit(allFormValues)
}
+ // After save, Form.Items for objectChildren unmount and a merge would
+ // keep the previous child rows instead of the updated object.
+ if (!isEditingRef.current) {
+ return
+ }
+
// Recompute derived fields from the full current form snapshot so
// toggles like overridePrice/overrideCost are preserved while typing.
- const currentFormData = mergeWith(
- {},
+ const currentFormData = mergeFormData(
serverObjectData.current || {},
objectData || {},
- allFormValues,
- arrayReplaceCustomizer
+ allFormValues
)
const computedEntries = calculateComputedValues(
currentFormData,
@@ -837,14 +885,7 @@ const ObjectForm = forwardRef(
mergedFormValues._isEditing = isEditingRef.current
- setObjectData((prev) => {
- return mergeWith(
- {},
- prev,
- mergedFormValues,
- arrayReplaceCustomizer
- )
- })
+ setObjectData((prev) => mergeFormData(prev, mergedFormValues))
}}
>
{children({
diff --git a/src/components/Dashboard/common/ObjectInfo.jsx b/src/components/Dashboard/common/ObjectInfo.jsx
index f80c5216..99558e2e 100644
--- a/src/components/Dashboard/common/ObjectInfo.jsx
+++ b/src/components/Dashboard/common/ObjectInfo.jsx
@@ -1,16 +1,8 @@
import { Spin, Descriptions, Flex } from 'antd'
-import { useState, useEffect } from 'react'
import { LoadingOutlined } from '@ant-design/icons'
import PropTypes from 'prop-types'
import ObjectProperty from './ObjectProperty'
import { getModelProperties } from '../../../database/ObjectModels'
-import mergeWith from 'lodash/mergeWith'
-
-const arrayReplaceCustomizer = (objValue, srcValue) => {
- if (Array.isArray(srcValue)) {
- return srcValue
- }
-}
const ObjectInfo = ({
loading = false,
@@ -39,14 +31,6 @@ const ObjectInfo = ({
}) => {
const allItems = getModelProperties(type)
- const [combinedObjectData, setCombinedObjectData] = useState(objectData)
-
- useEffect(() => {
- setCombinedObjectData((prev) =>
- mergeWith({}, prev, objectData, arrayReplaceCustomizer)
- )
- }, [objectData])
-
// If properties array is empty, show all properties
// Otherwise, filter and order by the properties array
let items
@@ -78,7 +62,7 @@ const ObjectInfo = ({
const propertyName = item.name
// Support property.visible as a function (objectData) => boolean
if (typeof item.visible === 'function') {
- const visible = item.visible(objectData || combinedObjectData || {})
+ const visible = item.visible(objectData || {})
if (!visible) return false
}
if (isWhitelistMode) {
@@ -112,11 +96,11 @@ const ObjectInfo = ({
{...item}
{...objectPropertyProps}
isEditing={isEditing}
- objectData={combinedObjectData}
+ objectData={objectData}
parentData={parentData}
showSince={true}
useFormItem={isControlled ? false : objectPropertyProps.useFormItem}
- value={isControlled ? combinedObjectData?.[item.name] : undefined}
+ value={isControlled ? objectData?.[item.name] : undefined}
modelType={type}
onChange={
isControlled
diff --git a/src/components/Dashboard/common/ObjectList.jsx b/src/components/Dashboard/common/ObjectList.jsx
index 719dac19..4192eb59 100644
--- a/src/components/Dashboard/common/ObjectList.jsx
+++ b/src/components/Dashboard/common/ObjectList.jsx
@@ -22,7 +22,7 @@ const ObjectList = ({
wrap={!scrollHorizontal}
style={{
...style,
- width: 'fit-content'
+ width: '100%'
}}
justify={'start'}
>
@@ -39,11 +39,7 @@ const ObjectList = ({
)
if (scrollHorizontal) {
- return (
-
- {listContents}
-
- )
+ return {listContents}
} else {
return listContents
}
diff --git a/src/components/Dashboard/common/ObjectProperty.jsx b/src/components/Dashboard/common/ObjectProperty.jsx
index 3ef0cb5a..b7a86675 100644
--- a/src/components/Dashboard/common/ObjectProperty.jsx
+++ b/src/components/Dashboard/common/ObjectProperty.jsx
@@ -970,6 +970,7 @@ const ObjectProperty = ({
type={objectType}
multiple
showHyperlink={showHyperlink}
+ masterFilter={masterFilter}
{...inputProps}
/>
)
diff --git a/src/components/Dashboard/common/ObjectSelect.jsx b/src/components/Dashboard/common/ObjectSelect.jsx
index ab433731..396da1d4 100644
--- a/src/components/Dashboard/common/ObjectSelect.jsx
+++ b/src/components/Dashboard/common/ObjectSelect.jsx
@@ -13,7 +13,6 @@ import { ApiServerContext } from '../context/ApiServerContext'
import { AuthContext } from '../context/AuthContext'
import ObjectProperty from './ObjectProperty'
import { getModelByName } from '../../../database/ObjectModels'
-import merge from 'lodash/merge'
import { getModelProperty } from '../../../database/ObjectModels'
const { SHOW_CHILD } = TreeSelect
@@ -187,8 +186,7 @@ const ObjectSelect = ({
if (Array.isArray(data)) {
setObjectPropertiesTree((prev) => mergeGroups(prev, data))
} else {
- // Fallback if API returns something unexpected
- setObjectPropertiesTree((prev) => merge([], prev, data))
+ setObjectPropertiesTree(data)
}
setInitialLoading(false)
@@ -382,10 +380,18 @@ const ObjectSelect = ({
setTreeSelectValue(value)
onChange?.(selectedObjects)
} else {
- // Single selection
- const selectedObject = objectList.find((obj) => obj._id === value)
+ // Single selection: replace the previous object instead of emitting
+ // undefined (lodash merge skips undefined and would keep the old value).
+ if (value == null || value === '') {
+ setTreeSelectValue(null)
+ onChange?.(null)
+ return
+ }
+ const selectedObject = objectList.find((obj) =>
+ areValuesEqual(obj._id, value)
+ )
setTreeSelectValue(value)
- onChange?.(selectedObject)
+ onChange?.(selectedObject ?? null)
}
},
[multiple, objectList, onChange]
diff --git a/src/components/Dashboard/common/StateTag.jsx b/src/components/Dashboard/common/StateTag.jsx
index a812f959..156b3b94 100644
--- a/src/components/Dashboard/common/StateTag.jsx
+++ b/src/components/Dashboard/common/StateTag.jsx
@@ -116,6 +116,10 @@ const StateTag = ({ state, showBadge = true, showTag = true, style = {} }) => {
status = 'warning'
text = 'Used'
break
+ case 'consumed':
+ status = 'default'
+ text = 'Consumed'
+ break
case 'unconsumed':
status = 'success'
text = 'Unconsumed'
diff --git a/src/components/Dashboard/utils/Utils.js b/src/components/Dashboard/utils/Utils.js
index 36ec8646..3d1ba514 100644
--- a/src/components/Dashboard/utils/Utils.js
+++ b/src/components/Dashboard/utils/Utils.js
@@ -1,3 +1,5 @@
+import mergeWith from 'lodash/mergeWith'
+
export function capitalizeFirstLetter(string) {
try {
return string[0].toUpperCase() + string.slice(1)
@@ -34,5 +36,38 @@ export function round(num, decimals) {
return Math.round(num * 10 ** decimals) / 10 ** decimals
}
-// Re-export the functions for backward compatibility
-export {}
+const isIdRecord = (value) =>
+ value != null &&
+ typeof value === 'object' &&
+ !Array.isArray(value) &&
+ value._id != null
+
+// lodash merge combines arrays by index and deep-merges objects with an _id.
+// Form state must replace those wholesale so a newly selected object (e.g.
+// productSku) does not keep nested arrays like parts from the previous one.
+export function mergeFormCustomizer(objValue, srcValue, key) {
+ if (srcValue === null) {
+ return null
+ }
+ if (Array.isArray(srcValue)) {
+ return srcValue
+ }
+ if (
+ isIdRecord(objValue) &&
+ isIdRecord(srcValue) &&
+ String(objValue._id) !== String(srcValue._id)
+ ) {
+ return srcValue
+ }
+ if (key === 'permissions' && srcValue !== undefined) {
+ return srcValue
+ }
+}
+
+export function mergeFormData(...sources) {
+ return mergeWith(
+ {},
+ ...sources.filter((source) => source != null),
+ mergeFormCustomizer
+ )
+}
diff --git a/src/database/models/Listing.js b/src/database/models/Listing.js
index 4a0dc5e6..82715c2b 100644
--- a/src/database/models/Listing.js
+++ b/src/database/models/Listing.js
@@ -40,7 +40,11 @@ export const Listing = {
label: 'New Listing',
icon: PlusIcon,
content: (objectData, { onOk } = {}) => {
- return createElement(NewListing, { defaultValues: objectData, onOk, reset: true })
+ return createElement(NewListing, {
+ defaultValues: objectData,
+ onOk,
+ reset: true
+ })
}
},
{
@@ -188,13 +192,7 @@ export const Listing = {
'updatedAt',
'_id'
],
- group: [
- 'marketplace',
- 'product',
- 'vendor',
- 'stockLocation',
- 'courierServices'
- ],
+ group: ['marketplace', 'vendor', 'product', 'stockLocation'],
properties: [
{
name: '_id',
@@ -304,7 +302,7 @@ export const Listing = {
min: 0,
readOnly: true,
required: false,
- columnWidth: 150
+ columnWidth: 170
},
{
name: 'marketplace',
@@ -329,7 +327,8 @@ export const Listing = {
label: 'Condition',
type: 'select',
required: true,
- extra: 'Required by eBay for most categories. Stored in camel case and converted when publishing.',
+ extra:
+ 'Required by eBay for most categories. Stored in camel case and converted when publishing.',
options: [
{ value: 'new', label: 'New' },
{ value: 'likeNew', label: 'Like New' },
diff --git a/src/database/models/ListingVarient.js b/src/database/models/ListingVarient.js
index 11f3cae7..0666805a 100644
--- a/src/database/models/ListingVarient.js
+++ b/src/database/models/ListingVarient.js
@@ -1,16 +1,20 @@
import { createElement, lazy } from 'react'
const ListingVarientInfo = lazy(
- () => import('../../components/Dashboard/Sales/ListingVarients/ListingVarientInfo')
+ () =>
+ import('../../components/Dashboard/Sales/ListingVarients/ListingVarientInfo')
)
const NewListingVarient = lazy(
- () => import('../../components/Dashboard/Sales/ListingVarients/NewListingVarient')
+ () =>
+ import('../../components/Dashboard/Sales/ListingVarients/NewListingVarient')
)
const PublishListingVarient = lazy(
- () => import('../../components/Dashboard/Sales/ListingVarients/PublishListingVarient')
+ () =>
+ import('../../components/Dashboard/Sales/ListingVarients/PublishListingVarient')
)
const UnpublishListingVarient = lazy(
- () => import('../../components/Dashboard/Sales/ListingVarients/UnpublishListingVarient')
+ () =>
+ import('../../components/Dashboard/Sales/ListingVarients/UnpublishListingVarient')
)
const DeleteObject = lazy(
() => import('../../components/Dashboard/common/DeleteObject')
@@ -40,7 +44,11 @@ export const ListingVarient = {
label: 'New Listing Varient',
icon: PlusIcon,
content: (objectData, { onOk } = {}) => {
- return createElement(NewListingVarient, { defaultValues: objectData, onOk, reset: true })
+ return createElement(NewListingVarient, {
+ defaultValues: objectData,
+ onOk,
+ reset: true
+ })
}
},
{
@@ -162,7 +170,15 @@ export const ListingVarient = {
'updatedAt',
'_reference'
],
- sorters: ['state', 'stockQuantity', 'price', 'lastSyncedAt', 'createdAt', 'updatedAt', '_id'],
+ sorters: [
+ 'state',
+ 'stockQuantity',
+ 'price',
+ 'lastSyncedAt',
+ 'createdAt',
+ 'updatedAt',
+ '_id'
+ ],
group: ['listing', 'state'],
properties: [
{
@@ -241,7 +257,7 @@ export const ListingVarient = {
min: 0,
readOnly: true,
required: false,
- columnWidth: 150
+ columnWidth: 170
},
{
name: 'state',
diff --git a/src/database/models/PartStock.js b/src/database/models/PartStock.js
index 8d6e304d..0f7d8b2e 100644
--- a/src/database/models/PartStock.js
+++ b/src/database/models/PartStock.js
@@ -28,7 +28,11 @@ export const PartStock = {
label: 'New Part Stock',
icon: PlusIcon,
content: (objectData, { onOk } = {}) => {
- return createElement(NewPartStock, { defaultValues: objectData, onOk, reset: true })
+ return createElement(NewPartStock, {
+ defaultValues: objectData,
+ onOk,
+ reset: true
+ })
}
},
{
@@ -55,6 +59,7 @@ export const PartStock = {
}
],
filters: [
+ 'part',
'partSku',
'state',
'startingQuantity',
@@ -65,6 +70,7 @@ export const PartStock = {
'_reference'
],
sorters: [
+ 'part',
'partSku',
'startingQuantity',
'currentQuantity',
@@ -77,6 +83,7 @@ export const PartStock = {
'state',
'startingQuantity',
'currentQuantity',
+ 'part',
'partSku',
'stockLocation',
'createdAt',
@@ -132,6 +139,15 @@ export const PartStock = {
required: true,
masterFilter: ['subJob', 'stockTransfer']
},
+ {
+ name: 'part',
+ label: 'Part',
+ type: 'object',
+ objectType: 'part',
+ required: true,
+ showHyperlink: true,
+ columnWidth: 200
+ },
{
name: 'partSku',
label: 'Part SKU',
@@ -139,7 +155,10 @@ export const PartStock = {
objectType: 'partSku',
required: true,
showHyperlink: true,
- columnWidth: 200
+ columnWidth: 200,
+ masterFilter: (objectData) => {
+ return { part: objectData?.part?._id }
+ }
},
{
name: 'stockLocation',
diff --git a/src/database/models/ProductStock.js b/src/database/models/ProductStock.js
index eba24fcc..19dfa9dc 100644
--- a/src/database/models/ProductStock.js
+++ b/src/database/models/ProductStock.js
@@ -1,20 +1,22 @@
import { createElement, lazy } from 'react'
const ProductStockInfo = lazy(
- () => import('../../components/Dashboard/Inventory/ProductStocks/ProductStockInfo')
+ () =>
+ import('../../components/Dashboard/Inventory/ProductStocks/ProductStockInfo')
)
const NewProductStock = lazy(
- () => import('../../components/Dashboard/Inventory/ProductStocks/NewProductStock')
+ () =>
+ import('../../components/Dashboard/Inventory/ProductStocks/NewProductStock')
)
const PostProductStock = lazy(
- () => import('../../components/Dashboard/Inventory/ProductStocks/PostProductStock')
+ () =>
+ import('../../components/Dashboard/Inventory/ProductStocks/PostProductStock')
)
const DeleteObject = lazy(
() => import('../../components/Dashboard/common/DeleteObject')
)
import ProductStockIcon from '../../components/Icons/ProductStockIcon'
import PlusIcon from '../../components/Icons/PlusIcon'
-import { getModelByName } from '../ObjectModels.js'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import EditIcon from '../../components/Icons/EditIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
@@ -38,7 +40,11 @@ export const ProductStock = {
label: 'New Product Stock',
icon: PlusIcon,
content: (objectData, { onOk } = {}) => {
- return createElement(NewProductStock, { defaultValues: objectData, onOk, reset: true })
+ return createElement(NewProductStock, {
+ defaultValues: objectData,
+ onOk,
+ reset: true
+ })
}
},
{
@@ -116,6 +122,12 @@ export const ProductStock = {
modalWidth: 520,
label: 'Post',
icon: CheckIcon,
+ disabled: (objectData) => {
+ if (objectData?._isEditing == true) return true
+ return objectData?.partStockList?.some(
+ (row) => row?.remainingQuantity > 0
+ )
+ },
visible: (objectData) => {
return objectData?.state?.type == 'draft'
},
@@ -131,6 +143,7 @@ export const ProductStock = {
}
],
filters: [
+ 'product',
'productSku',
'state',
'currentQuantity',
@@ -140,16 +153,19 @@ export const ProductStock = {
'_reference'
],
sorters: [
+ 'product',
'productSku',
'currentQuantity',
'state',
'createdAt',
- 'updatedAt'
+ 'updatedAt',
+ 'stockLocation'
],
columns: [
'_reference',
'state',
'currentQuantity',
+ 'product',
'productSku',
'stockLocation',
'createdAt',
@@ -182,6 +198,13 @@ export const ProductStock = {
readOnly: true,
columnWidth: 180
},
+ {
+ name: 'updatedAt',
+ label: 'Updated At',
+ type: 'dateTime',
+ readOnly: true,
+ columnWidth: 175
+ },
{
name: 'state',
label: 'State',
@@ -196,12 +219,15 @@ export const ProductStock = {
readOnly: true,
columnWidth: 175
},
+
{
- name: 'updatedAt',
- label: 'Updated At',
- type: 'dateTime',
- readOnly: true,
- columnWidth: 175
+ name: 'product',
+ label: 'Product',
+ type: 'object',
+ objectType: 'product',
+ required: true,
+ showHyperlink: true,
+ columnWidth: 200
},
{
name: 'productSku',
@@ -210,14 +236,17 @@ export const ProductStock = {
objectType: 'productSku',
required: true,
showHyperlink: true,
- columnWidth: 200
+ columnWidth: 200,
+ masterFilter: (objectData) => {
+ return { product: objectData?.product?._id }
+ }
},
{
name: 'stockLocation',
label: 'Stock location',
type: 'object',
objectType: 'stockLocation',
- required: false,
+ required: true,
showHyperlink: true,
columnWidth: 200,
readOnly: (objectData) => {
@@ -232,11 +261,99 @@ export const ProductStock = {
required: true
},
{
- name: 'partStocks',
- label: 'Part Stocks',
+ name: 'partStockList',
+ label: 'Part Stock List',
type: 'objectChildren',
canAddRemove: false,
+ size: 'medium',
+ value: (objectData) => {
+ if (
+ objectData?._isEditing == false ||
+ objectData?.state?.type != 'draft'
+ ) {
+ return objectData?.partStockList || []
+ }
+ const getId = (val) => {
+ if (val == null) return null
+ if (typeof val === 'object') {
+ return val._id != null ? String(val._id) : null
+ }
+ return String(val)
+ }
+
+ const toPartStocks = (row) => {
+ if (Array.isArray(row?.partStocks)) return row.partStocks
+ if (row?.partStock) return [row.partStock]
+ return []
+ }
+
+ const skuParts = objectData?.productSku?.parts
+ const existing = objectData?.partStockList
+ const currentQuantity = Number(objectData?.currentQuantity) || 0
+ const requiredFromPart = (part) =>
+ (Number(part?.quantity) || 0) * currentQuantity
+
+ const buildRow = (part, existingRow) => ({
+ ...existingRow,
+ part: part?.part?._id ? part.part : { _id: part.part },
+ partSku: part.partSku?._id ? part.partSku : { _id: part.partSku },
+ requiredQuantity: requiredFromPart(part),
+ partStocks: toPartStocks(existingRow)
+ })
+
+ const rowsMatchSkuParts = (rows, parts) => {
+ if (!Array.isArray(rows) || rows.length !== parts.length) return false
+ return parts.every((part, index) => {
+ const row = rows[index]
+ return (
+ getId(row?.partSku) === getId(part.partSku) &&
+ getId(row?.part) === getId(part.part) &&
+ Number(row?.requiredQuantity) === requiredFromPart(part) &&
+ Array.isArray(row?.partStocks)
+ )
+ })
+ }
+
+ if (Array.isArray(skuParts)) {
+ if (skuParts.length === 0) return []
+ if (rowsMatchSkuParts(existing, skuParts)) return undefined
+ return skuParts.map((part) => {
+ const existingRow = Array.isArray(existing)
+ ? existing.find(
+ (row) => getId(row.partSku) === getId(part.partSku)
+ )
+ : undefined
+ return buildRow(part, existingRow)
+ })
+ }
+
+ if (!Array.isArray(existing)) return []
+
+ const needsNormalize = existing.some(
+ (row) =>
+ (row.requiredQuantity == null && row.quantity != null) ||
+ (row.part == null && row.partSku?.part != null) ||
+ !Array.isArray(row.partStocks)
+ )
+ if (!needsNormalize) return []
+
+ return existing.map((row) => ({
+ ...row,
+ part: row.part ?? row.partSku?.part,
+ requiredQuantity: row.requiredQuantity ?? row.quantity,
+ partStocks: toPartStocks(row)
+ }))
+ },
properties: [
+ {
+ name: 'part',
+ label: 'Part',
+ type: 'object',
+ objectType: 'part',
+ readOnly: true,
+ required: true,
+ showHyperlink: true
+ },
{
name: 'partSku',
label: 'Part SKU',
@@ -247,25 +364,52 @@ export const ProductStock = {
showHyperlink: true
},
{
- name: 'partStock',
- label: 'Part Stock',
- type: 'object',
+ name: 'partStocks',
+ label: 'Part Stocks',
+ type: 'objectList',
objectType: 'partStock',
- required: true,
+ required: false,
showHyperlink: true,
+ columnWidth: 260,
masterFilter: (objectData) => {
- const partSkuId = objectData?.partSku?._id
- if (partSkuId == null) return {}
return {
- partSku: getModelByName('partSku').prefix + ':' + partSkuId
+ part: objectData?.part?._id,
+ partSku: objectData?.partSku?._id,
+ $or: [{ 'state.type': 'new' }, { 'state.type': 'used' }]
}
}
},
{
- name: 'quantity',
- label: 'Quantity',
+ name: 'requiredQuantity',
+ label: 'Required Quantity',
type: 'number',
- required: true
+ required: true,
+ columnWidth: 190,
+ readOnly: true,
+ value: (objectData) =>
+ objectData?.requiredQuantity ?? objectData?.quantity
+ },
+ {
+ name: 'remainingQuantity',
+ label: 'Remaining Quantity',
+ type: 'number',
+ columnWidth: 190,
+ readOnly: true,
+ value: (objectData) => {
+ const required =
+ objectData?.requiredQuantity ?? objectData?.quantity ?? 0
+ const stocks = Array.isArray(objectData?.partStocks)
+ ? objectData.partStocks
+ : []
+ const available = Math.max(
+ 0,
+ stocks.reduce(
+ (sum, stock) => sum + (Number(stock?.currentQuantity) || 0),
+ 0
+ )
+ )
+ return Math.max(0, required - available)
+ }
}
]
}
@@ -278,8 +422,8 @@ export const ProductStock = {
color: 'default'
},
{
- name: 'posted.count',
- label: 'Posted',
+ name: 'new.count',
+ label: 'New',
type: 'number',
color: 'success'
},