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

View File

@ -1,6 +1,6 @@
import { useRef, useState } from 'react'
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 loglevel from 'loglevel'
import config from '../../../../config.js'
@ -21,9 +21,10 @@ import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
import { getModelProperty } from '../../../../database/ObjectModels.js'
import ObjectProperty from '../../common/ObjectProperty.jsx'
import OrderItemsIcon from '../../../Icons/OrderItemIcon.jsx'
import OrderItemIcon from '../../../Icons/OrderItemIcon.jsx'
import ShipShipment from './ShipShipment.jsx'
import ReceiveShipment from './ReceiveShipment.jsx'
import CancelShipment from './CancelShipment.jsx'
const log = loglevel.getLogger('ShipmentInfo')
log.setLevel(config.logLevel)
@ -51,6 +52,9 @@ const ShipmentInfo = () => {
objectData: {}
})
const [shipShipmentOpen, setShipShipmentOpen] = useState(false)
const [receiveShipmentOpen, setReceiveShipmentOpen] = useState(false)
const [cancelShipmentOpen, setCancelShipmentOpen] = useState(false)
const actions = {
reload: () => {
objectFormRef?.current?.handleFetchObject?.()
@ -71,6 +75,18 @@ const ShipmentInfo = () => {
delete: () => {
objectFormRef?.current?.handleDelete?.()
return true
},
ship: () => {
setShipShipmentOpen(true)
return true
},
receive: () => {
setReceiveShipmentOpen(true)
return true
},
cancel: () => {
setCancelShipmentOpen(true)
return true
}
}
@ -167,22 +183,26 @@ const ShipmentInfo = () => {
visibleProperties={{
items: false
}}
labelWidth='175px'
/>
</InfoCollapse>
<InfoCollapse
title='Shipment Items'
icon={<OrderItemsIcon />}
active={collapseState.info}
title='Shipment Order Items'
icon={<OrderItemIcon />}
active={collapseState.orderItems}
onToggle={(expanded) =>
updateCollapseState('info', expanded)
updateCollapseState('orderItems', expanded)
}
collapseKey='info'
collapseKey='orderItems'
>
<ObjectProperty
{...getModelProperty('shipment', 'items')}
isEditing={isEditing}
objectData={objectData}
loading={loading}
<ObjectTable
type='orderItem'
masterFilter={{ 'shipment._id': shipmentId }}
visibleColumns={{
shipment: false,
order: false,
orderType: false
}}
/>
</InfoCollapse>
</Flex>
@ -222,6 +242,60 @@ const ShipmentInfo = () => {
</Flex>
</ScrollBox>
</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 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 = {
name: 'shipment',
label: 'Shipment',
prefix: 'SHM',
prefix: 'SHP',
icon: ShipmentIcon,
actions: [
{
@ -14,25 +18,126 @@ export const Shipment = {
row: true,
icon: InfoCircleIcon,
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'],
filters: ['vendor', 'purchaseOrder', 'state', 'courierService'],
group: [],
filters: ['orderType', 'order', 'state', 'courierService'],
sorters: [
'createdAt',
'state',
'updatedAt',
'shippedDate',
'expectedDeliveryDate'
'shippedAt',
'expectedAt',
'deliveredAt'
],
columns: [
'_id',
'createdAt',
'_reference',
'state',
'orderType',
'order',
'amount',
'amountWithTax',
'taxRate',
'taxAmount',
'trackingNumber',
'shippedAt',
'expectedAt',
'deliveredAt',
'updatedAt',
'vendor',
'purchaseOrder',
'trackingNumber'
'createdAt'
],
properties: [
{
@ -50,231 +155,139 @@ export const Shipment = {
type: 'dateTime',
readOnly: true
},
{ name: 'state', label: 'State', type: 'state', readOnly: true },
{
name: '_reference',
label: 'Reference',
type: 'reference',
objectType: 'shipment',
showCopy: true,
readOnly: true
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true
},
{ name: 'state', label: 'State', type: 'state', readOnly: true },
{
name: 'purchaseOrder',
label: 'Purchase Order',
required: true,
type: 'object',
objectType: 'purchaseOrder',
showHyperlink: true
name: 'shippedAt',
label: 'Shipped At',
type: 'dateTime',
required: false
},
{
name: 'vendor',
label: 'Vendor',
name: 'orderType',
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,
type: 'object',
objectType: 'vendor',
showHyperlink: true
showHyperlink: true,
objectType: (objectData) => {
return objectData?.orderType
}
},
{
name: 'deliveredAt',
label: 'Delivered At',
type: 'dateTime',
required: false
},
{
name: 'courierService',
label: 'Courier Service',
required: false,
required: true,
type: 'object',
objectType: 'courierService'
},
{
name: 'trackingNumber',
label: 'Tracking Number',
type: 'string',
type: 'text',
required: false
},
{
name: 'items',
label: 'Shipment Items',
type: 'objectChildren',
required: true,
properties: [
{
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
}
}
showHyperlink: true
},
{
name: 'totalCostWithTax',
label: 'Total Cost w/ Tax',
name: 'taxRecord',
label: 'Tax Record',
type: 'object',
objectType: 'taxRecord',
showHyperlink: true
},
{
name: 'amount',
label: 'Amount',
type: 'number',
required: true,
readOnly: true,
prefix: '£',
min: 0,
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) => {
return (objectData?.amount * objectData?.taxRate?.rate) / 100 || 0
}
},
{
name: 'amountWithTax',
label: 'Amount w/ Tax',
type: 'number',
required: true,
prefix: '£',
min: 0,
step: 0.01,
readOnly: true,
columnWidth: 175,
value: (objectData) => {
if (objectData?.taxRate?.rateType == 'percentage') {
if (objectData?._isEditing == true) {
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
(objectData?.amount || 0) + (objectData?.taxAmount || 0)
).toFixed(2)
}
if (
Number.parseFloat(objectData?.amount) &&
(objectData.taxRate == undefined || objectData.taxRate == null)
) {
return Number.parseFloat(objectData?.amount).toFixed(2)
}
if (Number.parseFloat(objectData?.amountWithTax)) {
return Number.parseFloat(objectData?.amountWithTax).toFixed(2)
}
],
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)
return 0
}
}
]
},
{
name: 'cost',
label: 'Cost',
type: 'netGross',
required: true,
prefix: '£',
min: 0,
step: 0.01,
value: (objectData) => {
const net = objectData?.items?.reduce(
(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',
label: 'Shipped Date',
type: 'dateTime',
required: false
},
{
name: 'expectedDeliveryDate',
label: 'Expected Delivery Date',
type: 'dateTime',
required: false
},
{
name: 'actualDeliveryDate',
label: 'Actual Delivery Date',
type: 'dateTime',
required: false
},
{
name: 'notes',
label: 'Notes',
type: 'textarea',
required: false
}
]
}