Refactor shipment components and update shipment model

- Changed default shipment state from 'pending' to 'draft' in NewShipment component.
- Updated shipment information display to include new pricing and optional sections.
- Added modal dialogs for shipping, receiving, and canceling shipments in ShipmentInfo component.
- Enhanced Shipment model with new actions for editing, canceling edits, finishing edits, and managing shipment states.
- Introduced new properties for amount, tax rate, and tax amount in the Shipment model, along with updated sorting and filtering options.
This commit is contained in:
Tom Butcher 2025-12-27 13:52:13 +00:00
parent 1c586daf3b
commit 62a494509a
3 changed files with 341 additions and 229 deletions

View File

@ -2,15 +2,13 @@ import PropTypes from 'prop-types'
import ObjectInfo from '../../common/ObjectInfo' import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm' import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView' import WizardView from '../../common/WizardView'
import { getModelProperty } from '../../../../database/ObjectModels.js'
import ObjectProperty from '../../common/ObjectProperty.jsx'
const NewShipment = ({ onOk, reset, defaultValues }) => { const NewShipment = ({ onOk, reset, defaultValues }) => {
return ( return (
<NewObjectForm <NewObjectForm
type={'shipment'} type={'shipment'}
reset={reset} reset={reset}
defaultValues={{ state: { type: 'pending' }, ...defaultValues }} defaultValues={{ state: { type: 'draft' }, ...defaultValues }}
> >
{({ handleSubmit, submitLoading, objectData, formValid }) => { {({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [ const steps = [
@ -26,25 +24,48 @@ const NewShipment = ({ onOk, reset, defaultValues }) => {
required={true} required={true}
objectData={objectData} objectData={objectData}
visibleProperties={{ visibleProperties={{
items: false, amount: false,
cost: false, taxRate: false,
shippedDate: false, taxAmount: false,
expectedDeliveryDate: false, amountWithTax: false,
actualDeliveryDate: false, syncAmount: false
notes: false
}} }}
/> />
) )
}, },
{ {
title: 'Items', title: 'Pricing',
key: 'items', key: 'pricing',
content: ( content: (
<ObjectProperty <ObjectInfo
{...getModelProperty('shipment', 'items')} type='shipment'
column={1}
bordered={false}
isEditing={true} isEditing={true}
objectData={objectData} objectData={objectData}
loading={submitLoading} visibleProperties={{
amount: true,
taxRate: true,
taxAmount: true,
amountWithTax: true,
syncAmount: true
}}
/>
)
},
{
title: 'Optional',
key: 'optional',
content: (
<ObjectInfo
type='shipment'
column={1}
bordered={false}
isEditing={true}
objectData={objectData}
visibleProperties={{
trackingNumber: true
}}
/> />
) )
}, },
@ -58,9 +79,13 @@ const NewShipment = ({ onOk, reset, defaultValues }) => {
bordered={false} bordered={false}
visibleProperties={{ visibleProperties={{
_id: false, _id: false,
_reference: false,
createdAt: false, createdAt: false,
updatedAt: false, updatedAt: false,
items: false shippedAt: false,
expectedAt: false,
deliveredAt: false,
taxRecord: false
}} }}
isEditing={false} isEditing={false}
objectData={objectData} objectData={objectData}

View File

@ -1,6 +1,6 @@
import { useRef, useState } from 'react' import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import { Space, Flex, Card } from 'antd' import { Space, Flex, Card, Modal } from 'antd'
import { LoadingOutlined } from '@ant-design/icons' import { LoadingOutlined } from '@ant-design/icons'
import loglevel from 'loglevel' import loglevel from 'loglevel'
import config from '../../../../config.js' import config from '../../../../config.js'
@ -21,9 +21,10 @@ import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import ScrollBox from '../../common/ScrollBox.jsx' import ScrollBox from '../../common/ScrollBox.jsx'
import { getModelProperty } from '../../../../database/ObjectModels.js' import OrderItemIcon from '../../../Icons/OrderItemIcon.jsx'
import ObjectProperty from '../../common/ObjectProperty.jsx' import ShipShipment from './ShipShipment.jsx'
import OrderItemsIcon from '../../../Icons/OrderItemIcon.jsx' import ReceiveShipment from './ReceiveShipment.jsx'
import CancelShipment from './CancelShipment.jsx'
const log = loglevel.getLogger('ShipmentInfo') const log = loglevel.getLogger('ShipmentInfo')
log.setLevel(config.logLevel) log.setLevel(config.logLevel)
@ -51,6 +52,9 @@ const ShipmentInfo = () => {
objectData: {} objectData: {}
}) })
const [shipShipmentOpen, setShipShipmentOpen] = useState(false)
const [receiveShipmentOpen, setReceiveShipmentOpen] = useState(false)
const [cancelShipmentOpen, setCancelShipmentOpen] = useState(false)
const actions = { const actions = {
reload: () => { reload: () => {
objectFormRef?.current?.handleFetchObject?.() objectFormRef?.current?.handleFetchObject?.()
@ -71,6 +75,18 @@ const ShipmentInfo = () => {
delete: () => { delete: () => {
objectFormRef?.current?.handleDelete?.() objectFormRef?.current?.handleDelete?.()
return true return true
},
ship: () => {
setShipShipmentOpen(true)
return true
},
receive: () => {
setReceiveShipmentOpen(true)
return true
},
cancel: () => {
setCancelShipmentOpen(true)
return true
} }
} }
@ -167,22 +183,26 @@ const ShipmentInfo = () => {
visibleProperties={{ visibleProperties={{
items: false items: false
}} }}
labelWidth='175px'
/> />
</InfoCollapse> </InfoCollapse>
<InfoCollapse <InfoCollapse
title='Shipment Items' title='Shipment Order Items'
icon={<OrderItemsIcon />} icon={<OrderItemIcon />}
active={collapseState.info} active={collapseState.orderItems}
onToggle={(expanded) => onToggle={(expanded) =>
updateCollapseState('info', expanded) updateCollapseState('orderItems', expanded)
} }
collapseKey='info' collapseKey='orderItems'
> >
<ObjectProperty <ObjectTable
{...getModelProperty('shipment', 'items')} type='orderItem'
isEditing={isEditing} masterFilter={{ 'shipment._id': shipmentId }}
objectData={objectData} visibleColumns={{
loading={loading} shipment: false,
order: false,
orderType: false
}}
/> />
</InfoCollapse> </InfoCollapse>
</Flex> </Flex>
@ -222,6 +242,60 @@ const ShipmentInfo = () => {
</Flex> </Flex>
</ScrollBox> </ScrollBox>
</Flex> </Flex>
<Modal
open={shipShipmentOpen}
onCancel={() => {
setShipShipmentOpen(false)
}}
width={515}
footer={null}
destroyOnHidden={true}
centered={true}
>
<ShipShipment
onOk={() => {
setShipShipmentOpen(false)
actions.reload()
}}
objectData={objectFormState.objectData}
/>
</Modal>
<Modal
open={receiveShipmentOpen}
onCancel={() => {
setReceiveShipmentOpen(false)
}}
width={515}
footer={null}
destroyOnHidden={true}
centered={true}
>
<ReceiveShipment
onOk={() => {
setReceiveShipmentOpen(false)
actions.reload()
}}
objectData={objectFormState.objectData}
/>
</Modal>
<Modal
open={cancelShipmentOpen}
onCancel={() => {
setCancelShipmentOpen(false)
}}
width={515}
footer={null}
destroyOnHidden={true}
centered={true}
>
<CancelShipment
onOk={() => {
setCancelShipmentOpen(false)
actions.reload()
}}
objectData={objectFormState.objectData}
/>
</Modal>
</> </>
) )
} }

View File

@ -1,10 +1,14 @@
import ShipmentIcon from '../../components/Icons/ShipmentIcon' import ShipmentIcon from '../../components/Icons/ShipmentIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import EditIcon from '../../components/Icons/EditIcon'
import BinIcon from '../../components/Icons/BinIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
export const Shipment = { export const Shipment = {
name: 'shipment', name: 'shipment',
label: 'Shipment', label: 'Shipment',
prefix: 'SHM', prefix: 'SHP',
icon: ShipmentIcon, icon: ShipmentIcon,
actions: [ actions: [
{ {
@ -14,25 +18,126 @@ export const Shipment = {
row: true, row: true,
icon: InfoCircleIcon, icon: InfoCircleIcon,
url: (_id) => `/dashboard/inventory/shipments/info?shipmentId=${_id}` url: (_id) => `/dashboard/inventory/shipments/info?shipmentId=${_id}`
},
{
name: 'edit',
label: 'Edit',
type: 'button',
icon: EditIcon,
url: (_id) =>
`/dashboard/inventory/shipments/info?shipmentId=${_id}&action=edit`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
},
disabled: (objectData) => {
return objectData?.state?.type != 'draft'
}
},
{
name: 'cancelEdit',
label: 'Cancel Edit',
type: 'button',
icon: XMarkIcon,
url: (_id) =>
`/dashboard/inventory/shipments/info?shipmentId=${_id}&action=cancelEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
},
disabled: (objectData) => {
return objectData?.state?.type != 'draft'
}
},
{
name: 'finishEdit',
label: 'Finish Edit',
type: 'button',
icon: CheckIcon,
url: (_id) =>
`/dashboard/inventory/shipments/info?shipmentId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
},
disabled: (objectData) => {
return objectData?.state?.type != 'draft'
}
},
{
name: 'delete',
label: 'Delete',
type: 'button',
icon: BinIcon,
danger: true,
url: (_id) =>
`/dashboard/inventory/shipments/info?shipmentId=${_id}&action=delete`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
},
disabled: (objectData) => {
return objectData?.state?.type != 'draft'
}
},
{ type: 'divider' },
{
name: 'ship',
label: 'Ship',
type: 'button',
icon: CheckIcon,
url: (_id) =>
`/dashboard/inventory/shipments/info?shipmentId=${_id}&action=ship`,
disabled: (objectData) => {
return objectData?.state?.type != 'planned'
},
visible: (objectData) => {
return (
objectData?.state?.type == 'planned' ||
objectData?.state?.type == 'draft'
)
}
},
{
name: 'receive',
label: 'Receive',
type: 'button',
icon: CheckIcon,
url: (_id) =>
`/dashboard/inventory/shipments/info?shipmentId=${_id}&action=receive`,
disabled: (objectData) => {
return objectData?.state?.type != 'shipped'
},
visible: (objectData) => {
return (
objectData?.state?.type == 'shipped' ||
objectData?.state?.type == 'delivered'
)
}
} }
], ],
group: ['vendor', 'purchaseOrder'], group: [],
filters: ['vendor', 'purchaseOrder', 'state', 'courierService'], filters: ['orderType', 'order', 'state', 'courierService'],
sorters: [ sorters: [
'createdAt', 'createdAt',
'state', 'state',
'updatedAt', 'updatedAt',
'shippedDate', 'shippedAt',
'expectedDeliveryDate' 'expectedAt',
'deliveredAt'
], ],
columns: [ columns: [
'_id', '_id',
'createdAt', '_reference',
'state', 'state',
'orderType',
'order',
'amount',
'amountWithTax',
'taxRate',
'taxAmount',
'trackingNumber',
'shippedAt',
'expectedAt',
'deliveredAt',
'updatedAt', 'updatedAt',
'vendor', 'createdAt'
'purchaseOrder',
'trackingNumber'
], ],
properties: [ properties: [
{ {
@ -50,231 +155,139 @@ export const Shipment = {
type: 'dateTime', type: 'dateTime',
readOnly: true readOnly: true
}, },
{ name: 'state', label: 'State', type: 'state', readOnly: true }, {
name: '_reference',
label: 'Reference',
type: 'reference',
objectType: 'shipment',
showCopy: true,
readOnly: true
},
{ {
name: 'updatedAt', name: 'updatedAt',
label: 'Updated At', label: 'Updated At',
type: 'dateTime', type: 'dateTime',
readOnly: true readOnly: true
}, },
{ name: 'state', label: 'State', type: 'state', readOnly: true },
{ {
name: 'purchaseOrder', name: 'shippedAt',
label: 'Purchase Order', label: 'Shipped At',
required: true, type: 'dateTime',
type: 'object', required: false
objectType: 'purchaseOrder',
showHyperlink: true
}, },
{ {
name: 'vendor', name: 'orderType',
label: 'Vendor', label: 'Order Type',
required: true,
type: 'objectType',
masterFilter: ['purchaseOrder', 'salesOrder'],
showHyperlink: true
},
{
name: 'expectedAt',
label: 'Expected At',
type: 'dateTime',
required: false
},
{
name: 'order',
label: 'Order',
required: true, required: true,
type: 'object', type: 'object',
objectType: 'vendor', showHyperlink: true,
showHyperlink: true objectType: (objectData) => {
return objectData?.orderType
}
},
{
name: 'deliveredAt',
label: 'Delivered At',
type: 'dateTime',
required: false
}, },
{ {
name: 'courierService', name: 'courierService',
label: 'Courier Service', label: 'Courier Service',
required: false, required: true,
type: 'object', type: 'object',
objectType: 'courierService' objectType: 'courierService'
}, },
{ {
name: 'trackingNumber', name: 'trackingNumber',
label: 'Tracking Number', label: 'Tracking Number',
type: 'string', type: 'text',
required: false required: false
}, },
{ {
name: 'items', name: 'taxRate',
label: 'Shipment Items', label: 'Tax Rate',
type: 'objectChildren', type: 'object',
required: true, objectType: 'taxRate',
properties: [ showHyperlink: true
{
name: 'itemType',
label: 'Item Type',
type: 'objectType',
masterFilter: ['part', 'packaging'],
required: true
},
{
name: 'item',
label: 'Item',
type: 'object',
objectType: (objectData) => {
return objectData?.itemType
},
required: true,
showHyperlink: true
},
{
name: 'itemCost',
label: 'Item Cost',
type: 'number',
required: true,
prefix: '£',
min: 0,
step: 0.01,
columnWidth: 150,
value: (objectData) => {
if (objectData?.item) {
return objectData?.item?.cost || undefined
} else {
return undefined
}
}
},
{
name: 'quantity',
label: 'Quantity',
type: 'number',
required: true,
columnWidth: 150
},
{
name: 'totalCost',
label: 'Total Cost',
type: 'number',
required: true,
prefix: '£',
min: 0,
step: 0.01,
columnWidth: 150,
value: (objectData) => {
return (
(objectData?.itemCost || 0) * (objectData?.quantity || 0) ||
undefined
)
}
},
{
name: 'taxRate',
label: 'Tax Rate',
type: 'object',
objectType: 'taxRate',
showHyperlink: true,
value: (objectData) => {
if (objectData?.item) {
return objectData?.item?.costTaxRate || undefined
} else {
return undefined
}
}
},
{
name: 'totalCostWithTax',
label: 'Total Cost w/ Tax',
type: 'number',
required: true,
readOnly: true,
prefix: '£',
min: 0,
step: 0.01,
columnWidth: 175,
value: (objectData) => {
if (objectData?.taxRate?.rateType == 'percentage') {
return (
(
(objectData?.totalCost || 0) *
(1 + objectData?.taxRate?.rate / 100)
).toFixed(2) || undefined
)
} else if (objectData?.taxRate?.rateType == 'amount') {
return (
(
(objectData?.totalCost || 0) + objectData?.taxRate?.rate
).toFixed(2) || undefined
)
} else {
return objectData?.totalCost || undefined
}
}
}
],
rollups: [
{
name: 'totalQuantity',
label: 'Total',
type: 'number',
property: 'quantity',
value: (objectData) => {
return objectData?.items?.reduce(
(acc, item) => acc + item.quantity,
0
)
}
},
{
name: 'totalCost',
label: 'Total',
type: 'number',
prefix: '£',
property: 'totalCost',
value: (objectData) => {
return objectData?.items
?.reduce((acc, item) => acc + (item.totalCost || 0), 0)
.toFixed(2)
}
},
{
name: 'totalCostWithTax',
label: 'Total',
type: 'number',
prefix: '£',
property: 'totalCostWithTax',
value: (objectData) => {
return objectData?.items
?.reduce((acc, item) => acc + (item.totalCostWithTax || 0), 0)
.toFixed(2)
}
}
]
}, },
{ {
name: 'cost', name: 'taxRecord',
label: 'Cost', label: 'Tax Record',
type: 'netGross', type: 'object',
objectType: 'taxRecord',
showHyperlink: true
},
{
name: 'amount',
label: 'Amount',
type: 'number',
required: true, required: true,
prefix: '£', prefix: '£',
min: 0, min: 0,
step: 0.01, step: 0.01,
columnWidth: 150
},
{
name: 'taxAmount',
label: 'Tax Amount',
type: 'number',
required: true,
prefix: '£',
fixedNumber: 2,
min: 0,
step: 0.01,
readOnly: true,
value: (objectData) => { value: (objectData) => {
const net = objectData?.items?.reduce( return (objectData?.amount * objectData?.taxRate?.rate) / 100 || 0
(acc, item) => acc + (item.totalCost || 0),
0
)
const gross = objectData?.items?.reduce(
(acc, item) => acc + (item.totalCostWithTax || 0),
0
)
return { net: net, gross: gross }
} }
}, },
{ {
name: 'shippedDate', name: 'amountWithTax',
label: 'Shipped Date', label: 'Amount w/ Tax',
type: 'dateTime', type: 'number',
required: false required: true,
}, prefix: '£',
{ min: 0,
name: 'expectedDeliveryDate', step: 0.01,
label: 'Expected Delivery Date', readOnly: true,
type: 'dateTime', columnWidth: 175,
required: false value: (objectData) => {
}, if (objectData?._isEditing == true) {
{ return (
name: 'actualDeliveryDate', (objectData?.amount || 0) + (objectData?.taxAmount || 0)
label: 'Actual Delivery Date', ).toFixed(2)
type: 'dateTime', }
required: false if (
}, Number.parseFloat(objectData?.amount) &&
{ (objectData.taxRate == undefined || objectData.taxRate == null)
name: 'notes', ) {
label: 'Notes', return Number.parseFloat(objectData?.amount).toFixed(2)
type: 'textarea', }
required: false if (Number.parseFloat(objectData?.amountWithTax)) {
return Number.parseFloat(objectData?.amountWithTax).toFixed(2)
}
return 0
}
} }
] ]
} }