Refactor Product Stock Components and Update Property Names
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Renamed 'partStocks' to 'partStockList' in NewProductStock and ProductStockInfo components for consistency and clarity. - Updated visible properties and object data handling to reflect the new naming convention. - Refactored utility functions in NewObjectForm and ObjectForm to utilize a new mergeFormData function for improved data management. - Enhanced ObjectInfo and ObjectProperty components by removing unnecessary state management and ensuring proper data flow. - Adjusted layout properties in ObjectList and ObjectSelect components for better responsiveness and usability.
This commit is contained in:
parent
c4beb3d3a0
commit
e0a5ff3cf8
@ -24,7 +24,7 @@ const NewProductStock = ({ onOk, reset, defaultValues }) => {
|
|||||||
required={true}
|
required={true}
|
||||||
objectData={objectData}
|
objectData={objectData}
|
||||||
visibleProperties={{
|
visibleProperties={{
|
||||||
partStocks: false
|
partStockList: false
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@ -42,7 +42,7 @@ const NewProductStock = ({ onOk, reset, defaultValues }) => {
|
|||||||
_reference: false,
|
_reference: false,
|
||||||
createdAt: false,
|
createdAt: false,
|
||||||
updatedAt: false,
|
updatedAt: false,
|
||||||
partStocks: false
|
partStockList: false
|
||||||
}}
|
}}
|
||||||
isEditing={false}
|
isEditing={false}
|
||||||
objectData={objectData}
|
objectData={objectData}
|
||||||
|
|||||||
@ -10,7 +10,10 @@ import InfoCollapse from '../../common/InfoCollapse.jsx'
|
|||||||
import ObjectInfo from '../../common/ObjectInfo.jsx'
|
import ObjectInfo from '../../common/ObjectInfo.jsx'
|
||||||
import ObjectProperty from '../../common/ObjectProperty.jsx'
|
import ObjectProperty from '../../common/ObjectProperty.jsx'
|
||||||
import ViewButton from '../../common/ViewButton.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 InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
||||||
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
||||||
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||||
@ -54,7 +57,7 @@ const ProductStockInfo = () => {
|
|||||||
'ProductStockInfo',
|
'ProductStockInfo',
|
||||||
{
|
{
|
||||||
info: true,
|
info: true,
|
||||||
partStocks: true,
|
partStockList: true,
|
||||||
events: true,
|
events: true,
|
||||||
history: true,
|
history: true,
|
||||||
notes: true,
|
notes: true,
|
||||||
@ -97,7 +100,7 @@ const ProductStockInfo = () => {
|
|||||||
finishEdit: () => {
|
finishEdit: () => {
|
||||||
objectFormRef?.current.handleUpdate()
|
objectFormRef?.current.handleUpdate()
|
||||||
return true
|
return true
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -123,7 +126,7 @@ const ProductStockInfo = () => {
|
|||||||
disabled={objectFormState.loading}
|
disabled={objectFormState.loading}
|
||||||
items={[
|
items={[
|
||||||
{ key: 'info', label: 'Product Stock Information' },
|
{ key: 'info', label: 'Product Stock Information' },
|
||||||
{ key: 'partStocks', label: 'Part Stocks' },
|
{ key: 'partStockList', label: 'Part Stock List' },
|
||||||
{ key: 'events', label: 'Product Stock Events' },
|
{ key: 'events', label: 'Product Stock Events' },
|
||||||
{ key: 'history', label: 'Product Stock History' },
|
{ key: 'history', label: 'Product Stock History' },
|
||||||
{ key: 'notes', label: 'Notes' },
|
{ key: 'notes', label: 'Notes' },
|
||||||
@ -207,20 +210,20 @@ const ProductStockInfo = () => {
|
|||||||
type='productStock'
|
type='productStock'
|
||||||
objectData={objectData}
|
objectData={objectData}
|
||||||
labelWidth='175px'
|
labelWidth='175px'
|
||||||
visibleProperties={{ partStocks: false }}
|
visibleProperties={{ partStockList: false }}
|
||||||
/>
|
/>
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
<InfoCollapse
|
<InfoCollapse
|
||||||
title='Part Stocks'
|
title='Part Stock List'
|
||||||
icon={<PartStockIcon />}
|
icon={<PartStockIcon />}
|
||||||
active={collapseState.partStocks}
|
active={collapseState.partStockList}
|
||||||
onToggle={(expanded) =>
|
onToggle={(expanded) =>
|
||||||
updateCollapseState('partStocks', expanded)
|
updateCollapseState('partStockList', expanded)
|
||||||
}
|
}
|
||||||
collapseKey='partStocks'
|
collapseKey='partStockList'
|
||||||
>
|
>
|
||||||
<ObjectProperty
|
<ObjectProperty
|
||||||
{...getModelProperty('productStock', 'partStocks')}
|
{...getModelProperty('productStock', 'partStockList')}
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
objectData={objectData}
|
objectData={objectData}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
@ -244,7 +247,10 @@ const ProductStockInfo = () => {
|
|||||||
) : (
|
) : (
|
||||||
<ObjectTable
|
<ObjectTable
|
||||||
type='stockEvent'
|
type='stockEvent'
|
||||||
masterFilter={{ parent: getModelByName('productStock').prefix + ':' + productStockId }}
|
masterFilter={{
|
||||||
|
parent:
|
||||||
|
getModelByName('productStock').prefix + ':' + productStockId
|
||||||
|
}}
|
||||||
visibleColumns={{ parent: false }}
|
visibleColumns={{ parent: false }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -281,7 +287,7 @@ const ProductStockInfo = () => {
|
|||||||
indicator={<LoadingOutlined />}
|
indicator={<LoadingOutlined />}
|
||||||
>
|
>
|
||||||
<Card>
|
<Card>
|
||||||
<NotesPanel _id={productStockId} type='productStock' />
|
<NotesPanel _id={productStockId} type='productStock' />
|
||||||
</Card>
|
</Card>
|
||||||
</Spin>
|
</Spin>
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
@ -299,10 +305,8 @@ const ProductStockInfo = () => {
|
|||||||
<ObjectTable
|
<ObjectTable
|
||||||
type='auditLog'
|
type='auditLog'
|
||||||
masterFilter={{
|
masterFilter={{
|
||||||
parent:
|
parent:
|
||||||
getModelByName('productStock').prefix +
|
getModelByName('productStock').prefix + ':' + productStockId
|
||||||
':' +
|
|
||||||
productStockId
|
|
||||||
}}
|
}}
|
||||||
visibleColumns={{ _id: false, parent: false }}
|
visibleColumns={{ _id: false, parent: false }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -3,10 +3,9 @@ import { Form } from 'antd'
|
|||||||
import { ApiServerContext } from '../context/ApiServerContext'
|
import { ApiServerContext } from '../context/ApiServerContext'
|
||||||
import { useMessageContext } from '../context/MessageContext'
|
import { useMessageContext } from '../context/MessageContext'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import merge from 'lodash/merge'
|
|
||||||
import mergeWith from 'lodash/mergeWith'
|
|
||||||
import set from 'lodash/set'
|
import set from 'lodash/set'
|
||||||
import { getModelByName } from '../../../database/ObjectModels'
|
import { getModelByName } from '../../../database/ObjectModels'
|
||||||
|
import { mergeFormData } from '../utils/Utils'
|
||||||
|
|
||||||
const buildObjectFromEntries = (entries = []) => {
|
const buildObjectFromEntries = (entries = []) => {
|
||||||
return entries.reduce((acc, entry) => {
|
return entries.reduce((acc, entry) => {
|
||||||
@ -20,7 +19,7 @@ const buildObjectFromEntries = (entries = []) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const applyComputedEntries = (base, entries = []) => {
|
const applyComputedEntries = (base, entries = []) => {
|
||||||
const result = merge({}, base || {})
|
const result = mergeFormData(base || {})
|
||||||
entries.forEach((entry) => {
|
entries.forEach((entry) => {
|
||||||
const { namePath, value } = entry || {}
|
const { namePath, value } = entry || {}
|
||||||
if (!Array.isArray(namePath) || value === undefined) return
|
if (!Array.isArray(namePath) || value === undefined) return
|
||||||
@ -29,15 +28,6 @@ const applyComputedEntries = (base, entries = []) => {
|
|||||||
return result
|
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.
|
* NewObjectForm is a reusable form component for creating new objects.
|
||||||
* It handles form validation, submission, and error handling logic.
|
* It handles form validation, submission, and error handling logic.
|
||||||
@ -188,9 +178,7 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
|
|||||||
computedEntries
|
computedEntries
|
||||||
)
|
)
|
||||||
form.setFieldsValue(initialFormData)
|
form.setFieldsValue(initialFormData)
|
||||||
setObjectData((prev) =>
|
setObjectData((prev) => mergeFormData(prev, initialFormData))
|
||||||
mergeWith({}, prev, initialFormData, arrayReplaceCustomizer)
|
|
||||||
)
|
|
||||||
return validateForm()
|
return validateForm()
|
||||||
}
|
}
|
||||||
}, [form, defaultValues, calculateComputedValues, model, validateForm])
|
}, [form, defaultValues, calculateComputedValues, model, validateForm])
|
||||||
@ -233,12 +221,7 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
|
|||||||
style={style}
|
style={style}
|
||||||
onValuesChange={(_changedValues, allFormValues) => {
|
onValuesChange={(_changedValues, allFormValues) => {
|
||||||
// Calculate computed values based on current form data
|
// Calculate computed values based on current form data
|
||||||
const currentFormData = mergeWith(
|
const currentFormData = mergeFormData(objectData || {}, allFormValues)
|
||||||
{},
|
|
||||||
objectData || {},
|
|
||||||
allFormValues,
|
|
||||||
arrayReplaceCustomizer
|
|
||||||
)
|
|
||||||
const computedEntries = calculateComputedValues(currentFormData, model)
|
const computedEntries = calculateComputedValues(currentFormData, model)
|
||||||
|
|
||||||
if (Array.isArray(computedEntries) && computedEntries.length > 0) {
|
if (Array.isArray(computedEntries) && computedEntries.length > 0) {
|
||||||
@ -259,9 +242,7 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const allValues = applyComputedEntries(allFormValues, computedEntries)
|
const allValues = applyComputedEntries(allFormValues, computedEntries)
|
||||||
setObjectData((prev) => {
|
setObjectData((prev) => mergeFormData(prev, allValues))
|
||||||
return mergeWith({}, prev, allValues, arrayReplaceCustomizer)
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children({
|
{children({
|
||||||
|
|||||||
@ -11,22 +11,12 @@ import { Form } from 'antd'
|
|||||||
import { ApiServerContext } from '../context/ApiServerContext'
|
import { ApiServerContext } from '../context/ApiServerContext'
|
||||||
import { AuthContext } from '../context/AuthContext'
|
import { AuthContext } from '../context/AuthContext'
|
||||||
import { useMessageContext } from '../context/MessageContext'
|
import { useMessageContext } from '../context/MessageContext'
|
||||||
import merge from 'lodash/merge'
|
|
||||||
import mergeWith from 'lodash/mergeWith'
|
|
||||||
import set from 'lodash/set'
|
import set from 'lodash/set'
|
||||||
import { getModelByName } from '../../../database/ObjectModels'
|
import { getModelByName } from '../../../database/ObjectModels'
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { useActions } from '../context/ActionsContext'
|
import { useActions } from '../context/ActionsContext'
|
||||||
|
import { mergeFormData } from '../utils/Utils'
|
||||||
const arrayReplaceCustomizer = (objValue, srcValue, key) => {
|
|
||||||
if (Array.isArray(srcValue)) {
|
|
||||||
return srcValue
|
|
||||||
}
|
|
||||||
if (key === 'permissions' && srcValue !== undefined) {
|
|
||||||
return srcValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getUserId = (user) => user?._id || user
|
const getUserId = (user) => user?._id || user
|
||||||
|
|
||||||
@ -82,7 +72,7 @@ const buildObjectFromEntries = (entries = []) => {
|
|||||||
// merge-replacing arrays would wipe stored child-row fields (shipment, amount,
|
// merge-replacing arrays would wipe stored child-row fields (shipment, amount,
|
||||||
// etc.) while leaving calculated columns intact.
|
// etc.) while leaving calculated columns intact.
|
||||||
const applyComputedEntries = (base, entries = []) => {
|
const applyComputedEntries = (base, entries = []) => {
|
||||||
const result = merge({}, base || {})
|
const result = mergeFormData(base || {})
|
||||||
entries.forEach((entry) => {
|
entries.forEach((entry) => {
|
||||||
const { namePath, value } = entry || {}
|
const { namePath, value } = entry || {}
|
||||||
if (!Array.isArray(namePath) || value === undefined) return
|
if (!Array.isArray(namePath) || value === undefined) return
|
||||||
@ -91,6 +81,21 @@ const applyComputedEntries = (base, entries = []) => {
|
|||||||
return result
|
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) => {
|
const getEditDisabled = (model, objectData, userProfile) => {
|
||||||
if (model?.readOnly === true) {
|
if (model?.readOnly === true) {
|
||||||
return true
|
return true
|
||||||
@ -261,14 +266,16 @@ const ObjectForm = forwardRef(
|
|||||||
|
|
||||||
// Function to calculate computed values from model properties
|
// Function to calculate computed values from model properties
|
||||||
const calculateComputedValues = useCallback(
|
const calculateComputedValues = useCallback(
|
||||||
(currentData, modelDefinition) => {
|
(currentData, modelDefinition, options = {}) => {
|
||||||
if (!modelDefinition || !Array.isArray(modelDefinition.properties)) {
|
if (!modelDefinition || !Array.isArray(modelDefinition.properties)) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const skipObjectChildrenValue = options.skipObjectChildrenValue === true
|
||||||
|
|
||||||
// Clone currentData to allow sequential updates
|
// Clone currentData to allow sequential updates
|
||||||
// We use this working copy to calculate subsequent dependent values
|
// We use this working copy to calculate subsequent dependent values
|
||||||
const workingData = merge({}, currentData)
|
const workingData = mergeFormData(currentData)
|
||||||
|
|
||||||
const normalizedPath = (name, parentPath = []) => {
|
const normalizedPath = (name, parentPath = []) => {
|
||||||
if (Array.isArray(name)) {
|
if (Array.isArray(name)) {
|
||||||
@ -307,21 +314,25 @@ const ObjectForm = forwardRef(
|
|||||||
: getValueAtPath(workingData, parentPath)
|
: getValueAtPath(workingData, parentPath)
|
||||||
|
|
||||||
if (property.value && typeof property.value === 'function') {
|
if (property.value && typeof property.value === 'function') {
|
||||||
try {
|
const skipValue =
|
||||||
const computedValue = property.value(scopeData || {})
|
skipObjectChildrenValue && property.type === 'objectChildren'
|
||||||
if (computedValue !== undefined) {
|
if (!skipValue) {
|
||||||
computedEntries.push({
|
try {
|
||||||
namePath: propertyPath,
|
const computedValue = property.value(scopeData || {})
|
||||||
value: computedValue
|
if (computedValue !== undefined) {
|
||||||
})
|
computedEntries.push({
|
||||||
// Update workingData so subsequent properties can use this value
|
namePath: propertyPath,
|
||||||
set(workingData, propertyPath, computedValue)
|
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
|
}, []) // Empty dependency array - only run on mount/unmount
|
||||||
|
|
||||||
const handleFetchObject = useCallback(async () => {
|
const handleFetchObject = useCallback(
|
||||||
const objectKey = `${type}:${id}`
|
async (options = {}) => {
|
||||||
|
const objectKey = `${type}:${id}`
|
||||||
|
const skipObjectChildrenValue = options.skipObjectChildrenValue === true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setFetchLoading(true)
|
setFetchLoading(true)
|
||||||
onStateChangeRef.current({ loading: true })
|
onStateChangeRef.current({ loading: true })
|
||||||
const data = await fetchObject(id, type)
|
const data = await fetchObject(id, type)
|
||||||
const initialActivities = await fetchObjectActivities(id, type)
|
const initialActivities = await fetchObjectActivities(id, type)
|
||||||
|
|
||||||
if (fetchedObjectRef.current !== objectKey) {
|
if (fetchedObjectRef.current !== objectKey) {
|
||||||
return
|
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)
|
[
|
||||||
|
fetchObject,
|
||||||
if (
|
fetchObjectActivities,
|
||||||
isEditingRef.current &&
|
id,
|
||||||
getBeingEditedByOther(initialActivities, userProfile?._id)
|
type,
|
||||||
) {
|
form,
|
||||||
releaseEditingState()
|
showError,
|
||||||
}
|
calculateComputedValues,
|
||||||
|
model,
|
||||||
serverObjectData.current = data
|
userProfile?._id,
|
||||||
|
releaseEditingState
|
||||||
// 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
|
|
||||||
])
|
|
||||||
|
|
||||||
// Update event handler
|
// Update event handler
|
||||||
const updateObjectEventHandler = useCallback((value) => {
|
const updateObjectEventHandler = useCallback(
|
||||||
setObjectData((prev) =>
|
(value) => {
|
||||||
mergeWith({}, prev, value, arrayReplaceCustomizer)
|
setObjectData((prev) => {
|
||||||
)
|
const next = mergeFormData(prev, value)
|
||||||
}, [])
|
if (!isEditingRef.current) {
|
||||||
|
copyObjectChildren(next, value, model)
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[model]
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
notifyActivityState(activities)
|
notifyActivityState(activities)
|
||||||
@ -728,6 +758,7 @@ const ObjectForm = forwardRef(
|
|||||||
|
|
||||||
const handleUpdate = async () => {
|
const handleUpdate = async () => {
|
||||||
let error
|
let error
|
||||||
|
let savedSuccessfully = false
|
||||||
const value = await form.validateFields().catch((err) => {
|
const value = await form.validateFields().catch((err) => {
|
||||||
error = err
|
error = err
|
||||||
})
|
})
|
||||||
@ -746,16 +777,29 @@ const ObjectForm = forwardRef(
|
|||||||
console.log('THERE IS AN ERROR')
|
console.log('THERE IS AN ERROR')
|
||||||
error = updatedObject
|
error = updatedObject
|
||||||
} else {
|
} else {
|
||||||
|
savedSuccessfully = true
|
||||||
setIsEditing(false)
|
setIsEditing(false)
|
||||||
isEditingRef.current = false
|
isEditingRef.current = false
|
||||||
onStateChangeRef.current({ isEditing: isEditingRef.current })
|
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({
|
setObjectData({
|
||||||
...objectData,
|
...nextObjectData,
|
||||||
...currentFormData,
|
_isEditing: false
|
||||||
...updatedObject,
|
|
||||||
_isEditing: isEditingRef.current
|
|
||||||
})
|
})
|
||||||
|
form.setFieldsValue(nextObjectData)
|
||||||
setObjectActivity(id, type, 'viewing')
|
setObjectActivity(id, type, 'viewing')
|
||||||
showSuccess(`${getModelMessageLabel(type)} edited successfully!`)
|
showSuccess(`${getModelMessageLabel(type)} edited successfully!`)
|
||||||
}
|
}
|
||||||
@ -768,7 +812,7 @@ const ObjectForm = forwardRef(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFetchObject()
|
handleFetchObject({ skipObjectChildrenValue: savedSuccessfully })
|
||||||
setEditLoading(false)
|
setEditLoading(false)
|
||||||
onStateChangeRef.current({ editLoading: false })
|
onStateChangeRef.current({ editLoading: false })
|
||||||
}
|
}
|
||||||
@ -799,14 +843,18 @@ const ObjectForm = forwardRef(
|
|||||||
onEdit(allFormValues)
|
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
|
// Recompute derived fields from the full current form snapshot so
|
||||||
// toggles like overridePrice/overrideCost are preserved while typing.
|
// toggles like overridePrice/overrideCost are preserved while typing.
|
||||||
const currentFormData = mergeWith(
|
const currentFormData = mergeFormData(
|
||||||
{},
|
|
||||||
serverObjectData.current || {},
|
serverObjectData.current || {},
|
||||||
objectData || {},
|
objectData || {},
|
||||||
allFormValues,
|
allFormValues
|
||||||
arrayReplaceCustomizer
|
|
||||||
)
|
)
|
||||||
const computedEntries = calculateComputedValues(
|
const computedEntries = calculateComputedValues(
|
||||||
currentFormData,
|
currentFormData,
|
||||||
@ -837,14 +885,7 @@ const ObjectForm = forwardRef(
|
|||||||
|
|
||||||
mergedFormValues._isEditing = isEditingRef.current
|
mergedFormValues._isEditing = isEditingRef.current
|
||||||
|
|
||||||
setObjectData((prev) => {
|
setObjectData((prev) => mergeFormData(prev, mergedFormValues))
|
||||||
return mergeWith(
|
|
||||||
{},
|
|
||||||
prev,
|
|
||||||
mergedFormValues,
|
|
||||||
arrayReplaceCustomizer
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children({
|
{children({
|
||||||
|
|||||||
@ -1,16 +1,8 @@
|
|||||||
import { Spin, Descriptions, Flex } from 'antd'
|
import { Spin, Descriptions, Flex } from 'antd'
|
||||||
import { useState, useEffect } from 'react'
|
|
||||||
import { LoadingOutlined } from '@ant-design/icons'
|
import { LoadingOutlined } from '@ant-design/icons'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import ObjectProperty from './ObjectProperty'
|
import ObjectProperty from './ObjectProperty'
|
||||||
import { getModelProperties } from '../../../database/ObjectModels'
|
import { getModelProperties } from '../../../database/ObjectModels'
|
||||||
import mergeWith from 'lodash/mergeWith'
|
|
||||||
|
|
||||||
const arrayReplaceCustomizer = (objValue, srcValue) => {
|
|
||||||
if (Array.isArray(srcValue)) {
|
|
||||||
return srcValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ObjectInfo = ({
|
const ObjectInfo = ({
|
||||||
loading = false,
|
loading = false,
|
||||||
@ -39,14 +31,6 @@ const ObjectInfo = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const allItems = getModelProperties(type)
|
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
|
// If properties array is empty, show all properties
|
||||||
// Otherwise, filter and order by the properties array
|
// Otherwise, filter and order by the properties array
|
||||||
let items
|
let items
|
||||||
@ -78,7 +62,7 @@ const ObjectInfo = ({
|
|||||||
const propertyName = item.name
|
const propertyName = item.name
|
||||||
// Support property.visible as a function (objectData) => boolean
|
// Support property.visible as a function (objectData) => boolean
|
||||||
if (typeof item.visible === 'function') {
|
if (typeof item.visible === 'function') {
|
||||||
const visible = item.visible(objectData || combinedObjectData || {})
|
const visible = item.visible(objectData || {})
|
||||||
if (!visible) return false
|
if (!visible) return false
|
||||||
}
|
}
|
||||||
if (isWhitelistMode) {
|
if (isWhitelistMode) {
|
||||||
@ -112,11 +96,11 @@ const ObjectInfo = ({
|
|||||||
{...item}
|
{...item}
|
||||||
{...objectPropertyProps}
|
{...objectPropertyProps}
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
objectData={combinedObjectData}
|
objectData={objectData}
|
||||||
parentData={parentData}
|
parentData={parentData}
|
||||||
showSince={true}
|
showSince={true}
|
||||||
useFormItem={isControlled ? false : objectPropertyProps.useFormItem}
|
useFormItem={isControlled ? false : objectPropertyProps.useFormItem}
|
||||||
value={isControlled ? combinedObjectData?.[item.name] : undefined}
|
value={isControlled ? objectData?.[item.name] : undefined}
|
||||||
modelType={type}
|
modelType={type}
|
||||||
onChange={
|
onChange={
|
||||||
isControlled
|
isControlled
|
||||||
|
|||||||
@ -22,7 +22,7 @@ const ObjectList = ({
|
|||||||
wrap={!scrollHorizontal}
|
wrap={!scrollHorizontal}
|
||||||
style={{
|
style={{
|
||||||
...style,
|
...style,
|
||||||
width: 'fit-content'
|
width: '100%'
|
||||||
}}
|
}}
|
||||||
justify={'start'}
|
justify={'start'}
|
||||||
>
|
>
|
||||||
@ -39,11 +39,7 @@ const ObjectList = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (scrollHorizontal) {
|
if (scrollHorizontal) {
|
||||||
return (
|
return <ScrollBox horizontalBottomPadding={9}>{listContents}</ScrollBox>
|
||||||
<ScrollBox horizontalBottomPadding={9}>
|
|
||||||
{listContents}
|
|
||||||
</ScrollBox>
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
return listContents
|
return listContents
|
||||||
}
|
}
|
||||||
|
|||||||
@ -970,6 +970,7 @@ const ObjectProperty = ({
|
|||||||
type={objectType}
|
type={objectType}
|
||||||
multiple
|
multiple
|
||||||
showHyperlink={showHyperlink}
|
showHyperlink={showHyperlink}
|
||||||
|
masterFilter={masterFilter}
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import { ApiServerContext } from '../context/ApiServerContext'
|
|||||||
import { AuthContext } from '../context/AuthContext'
|
import { AuthContext } from '../context/AuthContext'
|
||||||
import ObjectProperty from './ObjectProperty'
|
import ObjectProperty from './ObjectProperty'
|
||||||
import { getModelByName } from '../../../database/ObjectModels'
|
import { getModelByName } from '../../../database/ObjectModels'
|
||||||
import merge from 'lodash/merge'
|
|
||||||
import { getModelProperty } from '../../../database/ObjectModels'
|
import { getModelProperty } from '../../../database/ObjectModels'
|
||||||
const { SHOW_CHILD } = TreeSelect
|
const { SHOW_CHILD } = TreeSelect
|
||||||
|
|
||||||
@ -187,8 +186,7 @@ const ObjectSelect = ({
|
|||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
setObjectPropertiesTree((prev) => mergeGroups(prev, data))
|
setObjectPropertiesTree((prev) => mergeGroups(prev, data))
|
||||||
} else {
|
} else {
|
||||||
// Fallback if API returns something unexpected
|
setObjectPropertiesTree(data)
|
||||||
setObjectPropertiesTree((prev) => merge([], prev, data))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setInitialLoading(false)
|
setInitialLoading(false)
|
||||||
@ -382,10 +380,18 @@ const ObjectSelect = ({
|
|||||||
setTreeSelectValue(value)
|
setTreeSelectValue(value)
|
||||||
onChange?.(selectedObjects)
|
onChange?.(selectedObjects)
|
||||||
} else {
|
} else {
|
||||||
// Single selection
|
// Single selection: replace the previous object instead of emitting
|
||||||
const selectedObject = objectList.find((obj) => obj._id === value)
|
// 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)
|
setTreeSelectValue(value)
|
||||||
onChange?.(selectedObject)
|
onChange?.(selectedObject ?? null)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[multiple, objectList, onChange]
|
[multiple, objectList, onChange]
|
||||||
|
|||||||
@ -116,6 +116,10 @@ const StateTag = ({ state, showBadge = true, showTag = true, style = {} }) => {
|
|||||||
status = 'warning'
|
status = 'warning'
|
||||||
text = 'Used'
|
text = 'Used'
|
||||||
break
|
break
|
||||||
|
case 'consumed':
|
||||||
|
status = 'default'
|
||||||
|
text = 'Consumed'
|
||||||
|
break
|
||||||
case 'unconsumed':
|
case 'unconsumed':
|
||||||
status = 'success'
|
status = 'success'
|
||||||
text = 'Unconsumed'
|
text = 'Unconsumed'
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import mergeWith from 'lodash/mergeWith'
|
||||||
|
|
||||||
export function capitalizeFirstLetter(string) {
|
export function capitalizeFirstLetter(string) {
|
||||||
try {
|
try {
|
||||||
return string[0].toUpperCase() + string.slice(1)
|
return string[0].toUpperCase() + string.slice(1)
|
||||||
@ -34,5 +36,38 @@ export function round(num, decimals) {
|
|||||||
return Math.round(num * 10 ** decimals) / 10 ** decimals
|
return Math.round(num * 10 ** decimals) / 10 ** decimals
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-export the functions for backward compatibility
|
const isIdRecord = (value) =>
|
||||||
export {}
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -40,7 +40,11 @@ export const Listing = {
|
|||||||
label: 'New Listing',
|
label: 'New Listing',
|
||||||
icon: PlusIcon,
|
icon: PlusIcon,
|
||||||
content: (objectData, { onOk } = {}) => {
|
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',
|
'updatedAt',
|
||||||
'_id'
|
'_id'
|
||||||
],
|
],
|
||||||
group: [
|
group: ['marketplace', 'vendor', 'product', 'stockLocation'],
|
||||||
'marketplace',
|
|
||||||
'product',
|
|
||||||
'vendor',
|
|
||||||
'stockLocation',
|
|
||||||
'courierServices'
|
|
||||||
],
|
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
name: '_id',
|
name: '_id',
|
||||||
@ -304,7 +302,7 @@ export const Listing = {
|
|||||||
min: 0,
|
min: 0,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
required: false,
|
required: false,
|
||||||
columnWidth: 150
|
columnWidth: 170
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'marketplace',
|
name: 'marketplace',
|
||||||
@ -329,7 +327,8 @@ export const Listing = {
|
|||||||
label: 'Condition',
|
label: 'Condition',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
required: true,
|
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: [
|
options: [
|
||||||
{ value: 'new', label: 'New' },
|
{ value: 'new', label: 'New' },
|
||||||
{ value: 'likeNew', label: 'Like New' },
|
{ value: 'likeNew', label: 'Like New' },
|
||||||
|
|||||||
@ -1,16 +1,20 @@
|
|||||||
import { createElement, lazy } from 'react'
|
import { createElement, lazy } from 'react'
|
||||||
|
|
||||||
const ListingVarientInfo = lazy(
|
const ListingVarientInfo = lazy(
|
||||||
() => import('../../components/Dashboard/Sales/ListingVarients/ListingVarientInfo')
|
() =>
|
||||||
|
import('../../components/Dashboard/Sales/ListingVarients/ListingVarientInfo')
|
||||||
)
|
)
|
||||||
const NewListingVarient = lazy(
|
const NewListingVarient = lazy(
|
||||||
() => import('../../components/Dashboard/Sales/ListingVarients/NewListingVarient')
|
() =>
|
||||||
|
import('../../components/Dashboard/Sales/ListingVarients/NewListingVarient')
|
||||||
)
|
)
|
||||||
const PublishListingVarient = lazy(
|
const PublishListingVarient = lazy(
|
||||||
() => import('../../components/Dashboard/Sales/ListingVarients/PublishListingVarient')
|
() =>
|
||||||
|
import('../../components/Dashboard/Sales/ListingVarients/PublishListingVarient')
|
||||||
)
|
)
|
||||||
const UnpublishListingVarient = lazy(
|
const UnpublishListingVarient = lazy(
|
||||||
() => import('../../components/Dashboard/Sales/ListingVarients/UnpublishListingVarient')
|
() =>
|
||||||
|
import('../../components/Dashboard/Sales/ListingVarients/UnpublishListingVarient')
|
||||||
)
|
)
|
||||||
const DeleteObject = lazy(
|
const DeleteObject = lazy(
|
||||||
() => import('../../components/Dashboard/common/DeleteObject')
|
() => import('../../components/Dashboard/common/DeleteObject')
|
||||||
@ -40,7 +44,11 @@ export const ListingVarient = {
|
|||||||
label: 'New Listing Varient',
|
label: 'New Listing Varient',
|
||||||
icon: PlusIcon,
|
icon: PlusIcon,
|
||||||
content: (objectData, { onOk } = {}) => {
|
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',
|
'updatedAt',
|
||||||
'_reference'
|
'_reference'
|
||||||
],
|
],
|
||||||
sorters: ['state', 'stockQuantity', 'price', 'lastSyncedAt', 'createdAt', 'updatedAt', '_id'],
|
sorters: [
|
||||||
|
'state',
|
||||||
|
'stockQuantity',
|
||||||
|
'price',
|
||||||
|
'lastSyncedAt',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
'_id'
|
||||||
|
],
|
||||||
group: ['listing', 'state'],
|
group: ['listing', 'state'],
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
@ -241,7 +257,7 @@ export const ListingVarient = {
|
|||||||
min: 0,
|
min: 0,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
required: false,
|
required: false,
|
||||||
columnWidth: 150
|
columnWidth: 170
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'state',
|
name: 'state',
|
||||||
|
|||||||
@ -28,7 +28,11 @@ export const PartStock = {
|
|||||||
label: 'New Part Stock',
|
label: 'New Part Stock',
|
||||||
icon: PlusIcon,
|
icon: PlusIcon,
|
||||||
content: (objectData, { onOk } = {}) => {
|
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: [
|
filters: [
|
||||||
|
'part',
|
||||||
'partSku',
|
'partSku',
|
||||||
'state',
|
'state',
|
||||||
'startingQuantity',
|
'startingQuantity',
|
||||||
@ -65,6 +70,7 @@ export const PartStock = {
|
|||||||
'_reference'
|
'_reference'
|
||||||
],
|
],
|
||||||
sorters: [
|
sorters: [
|
||||||
|
'part',
|
||||||
'partSku',
|
'partSku',
|
||||||
'startingQuantity',
|
'startingQuantity',
|
||||||
'currentQuantity',
|
'currentQuantity',
|
||||||
@ -77,6 +83,7 @@ export const PartStock = {
|
|||||||
'state',
|
'state',
|
||||||
'startingQuantity',
|
'startingQuantity',
|
||||||
'currentQuantity',
|
'currentQuantity',
|
||||||
|
'part',
|
||||||
'partSku',
|
'partSku',
|
||||||
'stockLocation',
|
'stockLocation',
|
||||||
'createdAt',
|
'createdAt',
|
||||||
@ -132,6 +139,15 @@ export const PartStock = {
|
|||||||
required: true,
|
required: true,
|
||||||
masterFilter: ['subJob', 'stockTransfer']
|
masterFilter: ['subJob', 'stockTransfer']
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'part',
|
||||||
|
label: 'Part',
|
||||||
|
type: 'object',
|
||||||
|
objectType: 'part',
|
||||||
|
required: true,
|
||||||
|
showHyperlink: true,
|
||||||
|
columnWidth: 200
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'partSku',
|
name: 'partSku',
|
||||||
label: 'Part SKU',
|
label: 'Part SKU',
|
||||||
@ -139,7 +155,10 @@ export const PartStock = {
|
|||||||
objectType: 'partSku',
|
objectType: 'partSku',
|
||||||
required: true,
|
required: true,
|
||||||
showHyperlink: true,
|
showHyperlink: true,
|
||||||
columnWidth: 200
|
columnWidth: 200,
|
||||||
|
masterFilter: (objectData) => {
|
||||||
|
return { part: objectData?.part?._id }
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'stockLocation',
|
name: 'stockLocation',
|
||||||
|
|||||||
@ -1,20 +1,22 @@
|
|||||||
import { createElement, lazy } from 'react'
|
import { createElement, lazy } from 'react'
|
||||||
|
|
||||||
const ProductStockInfo = lazy(
|
const ProductStockInfo = lazy(
|
||||||
() => import('../../components/Dashboard/Inventory/ProductStocks/ProductStockInfo')
|
() =>
|
||||||
|
import('../../components/Dashboard/Inventory/ProductStocks/ProductStockInfo')
|
||||||
)
|
)
|
||||||
const NewProductStock = lazy(
|
const NewProductStock = lazy(
|
||||||
() => import('../../components/Dashboard/Inventory/ProductStocks/NewProductStock')
|
() =>
|
||||||
|
import('../../components/Dashboard/Inventory/ProductStocks/NewProductStock')
|
||||||
)
|
)
|
||||||
const PostProductStock = lazy(
|
const PostProductStock = lazy(
|
||||||
() => import('../../components/Dashboard/Inventory/ProductStocks/PostProductStock')
|
() =>
|
||||||
|
import('../../components/Dashboard/Inventory/ProductStocks/PostProductStock')
|
||||||
)
|
)
|
||||||
const DeleteObject = lazy(
|
const DeleteObject = lazy(
|
||||||
() => import('../../components/Dashboard/common/DeleteObject')
|
() => import('../../components/Dashboard/common/DeleteObject')
|
||||||
)
|
)
|
||||||
import ProductStockIcon from '../../components/Icons/ProductStockIcon'
|
import ProductStockIcon from '../../components/Icons/ProductStockIcon'
|
||||||
import PlusIcon from '../../components/Icons/PlusIcon'
|
import PlusIcon from '../../components/Icons/PlusIcon'
|
||||||
import { getModelByName } from '../ObjectModels.js'
|
|
||||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
import EditIcon from '../../components/Icons/EditIcon'
|
import EditIcon from '../../components/Icons/EditIcon'
|
||||||
import CheckIcon from '../../components/Icons/CheckIcon'
|
import CheckIcon from '../../components/Icons/CheckIcon'
|
||||||
@ -38,7 +40,11 @@ export const ProductStock = {
|
|||||||
label: 'New Product Stock',
|
label: 'New Product Stock',
|
||||||
icon: PlusIcon,
|
icon: PlusIcon,
|
||||||
content: (objectData, { onOk } = {}) => {
|
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,
|
modalWidth: 520,
|
||||||
label: 'Post',
|
label: 'Post',
|
||||||
icon: CheckIcon,
|
icon: CheckIcon,
|
||||||
|
disabled: (objectData) => {
|
||||||
|
if (objectData?._isEditing == true) return true
|
||||||
|
return objectData?.partStockList?.some(
|
||||||
|
(row) => row?.remainingQuantity > 0
|
||||||
|
)
|
||||||
|
},
|
||||||
visible: (objectData) => {
|
visible: (objectData) => {
|
||||||
return objectData?.state?.type == 'draft'
|
return objectData?.state?.type == 'draft'
|
||||||
},
|
},
|
||||||
@ -131,6 +143,7 @@ export const ProductStock = {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
filters: [
|
filters: [
|
||||||
|
'product',
|
||||||
'productSku',
|
'productSku',
|
||||||
'state',
|
'state',
|
||||||
'currentQuantity',
|
'currentQuantity',
|
||||||
@ -140,16 +153,19 @@ export const ProductStock = {
|
|||||||
'_reference'
|
'_reference'
|
||||||
],
|
],
|
||||||
sorters: [
|
sorters: [
|
||||||
|
'product',
|
||||||
'productSku',
|
'productSku',
|
||||||
'currentQuantity',
|
'currentQuantity',
|
||||||
'state',
|
'state',
|
||||||
'createdAt',
|
'createdAt',
|
||||||
'updatedAt'
|
'updatedAt',
|
||||||
|
'stockLocation'
|
||||||
],
|
],
|
||||||
columns: [
|
columns: [
|
||||||
'_reference',
|
'_reference',
|
||||||
'state',
|
'state',
|
||||||
'currentQuantity',
|
'currentQuantity',
|
||||||
|
'product',
|
||||||
'productSku',
|
'productSku',
|
||||||
'stockLocation',
|
'stockLocation',
|
||||||
'createdAt',
|
'createdAt',
|
||||||
@ -182,6 +198,13 @@ export const ProductStock = {
|
|||||||
readOnly: true,
|
readOnly: true,
|
||||||
columnWidth: 180
|
columnWidth: 180
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'updatedAt',
|
||||||
|
label: 'Updated At',
|
||||||
|
type: 'dateTime',
|
||||||
|
readOnly: true,
|
||||||
|
columnWidth: 175
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'state',
|
name: 'state',
|
||||||
label: 'State',
|
label: 'State',
|
||||||
@ -196,12 +219,15 @@ export const ProductStock = {
|
|||||||
readOnly: true,
|
readOnly: true,
|
||||||
columnWidth: 175
|
columnWidth: 175
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'updatedAt',
|
name: 'product',
|
||||||
label: 'Updated At',
|
label: 'Product',
|
||||||
type: 'dateTime',
|
type: 'object',
|
||||||
readOnly: true,
|
objectType: 'product',
|
||||||
columnWidth: 175
|
required: true,
|
||||||
|
showHyperlink: true,
|
||||||
|
columnWidth: 200
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'productSku',
|
name: 'productSku',
|
||||||
@ -210,14 +236,17 @@ export const ProductStock = {
|
|||||||
objectType: 'productSku',
|
objectType: 'productSku',
|
||||||
required: true,
|
required: true,
|
||||||
showHyperlink: true,
|
showHyperlink: true,
|
||||||
columnWidth: 200
|
columnWidth: 200,
|
||||||
|
masterFilter: (objectData) => {
|
||||||
|
return { product: objectData?.product?._id }
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'stockLocation',
|
name: 'stockLocation',
|
||||||
label: 'Stock location',
|
label: 'Stock location',
|
||||||
type: 'object',
|
type: 'object',
|
||||||
objectType: 'stockLocation',
|
objectType: 'stockLocation',
|
||||||
required: false,
|
required: true,
|
||||||
showHyperlink: true,
|
showHyperlink: true,
|
||||||
columnWidth: 200,
|
columnWidth: 200,
|
||||||
readOnly: (objectData) => {
|
readOnly: (objectData) => {
|
||||||
@ -232,11 +261,99 @@ export const ProductStock = {
|
|||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'partStocks',
|
name: 'partStockList',
|
||||||
label: 'Part Stocks',
|
label: 'Part Stock List',
|
||||||
type: 'objectChildren',
|
type: 'objectChildren',
|
||||||
canAddRemove: false,
|
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: [
|
properties: [
|
||||||
|
{
|
||||||
|
name: 'part',
|
||||||
|
label: 'Part',
|
||||||
|
type: 'object',
|
||||||
|
objectType: 'part',
|
||||||
|
readOnly: true,
|
||||||
|
required: true,
|
||||||
|
showHyperlink: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'partSku',
|
name: 'partSku',
|
||||||
label: 'Part SKU',
|
label: 'Part SKU',
|
||||||
@ -247,25 +364,52 @@ export const ProductStock = {
|
|||||||
showHyperlink: true
|
showHyperlink: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'partStock',
|
name: 'partStocks',
|
||||||
label: 'Part Stock',
|
label: 'Part Stocks',
|
||||||
type: 'object',
|
type: 'objectList',
|
||||||
objectType: 'partStock',
|
objectType: 'partStock',
|
||||||
required: true,
|
required: false,
|
||||||
showHyperlink: true,
|
showHyperlink: true,
|
||||||
|
columnWidth: 260,
|
||||||
masterFilter: (objectData) => {
|
masterFilter: (objectData) => {
|
||||||
const partSkuId = objectData?.partSku?._id
|
|
||||||
if (partSkuId == null) return {}
|
|
||||||
return {
|
return {
|
||||||
partSku: getModelByName('partSku').prefix + ':' + partSkuId
|
part: objectData?.part?._id,
|
||||||
|
partSku: objectData?.partSku?._id,
|
||||||
|
$or: [{ 'state.type': 'new' }, { 'state.type': 'used' }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'quantity',
|
name: 'requiredQuantity',
|
||||||
label: 'Quantity',
|
label: 'Required Quantity',
|
||||||
type: 'number',
|
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'
|
color: 'default'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'posted.count',
|
name: 'new.count',
|
||||||
label: 'Posted',
|
label: 'New',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
color: 'success'
|
color: 'success'
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user