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}
|
||||
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}
|
||||
|
||||
@ -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 }}
|
||||
/>
|
||||
</InfoCollapse>
|
||||
<InfoCollapse
|
||||
title='Part Stocks'
|
||||
title='Part Stock List'
|
||||
icon={<PartStockIcon />}
|
||||
active={collapseState.partStocks}
|
||||
active={collapseState.partStockList}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('partStocks', expanded)
|
||||
updateCollapseState('partStockList', expanded)
|
||||
}
|
||||
collapseKey='partStocks'
|
||||
collapseKey='partStockList'
|
||||
>
|
||||
<ObjectProperty
|
||||
{...getModelProperty('productStock', 'partStocks')}
|
||||
{...getModelProperty('productStock', 'partStockList')}
|
||||
isEditing={isEditing}
|
||||
objectData={objectData}
|
||||
loading={loading}
|
||||
@ -244,7 +247,10 @@ const ProductStockInfo = () => {
|
||||
) : (
|
||||
<ObjectTable
|
||||
type='stockEvent'
|
||||
masterFilter={{ parent: getModelByName('productStock').prefix + ':' + productStockId }}
|
||||
masterFilter={{
|
||||
parent:
|
||||
getModelByName('productStock').prefix + ':' + productStockId
|
||||
}}
|
||||
visibleColumns={{ parent: false }}
|
||||
/>
|
||||
)}
|
||||
@ -300,9 +306,7 @@ const ProductStockInfo = () => {
|
||||
type='auditLog'
|
||||
masterFilter={{
|
||||
parent:
|
||||
getModelByName('productStock').prefix +
|
||||
':' +
|
||||
productStockId
|
||||
getModelByName('productStock').prefix + ':' + productStockId
|
||||
}}
|
||||
visibleColumns={{ _id: false, parent: false }}
|
||||
/>
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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,6 +314,9 @@ const ObjectForm = forwardRef(
|
||||
: getValueAtPath(workingData, parentPath)
|
||||
|
||||
if (property.value && typeof property.value === 'function') {
|
||||
const skipValue =
|
||||
skipObjectChildrenValue && property.type === 'objectChildren'
|
||||
if (!skipValue) {
|
||||
try {
|
||||
const computedValue = property.value(scopeData || {})
|
||||
if (computedValue !== undefined) {
|
||||
@ -324,6 +334,7 @@ const ObjectForm = forwardRef(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(property.properties) &&
|
||||
@ -478,8 +489,10 @@ const ObjectForm = forwardRef(
|
||||
}
|
||||
}, []) // Empty dependency array - only run on mount/unmount
|
||||
|
||||
const handleFetchObject = useCallback(async () => {
|
||||
const handleFetchObject = useCallback(
|
||||
async (options = {}) => {
|
||||
const objectKey = `${type}:${id}`
|
||||
const skipObjectChildrenValue = options.skipObjectChildrenValue === true
|
||||
|
||||
try {
|
||||
setFetchLoading(true)
|
||||
@ -503,9 +516,17 @@ const ObjectForm = forwardRef(
|
||||
serverObjectData.current = data
|
||||
|
||||
// Calculate and set computed values on initial load
|
||||
const computedEntries = calculateComputedValues(data, model)
|
||||
const computedEntries = calculateComputedValues(data, model, {
|
||||
skipObjectChildrenValue
|
||||
})
|
||||
const initialFormData = applyComputedEntries(data, computedEntries)
|
||||
setObjectData({ ...initialFormData, _isEditing: isEditingRef.current })
|
||||
if (skipObjectChildrenValue) {
|
||||
copyObjectChildren(initialFormData, data, model)
|
||||
}
|
||||
setObjectData({
|
||||
...initialFormData,
|
||||
_isEditing: isEditingRef.current
|
||||
})
|
||||
form.setFieldsValue(initialFormData)
|
||||
setFetchLoading(false)
|
||||
onStateChangeRef.current({ loading: false })
|
||||
@ -515,7 +536,8 @@ const ObjectForm = forwardRef(
|
||||
`Failed to fetch ${getModelMessageLabel(type).toLowerCase()} info.`
|
||||
)
|
||||
}
|
||||
}, [
|
||||
},
|
||||
[
|
||||
fetchObject,
|
||||
fetchObjectActivities,
|
||||
id,
|
||||
@ -526,14 +548,22 @@ const ObjectForm = forwardRef(
|
||||
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({
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 (
|
||||
<ScrollBox horizontalBottomPadding={9}>
|
||||
{listContents}
|
||||
</ScrollBox>
|
||||
)
|
||||
return <ScrollBox horizontalBottomPadding={9}>{listContents}</ScrollBox>
|
||||
} else {
|
||||
return listContents
|
||||
}
|
||||
|
||||
@ -970,6 +970,7 @@ const ObjectProperty = ({
|
||||
type={objectType}
|
||||
multiple
|
||||
showHyperlink={showHyperlink}
|
||||
masterFilter={masterFilter}
|
||||
{...inputProps}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@ -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' },
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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'
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user