Compare commits

..

No commits in common. "94406a1bfcf99470e457bcccbaec7120244cc2ae" and "d7827ecb6d013dbe73a654b85b1e46960c68d210" have entirely different histories.

46 changed files with 376 additions and 2939 deletions

View File

@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><g><path d="M29.953,40.788l10.342,0c1.761,0 3.18,1.258 3.18,3.055c0,1.788 -1.349,3.03 -3.18,3.03l-18.004,0c-1.785,0 -3.086,-1.234 -3.086,-3.075c0,-1.581 0.953,-2.598 2.336,-3.193l0.024,-0.01c2.054,-0.833 3.153,-2.551 3.153,-4.757c0,-0.425 -0.052,-0.856 -0.133,-1.305l-2.8,0c-1.588,0 -2.701,-1.131 -2.701,-2.591c0,-1.408 1.105,-2.56 2.701,-2.56l1.544,-0c-0.168,-0.87 -0.242,-1.672 -0.242,-2.485c0,-6.288 4.756,-9.887 11.404,-9.887c2.234,0 3.578,0.148 5.274,0.766c1.53,0.469 2.646,1.469 2.646,3.106c0,0.878 -0.321,1.553 -0.828,2.023c-0.48,0.446 -1.159,0.725 -2.011,0.725c-0.479,0 -1.089,-0.115 -1.715,-0.248l-0.045,-0.01c-0.697,-0.176 -1.631,-0.301 -2.775,-0.301c-2.932,0 -5.078,1.33 -5.078,3.98c0,0.734 0.074,1.394 0.272,2.332l7.292,0c1.585,0 2.701,1.163 2.701,2.56c0,1.449 -1.124,2.591 -2.701,2.591l-6.16,0c0.024,0.361 0.034,0.743 0.034,1.147c0,1.832 -0.479,3.671 -1.444,5.108Z"/><path d="M12.892,61l38.215,0c6.616,0 9.892,-3.245 9.892,-9.735l0,-38.499c0,-6.49 -3.276,-9.766 -9.892,-9.766l-38.215,-0c-6.584,0 -9.892,3.276 -9.892,9.766l0,38.499c0,6.49 3.308,9.735 9.892,9.735Zm0.063,-5.072c-3.15,0 -4.883,-1.67 -4.883,-4.946l0,-37.932c0,-3.276 1.733,-4.978 4.883,-4.978l38.089,0c3.119,0 4.883,1.701 4.883,4.978l0,37.932c0,3.276 -1.764,4.946 -4.883,4.946l-38.089,0Z" style="fill-rule:nonzero;"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.58577,0,0,0.58577,29,29)">
<path d="M11.031,59.75L48.719,59.75C55.859,59.75 59.75,55.891 59.75,48.797L59.75,10.969C59.75,3.891 55.859,-0 48.719,-0L11.031,-0C3.906,-0 -0,3.891 -0,10.969L-0,48.797C-0,55.891 3.906,59.75 11.031,59.75ZM11.906,51.688C9.391,51.688 8.063,50.469 8.063,47.813L8.063,11.969C8.063,9.313 9.391,8.078 11.906,8.078L47.844,8.078C50.344,8.078 51.688,9.313 51.688,11.969L51.688,47.813C51.688,50.469 50.344,51.688 47.844,51.688L11.906,51.688Z" style="fill-rule:nonzero;"/>
<g transform="matrix(0.664312,0,0,0.664312,5.121464,12.135807)">
<path d="M37.594,52.969C39.062,52.969 40.625,52.281 41.875,51L61.844,31.094C62.938,30 63.656,28.188 63.656,26.5C63.656,24.812 62.938,23 61.844,21.906L41.875,1.969C40.625,0.688 39.062,0 37.594,0C33.812,0 31.406,2.562 31.406,5.906C31.406,7.875 32.312,9.25 33.5,10.406L40.5,17.344L50.219,26.5L40.5,35.656L33.5,42.562C32.312,43.688 31.406,45.094 31.406,47.062C31.406,50.406 33.812,52.969 37.594,52.969ZM1.485,32.781L37.75,32.781L51.969,32.094C55.531,31.938 57.906,29.844 57.906,26.5C57.906,23.156 55.531,21.062 51.969,20.906L37.75,20.219L1.485,20.219C-2.515,20.219 -5.14,22.719 -5.14,26.5C-5.14,30.281 -2.515,32.781 1.485,32.781Z" style="fill-rule:nonzero;"/>
</g>
</g>
<g transform="matrix(0.739137,0,0,0.739137,-0,2.611674)">
<path d="M35.853,52.047L25.734,52.047C20.578,52.047 17.797,48.969 17.047,43.844L12.484,12.719L3.906,12.719C1.812,12.719 0,10.906 0,8.766C0,6.625 1.812,4.828 3.906,4.828L14.172,4.828C18.125,4.828 19.609,6.437 20.047,9.625L20.402,12.109L69.062,12.109C71.922,12.109 73.438,13.75 73.438,16.031C73.438,16.5 73.344,17.062 73.281,17.516L71.172,31.781C71.146,31.963 71.118,32.143 71.087,32.319L63.084,32.319C63.272,32.028 63.394,31.657 63.453,31.219L65.078,19.094L21.4,19.094L23.397,33.062L43.157,33.063C41.542,33.617 40.191,34.464 39.103,35.547C37.914,36.73 37.007,38.224 36.457,40.031L24.393,40.031L24.844,43.188C25,44.312 25.656,45.094 26.719,45.094L35.853,45.094L35.853,52.047ZM28.422,68.437C25.078,68.437 22.359,65.766 22.359,62.391C22.359,59.063 25.078,56.359 28.422,56.359C31.781,56.359 34.453,59.063 34.453,62.391C34.453,65.766 31.781,68.437 28.422,68.437Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -31,7 +31,6 @@ import AuthCallback from './components/App/AuthCallback.jsx'
import {
ProductionRoutes,
InventoryRoutes,
FinanceRoutes,
ManagementRoutes,
DeveloperRoutes
} from './routes'
@ -97,7 +96,6 @@ const AppContent = () => {
>
{ProductionRoutes}
{InventoryRoutes}
{FinanceRoutes}
{ManagementRoutes}
{DeveloperRoutes}
</Route>

View File

@ -1,61 +0,0 @@
import { useContext } from 'react'
import { Flex } from 'antd'
import useCollapseState from '../hooks/useCollapseState'
import StatsDisplay from '../common/StatsDisplay'
import InfoCollapse from '../common/InfoCollapse'
import ScrollBox from '../common/ScrollBox'
import { ApiServerContext } from '../context/ApiServerContext'
const FinanceOverview = () => {
const { connected } = useContext(ApiServerContext)
const [collapseState, updateCollapseState] = useCollapseState(
'FinanceOverview',
{
invoiceStats: true
}
)
if (!connected) {
return null
}
return (
<Flex
gap='large'
vertical='true'
style={{
maxHeight: '100%',
minHeight: 0
}}
>
<ScrollBox>
<Flex vertical gap={'large'}>
<InfoCollapse
title='Invoice Statistics'
icon={null}
active={collapseState.invoiceStats}
onToggle={(isActive) =>
updateCollapseState('invoiceStats', isActive)
}
className='no-t-padding-collapse'
collapseKey='invoiceStats'
>
<Flex
justify='flex-start'
gap='middle'
wrap='wrap'
align='flex-start'
>
<StatsDisplay objectType='invoice' />
</Flex>
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
)
}
export default FinanceOverview

View File

@ -1,46 +0,0 @@
import { useLocation } from 'react-router-dom'
import DashboardSidebar from '../common/DashboardSidebar'
import InvoiceIcon from '../../Icons/InvoiceIcon'
import FinanceIcon from '../../Icons/FinanceIcon'
const items = [
{
key: 'overview',
label: 'Overview',
icon: <FinanceIcon />,
path: '/dashboard/finance/overview'
},
{ type: 'divider' },
{
key: 'invoices',
label: 'Invoices',
icon: <InvoiceIcon />,
path: '/dashboard/finance/invoices'
}
]
const routeKeyMap = {
'/dashboard/finance/overview': 'overview',
'/dashboard/finance/invoices': 'invoices'
}
const FinanceSidebar = (props) => {
const location = useLocation()
const selectedKey = (() => {
const match = Object.keys(routeKeyMap).find((path) => {
const pathSplit = path.split('/')
const locationPathSplit = location.pathname.split('/')
if (pathSplit.length > locationPathSplit.length) return false
for (let i = 0; i < pathSplit.length; i++) {
if (pathSplit[i] !== locationPathSplit[i]) return false
}
return true
})
return match ? routeKeyMap[match] : 'overview'
})()
return <DashboardSidebar items={items} selectedKey={selectedKey} {...props} />
}
export default FinanceSidebar

View File

@ -1,98 +0,0 @@
import { useState, useRef } from 'react'
import { Button, Flex, Space, Dropdown, Modal } from 'antd'
import NewInvoice from './Invoices/NewInvoice'
import ObjectTable from '../common/ObjectTable'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import GridIcon from '../../Icons/GridIcon'
import ListIcon from '../../Icons/ListIcon'
import useViewMode from '../hooks/useViewMode'
import ColumnViewButton from '../common/ColumnViewButton'
const Invoices = () => {
const [newInvoiceOpen, setNewInvoiceOpen] = useState(false)
const tableRef = useRef()
const [viewMode, setViewMode] = useViewMode('invoices')
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('invoices')
const actionItems = {
items: [
{
label: 'New Invoice',
key: 'newInvoice',
icon: <PlusIcon />
},
{ type: 'divider' },
{
label: 'Reload List',
key: 'reloadList',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reloadList') {
tableRef.current?.reload()
} else if (key === 'newInvoice') {
setNewInvoiceOpen(true)
}
}
}
return (
<>
<Flex vertical={'true'} gap='large'>
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<ColumnViewButton
type='invoice'
loading={false}
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
</Space>
<Space>
<Button
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
onClick={() =>
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
}
/>
</Space>
</Flex>
<ObjectTable
ref={tableRef}
visibleColumns={columnVisibility}
type='invoice'
cards={viewMode === 'cards'}
/>
</Flex>
<Modal
open={newInvoiceOpen}
styles={{ content: { paddingBottom: '24px' } }}
footer={null}
width={800}
onCancel={() => {
setNewInvoiceOpen(false)
}}
destroyOnHidden={true}
>
<NewInvoice
onOk={() => {
setNewInvoiceOpen(false)
tableRef.current?.reload()
}}
reset={newInvoiceOpen}
/>
</Modal>
</>
)
}
export default Invoices

View File

@ -1,218 +0,0 @@
import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import loglevel from 'loglevel'
import config from '../../../../config.js'
import useCollapseState from '../../hooks/useCollapseState.js'
import NotesPanel from '../../common/NotesPanel.jsx'
import InfoCollapse from '../../common/InfoCollapse.jsx'
import ObjectInfo from '../../common/ObjectInfo.jsx'
import ViewButton from '../../common/ViewButton.jsx'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import ObjectForm from '../../common/ObjectForm.jsx'
import EditButtons from '../../common/EditButtons.jsx'
import LockIndicator from '../../common/LockIndicator.jsx'
import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
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 { getModelByName } from '../../../../database/ObjectModels.js'
const log = loglevel.getLogger('InvoiceInfo')
log.setLevel(config.logLevel)
const InvoiceInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const invoiceId = new URLSearchParams(location.search).get('invoiceId')
const [collapseState, updateCollapseState] = useCollapseState(
'InvoiceInfo',
{
info: true,
notes: true,
auditLogs: true
}
)
const [objectFormState, setEditFormState] = useState({
isEditing: false,
editLoading: false,
formValid: false,
lock: null,
loading: false,
objectData: {}
})
const actions = {
reload: () => {
objectFormRef?.current?.handleFetchObject?.()
return true
},
edit: () => {
objectFormRef?.current?.startEditing?.()
return false
},
cancelEdit: () => {
objectFormRef?.current?.cancelEditing?.()
return true
},
finishEdit: () => {
objectFormRef?.current?.handleUpdate?.()
return true
},
delete: () => {
objectFormRef?.current?.handleDelete?.()
return true
}
}
const editDisabled = getModelByName('invoice')
?.actions?.find((action) => action.name === 'edit')
?.disabled(objectFormState.objectData) ?? false
return (
<>
<Flex
gap='large'
vertical='true'
style={{
maxHeight: '100%',
minHeight: 0
}}
>
<Flex justify={'space-between'}>
<Space size='middle'>
<Space size='small'>
<ObjectActions
type='invoice'
id={invoiceId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Invoice Information' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='invoice'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>
<Space>
<EditButtons
isEditing={objectFormState.isEditing}
handleUpdate={() => {
actionHandlerRef.current.callAction('finishEdit')
}}
cancelEditing={() => {
actionHandlerRef.current.callAction('cancelEdit')
}}
startEditing={() => {
actionHandlerRef.current.callAction('edit')
}}
editLoading={objectFormState.editLoading}
formValid={objectFormState.formValid}
disabled={
objectFormState.lock?.locked ||
objectFormState.loading ||
editDisabled
}
loading={objectFormState.editLoading}
/>
</Space>
</Flex>
<ScrollBox>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
loading={objectFormState.loading}
ref={actionHandlerRef}
>
<ObjectForm
id={invoiceId}
type='invoice'
style={{ height: '100%' }}
ref={objectFormRef}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => (
<Flex vertical gap={'large'}>
<InfoCollapse
title='Invoice Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) =>
updateCollapseState('info', expanded)
}
collapseKey='info'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='invoice'
labelWidth='225px'
objectData={objectData}
/>
</InfoCollapse>
</Flex>
)}
</ObjectForm>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={invoiceId} type='invoice' />
</Card>
</InfoCollapse>
<InfoCollapse
title='Audit Logs'
icon={<AuditLogIcon />}
active={collapseState.auditLogs}
onToggle={(expanded) =>
updateCollapseState('auditLogs', expanded)
}
collapseKey='auditLogs'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='auditLog'
masterFilter={{ 'parent._id': invoiceId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
</>
)
}
export default InvoiceInfo

View File

@ -1,113 +0,0 @@
import PropTypes from 'prop-types'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
const NewInvoice = ({ onOk, reset, defaultValues }) => {
return (
<NewObjectForm
type={'invoice'}
reset={reset}
defaultValues={{
state: { type: 'draft' },
invoiceType: 'sales',
...defaultValues
}}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='invoice'
column={1}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
visibleProperties={{
orderType: true,
order: true,
vendor: true,
invoiceDate: true,
dueDate: true
}}
/>
)
},
{
title: 'Optional',
key: 'optional',
content: (
<ObjectInfo
type='invoice'
column={1}
bordered={false}
isEditing={true}
required={false}
objectData={objectData}
visibleProperties={{
relatedOrderType: true,
relatedOrder: true
}}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='invoice'
column={1}
bordered={false}
visibleProperties={{
_id: false,
createdAt: false,
updatedAt: false,
_reference: false,
totalAmount: false,
totalAmountWithTax: false,
totalTaxAmount: false,
shippingAmount: false,
shippingAmountWithTax: false,
grandTotalAmount: false,
sentAt: false,
paidAt: false,
cancelledAt: false,
overdueAt: false
}}
isEditing={false}
objectData={objectData}
/>
)
}
]
return (
<WizardView
steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Invoice'
onSubmit={async () => {
const result = await handleSubmit()
if (result) {
onOk()
}
}}
/>
)
}}
</NewObjectForm>
)
}
NewInvoice.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool,
defaultValues: PropTypes.object
}
export default NewInvoice

View File

@ -13,8 +13,7 @@ const InventoryOverview = () => {
const [collapseState, updateCollapseState] = useCollapseState(
'InventoryOverview',
{
inventoryStats: true,
purchaseOrderStats: true
inventoryStats: true
}
)
@ -56,27 +55,6 @@ const InventoryOverview = () => {
</Flex>
</InfoCollapse>
<InfoCollapse
title='Purchase Order Statistics'
icon={null}
canCollapse={false}
active={collapseState.purchaseOrderStats}
onToggle={(isActive) =>
updateCollapseState('purchaseOrderStats', isActive)
}
className='no-t-padding-collapse'
collapseKey='purchaseOrderStats'
>
<Flex
justify='flex-start'
gap='middle'
wrap='wrap'
align='flex-start'
>
<StatsDisplay objectType='purchaseOrder' />
</Flex>
</InfoCollapse>
<Flex gap='large' wrap='wrap'>
<Flex flex={1} vertical style={{ minWidth: '300px' }}>
<InfoCollapse

View File

@ -8,12 +8,7 @@ const NewOrderItem = ({ onOk, reset, defaultValues }) => {
<NewObjectForm
type={'orderItem'}
reset={reset}
defaultValues={{
state: { type: 'draft' },
syncAmount: null,
quantity: 1,
...defaultValues
}}
defaultValues={{ syncAmount: null, quantity: 1, ...defaultValues }}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
@ -59,23 +54,6 @@ const NewOrderItem = ({ onOk, reset, defaultValues }) => {
/>
)
},
{
title: 'Optional',
key: 'optional',
content: (
<ObjectInfo
type='orderItem'
column={1}
bordered={false}
isEditing={true}
required={false}
objectData={objectData}
visibleProperties={{
shipment: true
}}
/>
)
},
{
title: 'Summary',
key: 'summary',
@ -87,8 +65,7 @@ const NewOrderItem = ({ onOk, reset, defaultValues }) => {
visibleProperties={{
_id: false,
createdAt: false,
updatedAt: false,
_reference: false
updatedAt: false
}}
isEditing={false}
objectData={objectData}

View File

@ -1,46 +0,0 @@
import { useState, useContext } from 'react'
import PropTypes from 'prop-types'
import { ApiServerContext } from '../../context/ApiServerContext'
import { message } from 'antd'
import MessageDialogView from '../../common/MessageDialogView.jsx'
const AcknowledgePurchaseOrder = ({ onOk, objectData }) => {
const [acknowledgeLoading, setAcknowledgeLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handleAcknowledge = async () => {
setAcknowledgeLoading(true)
try {
const result = await sendObjectFunction(
objectData._id,
'PurchaseOrder',
'acknowledge'
)
if (result) {
message.success('Purchase order acknowledged successfully')
onOk(result)
}
} catch (error) {
console.error('Error acknowledging purchase order:', error)
} finally {
setAcknowledgeLoading(false)
}
}
return (
<MessageDialogView
title={'Are you sure you want to acknowledge this purchase order?'}
description={`Acknowledging purchase order ${objectData?.name || objectData?._reference || objectData?._id} will update its status to acknowledged.`}
onOk={handleAcknowledge}
okText='Acknowledge'
okLoading={acknowledgeLoading}
/>
)
}
AcknowledgePurchaseOrder.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default AcknowledgePurchaseOrder

View File

@ -1,46 +0,0 @@
import { useState, useContext } from 'react'
import PropTypes from 'prop-types'
import { ApiServerContext } from '../../context/ApiServerContext'
import { message } from 'antd'
import MessageDialogView from '../../common/MessageDialogView.jsx'
const CancelPurchaseOrder = ({ onOk, objectData }) => {
const [cancelLoading, setCancelLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handleCancel = async () => {
setCancelLoading(true)
try {
const result = await sendObjectFunction(
objectData._id,
'PurchaseOrder',
'cancel'
)
if (result) {
message.success('Purchase order cancelled successfully')
onOk(result)
}
} catch (error) {
console.error('Error cancelling purchase order:', error)
} finally {
setCancelLoading(false)
}
}
return (
<MessageDialogView
title={'Are you sure you want to cancel this purchase order?'}
description={`Cancelling purchase order ${objectData?.name || objectData?._reference || objectData?._id} will update its status to cancelled.`}
onOk={handleCancel}
okText='Cancel'
okLoading={cancelLoading}
/>
)
}
CancelPurchaseOrder.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default CancelPurchaseOrder

View File

@ -2,15 +2,15 @@ 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 NewPurchaseOrder = ({ onOk, reset, defaultValues }) => {
return (
<NewObjectForm
type={'purchaseOrder'}
reset={reset}
defaultValues={{
state: { type: 'draft' },
...defaultValues
}}
defaultValues={{ state: { type: 'new' }, ...defaultValues }}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
@ -26,13 +26,24 @@ const NewPurchaseOrder = ({ onOk, reset, defaultValues }) => {
required={true}
objectData={objectData}
visibleProperties={{
_reference: false,
items: false,
cost: false
}}
/>
)
},
{
title: 'Items',
key: 'items',
content: (
<ObjectProperty
{...getModelProperty('purchaseOrder', 'items')}
isEditing={true}
objectData={objectData}
loading={submitLoading}
/>
)
},
{
title: 'Summary',
key: 'summary',
@ -45,15 +56,7 @@ const NewPurchaseOrder = ({ onOk, reset, defaultValues }) => {
_id: false,
createdAt: false,
updatedAt: false,
_reference: false,
totalAmount: false,
totalAmountWithTax: false,
totalTaxAmount: false,
postedAt: false,
acknowledgedAt: false,
shippingAmount: false,
shippingAmountWithTax: false,
grandTotalAmount: false
items: false
}}
isEditing={false}
objectData={objectData}

View File

@ -1,46 +0,0 @@
import { useState, useContext } from 'react'
import PropTypes from 'prop-types'
import { ApiServerContext } from '../../context/ApiServerContext'
import { message } from 'antd'
import MessageDialogView from '../../common/MessageDialogView.jsx'
const PostPurchaseOrder = ({ onOk, objectData }) => {
const [postLoading, setPostLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handlePost = async () => {
setPostLoading(true)
try {
const result = await sendObjectFunction(
objectData._id,
'PurchaseOrder',
'post'
)
if (result) {
message.success('Purchase order posted successfully')
onOk(result)
}
} catch (error) {
console.error('Error posting purchase order:', error)
} finally {
setPostLoading(false)
}
}
return (
<MessageDialogView
title={'Are you sure you want to post this purchase order?'}
description={`Posting purchase order ${objectData?.name || objectData?._reference || objectData?._id} will finalize it and update inventory levels where applicable.`}
onOk={handlePost}
okText='Post'
okLoading={postLoading}
/>
)
}
PostPurchaseOrder.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default PostPurchaseOrder

View File

@ -23,12 +23,6 @@ import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
import OrderItemsIcon from '../../../Icons/OrderItemIcon.jsx'
import NewOrderItem from '../OrderItems/NewOrderItem.jsx'
import NewShipment from '../Shipments/NewShipment.jsx'
import PostPurchaseOrder from './PostPurchaseOrder.jsx'
import AcknowledgePurchaseOrder from './AcknowledgePurchaseOrder.jsx'
import CancelPurchaseOrder from './CancelPurchaseOrder.jsx'
import ShipmentIcon from '../../../Icons/ShipmentIcon.jsx'
import { getModelByName } from '../../../../database/ObjectModels.js'
const log = loglevel.getLogger('PurchaseOrderInfo')
log.setLevel(config.logLevel)
@ -36,15 +30,8 @@ log.setLevel(config.logLevel)
const PurchaseOrderInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const orderItemsTableRef = useRef(null)
const shipmentsTableRef = useRef(null)
const actionHandlerRef = useRef(null)
const [newOrderItemOpen, setNewOrderItemOpen] = useState(false)
const [newShipmentOpen, setNewShipmentOpen] = useState(false)
const [postPurchaseOrderOpen, setPostPurchaseOrderOpen] = useState(false)
const [acknowledgePurchaseOrderOpen, setAcknowledgePurchaseOrderOpen] =
useState(false)
const [cancelPurchaseOrderOpen, setCancelPurchaseOrderOpen] = useState(false)
const purchaseOrderId = new URLSearchParams(location.search).get(
'purchaseOrderId'
)
@ -72,17 +59,14 @@ const PurchaseOrderInfo = () => {
return true
},
edit: () => {
orderItemsTableRef?.current?.startEditing?.()
objectFormRef?.current?.startEditing?.()
return false
},
cancelEdit: () => {
orderItemsTableRef?.current?.cancelEditing?.()
objectFormRef?.current?.cancelEditing?.()
return true
},
finishEdit: () => {
orderItemsTableRef?.current?.handleUpdate?.()
objectFormRef?.current?.handleUpdate?.()
return true
},
@ -93,29 +77,9 @@ const PurchaseOrderInfo = () => {
newOrderItem: () => {
setNewOrderItemOpen(true)
return true
},
newShipment: () => {
setNewShipmentOpen(true)
return true
},
post: () => {
setPostPurchaseOrderOpen(true)
return true
},
acknowledge: () => {
setAcknowledgePurchaseOrderOpen(true)
return true
},
cancel: () => {
setCancelPurchaseOrderOpen(true)
return true
}
}
const editDisabled = getModelByName('purchaseOrder')
.actions.find((action) => action.name === 'edit')
.disabled(objectFormState.objectData)
return (
<>
<Flex
@ -139,8 +103,6 @@ const PurchaseOrderInfo = () => {
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Purchase Order Information' },
{ key: 'orderItems', label: 'Order Items' },
{ key: 'shipments', label: 'Shipments' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
@ -169,11 +131,7 @@ const PurchaseOrderInfo = () => {
}}
editLoading={objectFormState.editLoading}
formValid={objectFormState.formValid}
disabled={
objectFormState.lock?.locked ||
objectFormState.loading ||
editDisabled
}
disabled={objectFormState.lock?.locked || objectFormState.loading}
loading={objectFormState.editLoading}
/>
</Space>
@ -211,7 +169,6 @@ const PurchaseOrderInfo = () => {
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='purchaseOrder'
labelWidth='225px'
objectData={objectData}
visibleProperties={{
items: false
@ -221,39 +178,16 @@ const PurchaseOrderInfo = () => {
<InfoCollapse
title='Purchase Order Items'
icon={<OrderItemsIcon />}
active={collapseState.orderItems}
active={collapseState.info}
onToggle={(expanded) =>
updateCollapseState('orderItems', expanded)
updateCollapseState('info', expanded)
}
collapseKey='orderItems'
collapseKey='info'
>
<ObjectTable
type='orderItem'
masterFilter={{
'order._id': purchaseOrderId,
orderType: 'purchaseOrder'
}}
masterFilter={{ 'order._id': purchaseOrderId }}
visibleColumns={{ order: false }}
ref={orderItemsTableRef}
/>
</InfoCollapse>
<InfoCollapse
title='Shipments'
icon={<ShipmentIcon />}
active={collapseState.shipments}
onToggle={(expanded) =>
updateCollapseState('shipments', expanded)
}
collapseKey='shipments'
>
<ObjectTable
type='shipment'
masterFilter={{
'order._id': purchaseOrderId,
orderType: 'purchaseOrder'
}}
visibleColumns={{ order: false }}
ref={shipmentsTableRef}
/>
</InfoCollapse>
</Flex>
@ -314,80 +248,6 @@ const PurchaseOrderInfo = () => {
}}
/>
</Modal>
<Modal
open={newShipmentOpen}
onCancel={() => {
setNewShipmentOpen(false)
}}
width={800}
footer={null}
destroyOnHidden={true}
>
<NewShipment
onOk={() => {
setNewShipmentOpen(false)
}}
reset={newShipmentOpen}
defaultValues={{
orderType: 'purchaseOrder',
order: { _id: purchaseOrderId }
}}
/>
</Modal>
<Modal
open={postPurchaseOrderOpen}
onCancel={() => {
setPostPurchaseOrderOpen(false)
}}
width={500}
footer={null}
destroyOnHidden={true}
centered={true}
>
<PostPurchaseOrder
onOk={() => {
setPostPurchaseOrderOpen(false)
actions.reload()
}}
objectData={objectFormState.objectData}
/>
</Modal>
<Modal
open={acknowledgePurchaseOrderOpen}
onCancel={() => {
setAcknowledgePurchaseOrderOpen(false)
}}
width={515}
footer={null}
destroyOnHidden={true}
centered={true}
>
<AcknowledgePurchaseOrder
onOk={() => {
setAcknowledgePurchaseOrderOpen(false)
actions.reload()
}}
objectData={objectFormState.objectData}
/>
</Modal>
<Modal
open={cancelPurchaseOrderOpen}
onCancel={() => {
setCancelPurchaseOrderOpen(false)
}}
width={515}
footer={null}
destroyOnHidden={true}
centered={true}
>
<CancelPurchaseOrder
onOk={() => {
setCancelPurchaseOrderOpen(false)
actions.reload()
}}
objectData={objectFormState.objectData}
/>
</Modal>
</>
)
}

View File

@ -1,46 +0,0 @@
import { useState, useContext } from 'react'
import PropTypes from 'prop-types'
import { ApiServerContext } from '../../context/ApiServerContext'
import { message } from 'antd'
import MessageDialogView from '../../common/MessageDialogView.jsx'
const CancelShipment = ({ onOk, objectData }) => {
const [cancelLoading, setCancelLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handleCancel = async () => {
setCancelLoading(true)
try {
const result = await sendObjectFunction(
objectData._id,
'Shipment',
'cancel'
)
if (result) {
message.success('Shipment cancelled successfully')
onOk(result)
}
} catch (error) {
console.error('Error cancelling shipment:', error)
} finally {
setCancelLoading(false)
}
}
return (
<MessageDialogView
title={'Are you sure you want to cancel this shipment?'}
description={`Cancelling shipment ${objectData?.name || objectData?._reference || objectData?._id} will update its status to cancelled.`}
onOk={handleCancel}
okText='Cancel'
okLoading={cancelLoading}
/>
)
}
CancelShipment.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default CancelShipment

View File

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

View File

@ -1,362 +0,0 @@
import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card, Modal } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import loglevel from 'loglevel'
import config from '../../../../config.js'
import useCollapseState from '../../hooks/useCollapseState.js'
import NotesPanel from '../../common/NotesPanel.jsx'
import InfoCollapse from '../../common/InfoCollapse.jsx'
import ObjectInfo from '../../common/ObjectInfo.jsx'
import ViewButton from '../../common/ViewButton.jsx'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import ObjectForm from '../../common/ObjectForm.jsx'
import EditButtons from '../../common/EditButtons.jsx'
import LockIndicator from '../../common/LockIndicator.jsx'
import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
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 OrderItemsIcon from '../../../Icons/OrderItemIcon.jsx'
import NewOrderItem from '../OrderItems/NewOrderItem.jsx'
import NewShipment from './NewShipment.jsx'
import PostPurchaseOrder from '../PurchaseOrders/PostPurchaseOrder.jsx'
import AcknowledgePurchaseOrder from '../PurchaseOrders/AcknowledgePurchaseOrder.jsx'
import ShipmentIcon from '../../../Icons/ShipmentIcon.jsx'
const log = loglevel.getLogger('PurchaseOrderInfo')
log.setLevel(config.logLevel)
const PurchaseOrderInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const orderItemsTableRef = useRef(null)
const shipmentsTableRef = useRef(null)
const actionHandlerRef = useRef(null)
const [newOrderItemOpen, setNewOrderItemOpen] = useState(false)
const [newShipmentOpen, setNewShipmentOpen] = useState(false)
const [postPurchaseOrderOpen, setPostPurchaseOrderOpen] = useState(false)
const [acknowledgePurchaseOrderOpen, setAcknowledgePurchaseOrderOpen] =
useState(false)
const purchaseOrderId = new URLSearchParams(location.search).get(
'purchaseOrderId'
)
const [collapseState, updateCollapseState] = useCollapseState(
'PurchaseOrderInfo',
{
info: true,
notes: true,
auditLogs: true
}
)
const [objectFormState, setEditFormState] = useState({
isEditing: false,
editLoading: false,
formValid: false,
lock: null,
loading: false,
objectData: {}
})
const actions = {
reload: () => {
objectFormRef?.current?.handleFetchObject?.()
return true
},
edit: () => {
orderItemsTableRef?.current?.startEditing?.()
objectFormRef?.current?.startEditing?.()
return false
},
cancelEdit: () => {
orderItemsTableRef?.current?.cancelEditing?.()
objectFormRef?.current?.cancelEditing?.()
return true
},
finishEdit: () => {
orderItemsTableRef?.current?.handleUpdate?.()
objectFormRef?.current?.handleUpdate?.()
return true
},
delete: () => {
objectFormRef?.current?.handleDelete?.()
return true
},
newOrderItem: () => {
setNewOrderItemOpen(true)
return true
},
newShipment: () => {
setNewShipmentOpen(true)
return true
},
post: () => {
setPostPurchaseOrderOpen(true)
return true
},
acknowledge: () => {
setAcknowledgePurchaseOrderOpen(true)
return true
}
}
return (
<>
<Flex
gap='large'
vertical='true'
style={{
maxHeight: '100%',
minHeight: 0
}}
>
<Flex justify={'space-between'}>
<Space size='middle'>
<Space size='small'>
<ObjectActions
type='purchaseOrder'
id={purchaseOrderId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Purchase Order Information' },
{ key: 'orderItems', label: 'Order Items' },
{ key: 'shipments', label: 'Shipments' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='purchaseOrder'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>
<Space>
<EditButtons
isEditing={objectFormState.isEditing}
handleUpdate={() => {
actionHandlerRef.current.callAction('finishEdit')
}}
cancelEditing={() => {
actionHandlerRef.current.callAction('cancelEdit')
}}
startEditing={() => {
actionHandlerRef.current.callAction('edit')
}}
editLoading={objectFormState.editLoading}
formValid={objectFormState.formValid}
disabled={objectFormState.lock?.locked || objectFormState.loading}
loading={objectFormState.editLoading}
/>
</Space>
</Flex>
<ScrollBox>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
loading={objectFormState.loading}
ref={actionHandlerRef}
>
<ObjectForm
id={purchaseOrderId}
type='purchaseOrder'
style={{ height: '100%' }}
ref={objectFormRef}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => (
<Flex vertical gap={'large'}>
<InfoCollapse
title='Purchase Order Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) =>
updateCollapseState('info', expanded)
}
collapseKey='info'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='purchaseOrder'
labelWidth='225px'
objectData={objectData}
visibleProperties={{
items: false
}}
/>
</InfoCollapse>
<InfoCollapse
title='Purchase Order Items'
icon={<OrderItemsIcon />}
active={collapseState.orderItems}
onToggle={(expanded) =>
updateCollapseState('orderItems', expanded)
}
collapseKey='orderItems'
>
<ObjectTable
type='orderItem'
masterFilter={{
'order._id': purchaseOrderId,
orderType: 'purchaseOrder'
}}
visibleColumns={{ order: false }}
ref={orderItemsTableRef}
/>
</InfoCollapse>
<InfoCollapse
title='Shipments'
icon={<ShipmentIcon />}
active={collapseState.shipments}
onToggle={(expanded) =>
updateCollapseState('shipments', expanded)
}
collapseKey='shipments'
>
<ObjectTable
type='shipment'
masterFilter={{
'order._id': purchaseOrderId,
orderType: 'purchaseOrder'
}}
visibleColumns={{ order: false }}
ref={shipmentsTableRef}
/>
</InfoCollapse>
</Flex>
)}
</ObjectForm>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={purchaseOrderId} type='purchaseOrder' />
</Card>
</InfoCollapse>
<InfoCollapse
title='Audit Logs'
icon={<AuditLogIcon />}
active={collapseState.auditLogs}
onToggle={(expanded) =>
updateCollapseState('auditLogs', expanded)
}
collapseKey='auditLogs'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='auditLog'
masterFilter={{ 'parent._id': purchaseOrderId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
<Modal
open={newOrderItemOpen}
onCancel={() => {
setNewOrderItemOpen(false)
}}
width={800}
footer={null}
destroyOnHidden={true}
>
<NewOrderItem
onOk={() => {
setNewOrderItemOpen(false)
}}
reset={newOrderItemOpen}
defaultValues={{
order: { _id: purchaseOrderId },
orderType: 'purchaseOrder',
syncAmount: 'itemCost'
}}
/>
</Modal>
<Modal
open={newShipmentOpen}
onCancel={() => {
setNewShipmentOpen(false)
}}
width={800}
footer={null}
destroyOnHidden={true}
>
<NewShipment
onOk={() => {
setNewShipmentOpen(false)
}}
reset={newShipmentOpen}
defaultValues={{
orderType: 'purchaseOrder',
order: { _id: purchaseOrderId }
}}
/>
</Modal>
<Modal
open={postPurchaseOrderOpen}
onCancel={() => {
setPostPurchaseOrderOpen(false)
}}
width={500}
footer={null}
destroyOnHidden={true}
centered={true}
>
<PostPurchaseOrder
onOk={() => {
setPostPurchaseOrderOpen(false)
actions.reload()
}}
objectData={objectFormState.objectData}
/>
</Modal>
<Modal
open={acknowledgePurchaseOrderOpen}
onCancel={() => {
setAcknowledgePurchaseOrderOpen(false)
}}
width={515}
footer={null}
destroyOnHidden={true}
centered={true}
>
<AcknowledgePurchaseOrder
onOk={() => {
setAcknowledgePurchaseOrderOpen(false)
actions.reload()
}}
objectData={objectFormState.objectData}
/>
</Modal>
</>
)
}
export default PurchaseOrderInfo

View File

@ -1,46 +0,0 @@
import { useState, useContext } from 'react'
import PropTypes from 'prop-types'
import { ApiServerContext } from '../../context/ApiServerContext'
import { message } from 'antd'
import MessageDialogView from '../../common/MessageDialogView.jsx'
const ReceiveShipment = ({ onOk, objectData }) => {
const [receiveLoading, setReceiveLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handleReceive = async () => {
setReceiveLoading(true)
try {
const result = await sendObjectFunction(
objectData._id,
'Shipment',
'receive'
)
if (result) {
message.success('Shipment received successfully')
onOk(result)
}
} catch (error) {
console.error('Error receiving shipment:', error)
} finally {
setReceiveLoading(false)
}
}
return (
<MessageDialogView
title={'Are you sure you want to receive this shipment?'}
description={`Receiving shipment ${objectData?.name || objectData?._reference || objectData?._id} will update its status to delivered.`}
onOk={handleReceive}
okText='Receive'
okLoading={receiveLoading}
/>
)
}
ReceiveShipment.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default ReceiveShipment

View File

@ -1,46 +0,0 @@
import { useState, useContext } from 'react'
import PropTypes from 'prop-types'
import { ApiServerContext } from '../../context/ApiServerContext'
import { message } from 'antd'
import MessageDialogView from '../../common/MessageDialogView.jsx'
const ShipShipment = ({ onOk, objectData }) => {
const [shipLoading, setShipLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handleShip = async () => {
setShipLoading(true)
try {
const result = await sendObjectFunction(
objectData._id,
'Shipment',
'ship'
)
if (result) {
message.success('Shipment shipped successfully')
onOk(result)
}
} catch (error) {
console.error('Error shipping shipment:', error)
} finally {
setShipLoading(false)
}
}
return (
<MessageDialogView
title={'Are you sure you want to ship this shipment?'}
description={`Shipping shipment ${objectData?.name || objectData?._reference || objectData?._id} will update its status to shipped.`}
onOk={handleShip}
okText='Ship'
okLoading={shipLoading}
/>
)
}
ShipShipment.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default ShipShipment

View File

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

@ -4,7 +4,6 @@ import { Layout, Flex } from 'antd'
import { useLocation } from 'react-router-dom'
import ProductionSidebar from './Production/ProductionSidebar'
import InventorySidebar from './Inventory/InventorySidebar'
import FinanceSidebar from './Finance/FinanceSidebar'
import ManagementSidebar from './Management/ManagementSidebar'
import DashboardNavigation from './common/DashboardNavigation'
import DashboardBreadcrumb from './common/DashboardBreadcrumb'
@ -18,7 +17,6 @@ const DashboardLayout = ({ children }) => {
const location = useLocation()
const isProduction = location.pathname.startsWith('/dashboard/production')
const isInventory = location.pathname.startsWith('/dashboard/inventory')
const isFinance = location.pathname.startsWith('/dashboard/finance')
const isManagement = location.pathname.startsWith('/dashboard/management')
const isDeveloper = location.pathname.startsWith('/dashboard/developer')
@ -36,8 +34,6 @@ const DashboardLayout = ({ children }) => {
<ProductionSidebar />
) : isInventory ? (
<InventorySidebar />
) : isFinance ? (
<FinanceSidebar />
) : isManagement ? (
<ManagementSidebar />
) : isDeveloper ? (

View File

@ -10,7 +10,6 @@ const breadcrumbNameMap = {
inventory: 'Inventory',
management: 'Management',
developer: 'Developer',
finance: 'Finance',
overview: 'Overview',
info: 'Info',
design: 'Design',

View File

@ -30,7 +30,6 @@ import FarmControlLogoSmall from '../../Logos/FarmControlLogoSmall'
import MenuIcon from '../../Icons/MenuIcon'
import ProductionIcon from '../../Icons/ProductionIcon'
import InventoryIcon from '../../Icons/InventoryIcon'
import FinanceIcon from '../../Icons/FinanceIcon'
import PersonIcon from '../../Icons/PersonIcon'
import CloudIcon from '../../Icons/CloudIcon'
import BellIcon from '../../Icons/BellIcon'
@ -70,11 +69,6 @@ const DashboardNavigation = () => {
label: 'Inventory',
icon: <InventoryIcon />
},
{
key: 'finance',
label: 'Finance',
icon: <FinanceIcon />
},
{
key: 'management',
label: 'Management',
@ -136,8 +130,6 @@ const DashboardNavigation = () => {
navigate('/dashboard/production/overview')
} else if (key === 'inventory') {
navigate('/dashboard/inventory/overview')
} else if (key === 'finance') {
navigate('/dashboard/finance/overview')
} else if (key === 'management') {
navigate('/dashboard/management/filaments')
}

View File

@ -253,15 +253,10 @@ const HistoryDisplay = ({
// Create color mapping from model stats
const colors = {
success: themeColors.colorSuccess,
processing: themeColors.colorInfo,
info: themeColors.colorInfo,
error: themeColors.colorError,
warning: themeColors.colorWarning,
default: '#8c8c8c',
cyan: themeColors.colorCyan,
pink: themeColors.colorPink,
purple: themeColors.colorPurple,
magenta: themeColors.colorMagenta,
volcano: themeColors.colorVolcano
default: '#8c8c8c'
}
// Build color range array based on model stats order

View File

@ -1,40 +0,0 @@
import PropTypes from 'prop-types'
import { Flex, Typography, Button } from 'antd'
import InfoCircleIcon from '../../Icons/InfoCircleIcon.jsx'
const { Text } = Typography
const MessageDialogView = ({
icon,
title,
description,
onOk,
okText,
okLoading
}) => {
return (
<Flex vertical gap={'middle'}>
<Flex gap={'middle'}>
{icon || <InfoCircleIcon />}
<Text strong>{title}</Text>
</Flex>
{description && <Text>{description}</Text>}
<Flex justify={'end'}>
<Button type='primary' onClick={onOk} loading={okLoading}>
{okText || 'OK'}
</Button>
</Flex>
</Flex>
)
}
MessageDialogView.propTypes = {
icon: PropTypes.node,
title: PropTypes.node.isRequired,
description: PropTypes.node,
onOk: PropTypes.func.isRequired,
okText: PropTypes.string,
okLoading: PropTypes.bool
}
export default MessageDialogView

View File

@ -31,10 +31,7 @@ const buildObjectFromEntries = (entries = []) => {
* }) => ReactNode
*/
const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
const [objectData, setObjectData] = useState({
...defaultValues,
_isEditing: true
})
const [objectData, setObjectData] = useState(defaultValues)
const [submitLoading, setSubmitLoading] = useState(false)
const [formValid, setFormValid] = useState(false)
const [form] = Form.useForm()

View File

@ -16,7 +16,6 @@ import DeleteObjectModal from './DeleteObjectModal'
import merge from 'lodash/merge'
import set from 'lodash/set'
import { getModelByName } from '../../../database/ObjectModels'
import { useNavigate } from 'react-router-dom'
const buildObjectFromEntries = (entries = []) => {
return entries.reduce((acc, entry) => {
@ -73,7 +72,7 @@ const ObjectForm = forwardRef(
flushFile
} = useContext(ApiServerContext)
const { token } = useContext(AuthContext)
const navigate = useNavigate()
// Get the model definition for this object type
const model = getModelByName(type)
@ -463,7 +462,6 @@ const ObjectForm = forwardRef(
await deleteObject(id, type)
setDeleteModalOpen(false)
showSuccess('Deleted successfully')
navigate(-2)
// Optionally: trigger a callback to parent to remove this object from view
} catch (err) {
console.error(err)

View File

@ -87,7 +87,6 @@ const ObjectProperty = ({
showPreview = true,
options = [],
roundNumber = false,
fixedNumber = false,
showHyperlink,
showSince,
properties = [],
@ -273,9 +272,6 @@ const ObjectProperty = ({
if (roundNumber != false && typeof value === 'number') {
roundedValue = round(value, roundNumber)
}
if (fixedNumber != false && typeof value === 'number') {
roundedValue = value.toFixed(fixedNumber)
}
return (
<Text {...textParams}>

View File

@ -47,8 +47,6 @@ const ObjectSelect = ({
const [objectList, setObjectList] = useState([])
const [treeSelectValue, setTreeSelectValue] = useState(null)
const [initialLoading, setInitialLoading] = useState(true)
const [expandedKeys, setExpandedKeys] = useState([])
const [treeVersion, setTreeVersion] = useState(0)
const valueRef = useRef(null)
// Refs to track value changes
@ -213,7 +211,6 @@ const ObjectSelect = ({
objectType={type}
objectData={object}
isEditing={false}
showHyperlink={false}
style={{ top: '-0.5px' }}
/>
</div>
@ -249,15 +246,6 @@ const ObjectSelect = ({
value: valueString
})
var nodeChildren = buildTreeData(
children,
pIdx + 1,
parentKeys.concat(valueString),
newFilterPath
)
if (nodeChildren.length == 0) {
nodeChildren = undefined
}
const modelProperty = getModelProperty(type, property)
return {
title: <ObjectProperty {...modelProperty} value={value} />,
@ -269,7 +257,12 @@ const ObjectSelect = ({
filterPath: newFilterPath,
selectable: false,
isLeaf: false,
children: nodeChildren
children: buildTreeData(
children,
pIdx + 1,
parentKeys.concat(valueString),
newFilterPath
)
}
})
.filter(Boolean)
@ -279,7 +272,6 @@ const ObjectSelect = ({
// --- loadData for async loading on expand ---
const loadData = async (node) => {
console.log('loading data for node', node)
// node.property is the property name, node.value is the value key
if (!node.property) return
if (type == 'unknown') return
@ -392,39 +384,24 @@ const ObjectSelect = ({
// console.log('fullValue', fullValue)
// Build a new filter from value's properties that are in the properties list
const valueFilter = { ...filter }
const pathKeys = []
const parentKeys = []
properties.forEach((prop) => {
if (Object.prototype.hasOwnProperty.call(fullValue, prop)) {
const filterValue = fullValue[prop]
let valueString = filterValue
if (
filterValue &&
typeof filterValue === 'object' &&
filterValue._id
) {
valueFilter[prop] = filterValue._id
valueString = filterValue._id
} else if (filterValue?.name) {
valueFilter[prop] = filterValue.name
valueString = filterValue.name
} else if (Array.isArray(filterValue)) {
valueFilter[prop] = filterValue.join(',')
valueString = filterValue.join(',')
} else {
valueFilter[prop] = filterValue
valueString = filterValue
}
// Build the path key for this property level
const nodeKey = parentKeys
.concat(prop + ':' + valueString)
.join('-')
pathKeys.push(nodeKey)
parentKeys.push(valueString)
}
})
// Expand the path to the object
setExpandedKeys(pathKeys)
// Fetch with the new filter
handleFetchObjectsProperties(valueFilter)
// console.log('setting treeSelectValue', valueRef.current._id)
@ -485,8 +462,6 @@ const ObjectSelect = ({
setObjectPropertiesTree({})
setObjectList([])
setTreeData([])
setTreeVersion((v) => v + 1)
setExpandedKeys([])
setInitialized(false)
onTreeSelectChange(null)
setTreeSelectValue(null)
@ -554,11 +529,8 @@ const ObjectSelect = ({
// --- Main TreeSelect UI ---
return (
<TreeSelect
key={treeVersion}
treeDataSimpleMode={false}
treeDefaultExpandAll={true}
treeExpandedKeys={expandedKeys}
onTreeExpand={setExpandedKeys}
treeData={treeData}
showSearch={showSearch}
multiple={multiple}

View File

@ -5,7 +5,6 @@ import {
useEffect,
useState,
useCallback,
useMemo,
createElement
} from 'react'
import {
@ -20,8 +19,7 @@ import {
Button,
Input,
Space,
Tooltip,
Form
Tooltip
} from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import PropTypes from 'prop-types'
@ -45,46 +43,6 @@ import { ElectronContext } from '../context/ElectronContext'
const logger = loglevel.getLogger('DasboardTable')
logger.setLevel(config.logLevel)
const RowForm = ({ record, isEditing, onRegister, children }) => {
const [form] = Form.useForm()
useEffect(() => {
if (isEditing && record && !record.isSkeleton) {
form.setFieldsValue(record)
onRegister(record._id, form)
}
return () => {
if (record?._id) onRegister(record._id, null)
}
}, [isEditing, record, form, onRegister])
return (
<Form form={form} component={false}>
{children}
</Form>
)
}
RowForm.propTypes = {
record: PropTypes.object,
isEditing: PropTypes.bool,
onRegister: PropTypes.func,
children: PropTypes.node
}
const EditableRow = ({ record, isEditing, onRegister, ...props }) => {
return (
<RowForm record={record} isEditing={isEditing} onRegister={onRegister}>
<tr {...props} />
</RowForm>
)
}
EditableRow.propTypes = {
record: PropTypes.object,
isEditing: PropTypes.bool,
onRegister: PropTypes.func
}
const ObjectTable = forwardRef(
(
{
@ -96,26 +54,17 @@ const ObjectTable = forwardRef(
cards = false,
visibleColumns = {},
masterFilter = {},
size = 'middle',
onStateChange
size = 'middle'
},
ref
) => {
const { token } = useContext(AuthContext)
const { isElectron } = useContext(ElectronContext)
const onStateChangeRef = useRef(onStateChange)
useEffect(() => {
onStateChangeRef.current = onStateChange
}, [onStateChange])
const {
fetchObjects,
connected,
subscribeToObjectUpdates,
subscribeToObjectTypeUpdates,
updateMultipleObjects,
lockObject,
unlockObject
subscribeToObjectTypeUpdates
} = useContext(ApiServerContext)
const isMobile = useMediaQuery({ maxWidth: 768 })
const navigate = useNavigate()
@ -149,21 +98,6 @@ const ObjectTable = forwardRef(
const [lazyLoading, setLazyLoading] = useState(false)
const [tableData, setTableData] = useState([])
const [isEditing, setIsEditing] = useState(false)
const [editLoading, setEditLoading] = useState(false)
const rowFormsRef = useRef({})
const registerForm = useCallback((id, form) => {
if (form) {
rowFormsRef.current[id] = form
} else {
delete rowFormsRef.current[id]
}
}, [])
useEffect(() => {
onStateChangeRef.current?.({ isEditing, editLoading })
}, [isEditing, editLoading])
const subscribedIdsRef = useRef([])
// const [typeSubscribed, setTypeSubscribed] = useState(false)
const unsubscribesRef = useRef([])
@ -366,49 +300,6 @@ const ObjectTable = forwardRef(
}
}, [fetchData])
const startEditing = useCallback(() => {
setIsEditing(true)
tableData.forEach((item) => {
if (!item.isSkeleton) {
console.log('Locking object:', item)
lockObject(item._id, type)
}
})
}, [tableData, lockObject, type])
const cancelEditing = useCallback(() => {
setIsEditing(false)
tableData.forEach((item) => {
if (!item.isSkeleton) {
unlockObject(item._id, type)
}
})
}, [tableData, unlockObject, type])
const handleUpdate = useCallback(async () => {
setEditLoading(true)
try {
const updates = await Promise.all(
Object.entries(rowFormsRef.current).map(async ([id, form]) => {
const values = await form.validateFields()
return { _id: id, ...values }
})
)
await updateMultipleObjects(type, updates)
setIsEditing(false)
reload()
tableData.forEach((item) => {
if (!item.isSkeleton) {
unlockObject(item._id, type)
}
})
} catch (err) {
console.error(err)
} finally {
setEditLoading(false)
}
}, [type, updateMultipleObjects, reload, tableData, unlockObject])
// Update event handler for real-time updates
const updateEventHandler = useCallback((id, updatedData) => {
setPages((prevPages) =>
@ -425,12 +316,6 @@ const ObjectTable = forwardRef(
}
})
)
console.log('updatedData', updatedData)
if (rowFormsRef.current[id]) {
rowFormsRef.current[id].setFieldsValue(updatedData)
}
}, [])
// Store the latest updateEventHandler in a ref
@ -464,9 +349,6 @@ const ObjectTable = forwardRef(
updateEventHandlerRef.current(itemId, updateData)
}
)
console.log('unsubscribe', unsubscribe)
console.log('subscribedIdsRef', subscribedIdsRef.current)
console.log('unsubscribesRef', unsubscribesRef.current)
subscribedIdsRef.current.push(itemId)
if (unsubscribe) {
unsubscribesRef.current.push(unsubscribe)
@ -526,8 +408,8 @@ const ObjectTable = forwardRef(
}, [type, subscribeToObjectTypeUpdates, connected, newEventHandler])
const updateData = useCallback(
(id, updatedData) => {
updateEventHandler(id, updatedData)
(_id, updatedData) => {
updateEventHandler({ _id, ...updatedData })
},
[updateEventHandler]
)
@ -563,12 +445,7 @@ const ObjectTable = forwardRef(
setData: (newData) => {
setPages([{ pageNum: 1, items: newData }])
},
updateData,
startEditing,
cancelEditing,
handleUpdate,
isEditing,
editLoading
updateData
}))
useEffect(() => {
@ -737,7 +614,7 @@ const ObjectTable = forwardRef(
{...prop}
longId={false}
objectData={record}
isEditing={isEditing}
isEditing={false}
/>
)
}
@ -838,71 +715,61 @@ const ObjectTable = forwardRef(
loading={record.isSkeleton}
variant={'borderless'}
>
<RowForm
record={record}
isEditing={isEditing}
onRegister={registerForm}
>
<Flex align={'center'} vertical gap={'middle'}>
<Descriptions
column={1}
size='small'
bordered={true}
style={{ width: '100%', tableLayout: 'fixed' }}
className='objectTableDescritions'
>
{(() => {
const descriptionItems = []
<Flex align={'center'} vertical gap={'middle'}>
<Descriptions
column={1}
size='small'
bordered={true}
style={{ width: '100%', tableLayout: 'fixed' }}
className='objectTableDescritions'
>
{(() => {
const descriptionItems = []
// Add columns in the order specified by model.columns (same logic as table)
model.columns.forEach((colName) => {
const prop = modelProperties.find(
(p) => p.name === colName
)
if (prop) {
// Check if column should be visible based on visibleColumns prop
if (
Object.keys(visibleColumns).length > 0 &&
visibleColumns[prop.name] === false
) {
return // Skip this column if it's not visible
}
descriptionItems.push(
<Descriptions.Item
label={prop.label}
key={prop.name}
colspan={2}
>
<ObjectProperty
{...prop}
longId={false}
objectData={record}
isEditing={isEditing}
name={prop.name}
/>
</Descriptions.Item>
)
// Add columns in the order specified by model.columns (same logic as table)
model.columns.forEach((colName) => {
const prop = modelProperties.find(
(p) => p.name === colName
)
if (prop) {
// Check if column should be visible based on visibleColumns prop
if (
Object.keys(visibleColumns).length > 0 &&
visibleColumns[prop.name] === false
) {
return // Skip this column if it's not visible
}
})
// Add actions if they exist (same as table)
if (rowActions.length > 0) {
descriptionItems.push(
<Descriptions.Item
label={'Actions'}
key={'actions'}
label={prop.label}
key={prop.name}
colspan={2}
>
{renderActions(record)}
<ObjectProperty
{...prop}
longId={false}
objectData={record}
isEditing={false}
/>
</Descriptions.Item>
)
}
})
return descriptionItems
})()}
</Descriptions>
</Flex>
</RowForm>
// Add actions if they exist (same as table)
if (rowActions.length > 0) {
descriptionItems.push(
<Descriptions.Item label={'Actions'} key={'actions'}>
{renderActions(record)}
</Descriptions.Item>
)
}
return descriptionItems
})()}
</Descriptions>
</Flex>
</Card>
</Col>
))}
@ -910,52 +777,32 @@ const ObjectTable = forwardRef(
)
}
const components = useMemo(
() => ({
body: {
row: EditableRow
}
}),
[]
return (
<>
<Flex gap={'middle'} vertical>
<Table
ref={tableRef}
dataSource={tableData}
columns={columnsWithSkeleton}
className={cards ? 'dashboard-cards-header' : 'dashboard-table'}
pagination={false}
scroll={{ y: adjustedScrollHeight }}
rowKey='_id'
loading={{ spinning: loading, indicator: <LoadingOutlined spin /> }}
onScroll={handleScroll}
onChange={handleTableChange}
showSorterTooltip={false}
style={{ height: '100%' }}
size={size}
/>
{cards ? (
<Spin indicator={<LoadingOutlined />} spinning={loading}>
{renderCards()}
</Spin>
) : null}
</Flex>
</>
)
const onRow = useCallback(
(record) => ({
record,
isEditing,
onRegister: registerForm
}),
[isEditing, registerForm]
)
const tableContent = (
<Flex gap={'middle'} vertical>
<Table
ref={tableRef}
dataSource={tableData}
columns={columnsWithSkeleton}
className={cards ? 'dashboard-cards-header' : 'dashboard-table'}
pagination={false}
scroll={{ y: adjustedScrollHeight }}
rowKey='_id'
loading={{ spinning: loading, indicator: <LoadingOutlined spin /> }}
onScroll={handleScroll}
onChange={handleTableChange}
showSorterTooltip={false}
style={{ height: '100%' }}
size={size}
components={components}
onRow={onRow}
/>
{cards ? (
<Spin indicator={<LoadingOutlined />} spinning={loading}>
{renderCards()}
</Spin>
) : null}
</Flex>
)
return tableContent
}
)
@ -971,8 +818,7 @@ ObjectTable.propTypes = {
cardRenderer: PropTypes.func,
visibleColumns: PropTypes.object,
masterFilter: PropTypes.object,
size: PropTypes.string,
onStateChange: PropTypes.func
size: PropTypes.string
}
export default ObjectTable

View File

@ -96,46 +96,6 @@ const StateTag = ({ state, showBadge = true, style = {} }) => {
status = 'success'
text = 'Unconsumed'
break
case 'sent':
status = 'cyan'
text = 'Sent'
break
case 'acknowledged':
status = 'processing'
text = 'Acknowledged'
break
case 'ordered':
status = 'cyan'
text = 'Ordered'
break
case 'received':
status = 'success'
text = 'Received'
break
case 'invoiced':
status = 'warning'
text = 'Invoiced'
break
case 'planned':
status = 'warning'
text = 'Planned'
break
case 'partiallyShipped':
status = 'processing'
text = 'Partially Shipped'
break
case 'shipped':
status = 'processing'
text = 'Shipped'
break
case 'delivered':
status = 'success'
text = 'Delivered'
break
case 'paid':
status = 'success'
text = 'Paid'
break
default:
status = 'default'
text = state || 'Unknown'
@ -144,22 +104,10 @@ const StateTag = ({ state, showBadge = true, style = {} }) => {
return { badgeStatus: status, badgeText: text }
}, [state])
var badgeProps = {
status: badgeStatus
}
if (
!['success', 'warning', 'error', 'processing', 'default'].includes(
badgeStatus
)
) {
badgeProps = { color: badgeStatus }
}
return (
<Tag color={badgeStatus} style={{ marginRight: 0, ...style }}>
<Flex gap={6}>
{showBadge && <Badge {...badgeProps} />}
{showBadge && <Badge status={badgeStatus} />}
{badgeText}
</Flex>
</Tag>

View File

@ -1,5 +1,5 @@
import { useEffect, useState, useContext, useRef } from 'react'
import { Flex, Tag, Card, Typography, Skeleton, Badge } from 'antd'
import { Flex, Alert, Card, Typography, Skeleton } from 'antd'
import PropTypes from 'prop-types'
import { getModelByName } from '../../../database/ObjectModels'
import { ApiServerContext } from '../context/ApiServerContext'
@ -9,9 +9,9 @@ import { round } from '../utils/Utils'
const { Text } = Typography
/**
* Maps stat names to Tag colors for visual indication
* Maps stat names to Alert types for visual indication
*/
const getTagColor = (statName) => {
const getAlertType = (statName) => {
const name = statName.toLowerCase()
// Success states
@ -43,14 +43,14 @@ const getTagColor = (statName) => {
}
if (name.includes('printing')) {
return 'processing'
return 'info'
}
// Default states
return 'default'
}
/*i*
/**
* Gets a nested value from an object using dot notation
* e.g., getNestedValue(obj, 'states.ready') -> obj.states.ready
*/
@ -255,41 +255,18 @@ const StatsDisplay = ({ objectType, stats: statsProp }) => {
{modelStats.map((statDef) => {
const baseStatName = extractBaseStatName(statDef.name)
var statValue = getStatValue(stats, baseStatName, statDef)
const tagColor = statDef.color || getTagColor(statDef.name)
const alertType = getAlertType(statDef.name)
const label = statDef.label || statDef.name
if (statDef?.roundNumber) {
statValue = round(statValue, statDef?.roundNumber)
}
const statusColors = [
'success',
'warning',
'error',
'processing',
'default'
]
var badgeProps = {
status: tagColor
}
if (!statusColors.includes(tagColor)) {
badgeProps = { color: tagColor }
}
const content = (
<Flex vertical gap='3px'>
<Flex gap={'12px'} align='center'>
<Badge {...badgeProps} />
<Text>{label}</Text>
</Flex>
<Flex gap={'12px'} align='center'>
{Icon && (
<Text>
<Icon style={{ fontSize: 26 }} />
</Text>
)}
<Flex vertical>
<Text type='secondary'>{label}</Text>
<Flex gap={'small'}>
{Icon && <Icon style={{ fontSize: 26 }} />}
{loading ? (
<Flex justify='center' align='center' style={{ height: 44 }}>
<Skeleton.Button
@ -309,12 +286,12 @@ const StatsDisplay = ({ objectType, stats: statsProp }) => {
</Flex>
)
if (tagColor === 'default') {
if (alertType === 'default') {
return (
<Card
key={statDef.name}
style={{ minWidth: statDef?.cardWidth || 175 }}
styles={{ body: { padding: '16px 24px' } }}
styles={{ body: { padding: '20px 24px' } }}
>
{content}
</Card>
@ -322,17 +299,12 @@ const StatsDisplay = ({ objectType, stats: statsProp }) => {
}
return (
<Tag
<Alert
key={statDef.name}
color={tagColor}
style={{
minWidth: statDef?.cardWidth || 175,
padding: '16px 24px',
margin: 0
}}
>
{content}
</Tag>
type={alertType}
style={{ minWidth: statDef?.cardWidth || 175 }}
description={content}
/>
)
})}
</Flex>

View File

@ -797,28 +797,6 @@ const ApiServerProvider = ({ children }) => {
}
}
// Update multiple objects
const updateMultipleObjects = async (type, objects) => {
const updateUrl = `${config.backendUrl}/${type.toLowerCase()}s`
logger.debug('Updating multiple objects for ' + type)
try {
const response = await axios.put(updateUrl, objects, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
})
logger.debug('Objects updated successfully')
return response.data
} catch (err) {
console.error(err)
showError(err, () => {
updateMultipleObjects(type, objects)
})
return []
}
}
// Update filament information
const deleteObject = async (id, type) => {
const deleteUrl = `${config.backendUrl}/${type.toLowerCase()}s/${id}`
@ -862,27 +840,6 @@ const ApiServerProvider = ({ children }) => {
}
}
// Call a function on an object
const sendObjectFunction = async (id, type, functionName, value = {}) => {
const url = `${config.backendUrl}/${type.toLowerCase()}s/${id}/${functionName}`
logger.debug(`Calling object function ${functionName} for ${id} at ${url}`)
try {
const response = await axios.post(url, value, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
})
return response.data
} catch (err) {
console.error(err)
showError(err, () => {
sendObjectFunction(id, type, functionName, value)
})
return {}
}
}
// Download GCode file content
const fetchFileContent = async (file, download = false) => {
try {
@ -1264,9 +1221,7 @@ const ApiServerProvider = ({ children }) => {
unlockObject,
fetchObjectLock,
updateObject,
updateMultipleObjects,
createObject,
sendObjectFunction,
deleteObject,
subscribeToObjectUpdates,
subscribeToObjectEvent,

View File

@ -88,12 +88,7 @@ export const ThemeProvider = ({ children }) => {
colorWarning: '#FF9230',
colorError: '#FF4245',
colorInfo: '#0A84FF',
colorLink: '#5AC8F5',
colorCyan: '#5AC8F5',
colorPink: '#FF69B4',
colorPurple: '#800080',
colorMagenta: '#FF00FF',
colorVolcano: '#FF4500'
colorLink: '#5AC8F5'
}
const getColors = () => {

View File

@ -1,6 +0,0 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/financeicon.svg?react'
const FinanceIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default FinanceIcon

View File

@ -1,9 +0,0 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/invoiceicon.svg?react'
const InvoiceIcon = (props) => (
<Icon component={CustomIconSvg} {...props} />
)
export default InvoiceIcon

View File

@ -30,7 +30,6 @@ import { DocumentPrinter } from './models/DocumentPrinter.js'
import { DocumentJob } from './models/DocumentJob.js'
import { TaxRate } from './models/TaxRate.js'
import { TaxRecord } from './models/TaxRecord.js'
import { Invoice } from './models/Invoice.js'
import QuestionCircleIcon from '../components/Icons/QuestionCircleIcon'
export const objectModels = [
@ -65,8 +64,7 @@ export const objectModels = [
DocumentPrinter,
DocumentJob,
TaxRate,
TaxRecord,
Invoice
TaxRecord
]
// Re-export individual models for direct access
@ -102,8 +100,7 @@ export {
DocumentPrinter,
DocumentJob,
TaxRate,
TaxRecord,
Invoice
TaxRecord
}
export function getModelByName(name, ignoreCase = false) {

View File

@ -1,336 +0,0 @@
import InvoiceIcon from '../../components/Icons/InvoiceIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import EditIcon from '../../components/Icons/EditIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
import BinIcon from '../../components/Icons/BinIcon'
export const Invoice = {
name: 'invoice',
label: 'Invoice',
prefix: 'INV',
icon: InvoiceIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/finance/invoices/info?invoiceId=${_id}`
},
{
name: 'edit',
label: 'Edit',
type: 'button',
icon: EditIcon,
url: (_id) =>
`/dashboard/finance/invoices/info?invoiceId=${_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/finance/invoices/info?invoiceId=${_id}&action=cancelEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'finishEdit',
label: 'Finish Edit',
type: 'button',
icon: CheckIcon,
url: (_id) =>
`/dashboard/finance/invoices/info?invoiceId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'delete',
label: 'Delete',
type: 'button',
icon: BinIcon,
danger: true,
url: (_id) =>
`/dashboard/finance/invoices/info?invoiceId=${_id}&action=delete`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
},
disabled: (objectData) => {
return objectData?.state?.type != 'draft'
}
},
{ type: 'divider' },
{
name: 'send',
label: 'Send',
type: 'button',
icon: CheckIcon,
url: (_id) =>
`/dashboard/finance/invoices/info?invoiceId=${_id}&action=send`,
visible: (objectData) => {
return objectData?.state?.type == 'draft'
}
},
{
name: 'markPaid',
label: 'Mark Paid',
type: 'button',
icon: CheckIcon,
url: (_id) =>
`/dashboard/finance/invoices/info?invoiceId=${_id}&action=markPaid`,
visible: (objectData) => {
return (
objectData?.state?.type == 'sent' ||
objectData?.state?.type == 'partiallyPaid'
)
}
},
{
name: 'cancel',
label: 'Cancel',
type: 'button',
icon: XMarkIcon,
danger: true,
url: (_id) =>
`/dashboard/finance/invoices/info?invoiceId=${_id}&action=cancel`,
disabled: (objectData) => {
return (
objectData?.state?.type == 'cancelled' ||
objectData?.state?.type == 'paid'
)
},
visible: (objectData) => {
return (
objectData?.state?.type == 'draft' ||
objectData?.state?.type == 'sent'
)
}
}
],
group: ['vendor', 'customer', 'invoiceType'],
filters: ['vendor', 'customer', 'invoiceType'],
sorters: ['createdAt', 'state', 'updatedAt', 'invoiceDate', 'dueDate'],
columns: [
'_id',
'_reference',
'state',
'invoiceType',
'vendor',
'customer',
'invoiceDate',
'dueDate',
'totalAmount',
'totalAmountWithTax',
'totalTaxAmount',
'shippingAmount',
'shippingAmountWithTax',
'grandTotalAmount',
'createdAt',
'updatedAt'
],
properties: [
{
name: '_id',
label: 'ID',
type: 'id',
columnFixed: 'left',
objectType: 'invoice',
columnWidth: 140,
showCopy: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
{
name: '_reference',
label: 'Reference',
type: 'reference',
required: true,
objectType: 'invoice',
showCopy: true,
readOnly: true
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true
},
{ name: 'state', label: 'State', type: 'state', readOnly: true },
{
name: 'invoiceDate',
label: 'Invoice Date',
type: 'date',
readOnly: false
},
{
name: 'dueDate',
label: 'Due Date',
type: 'date',
readOnly: false
},
{
name: 'vendor',
label: 'Vendor',
required: true,
type: 'object',
objectType: 'vendor',
showHyperlink: true,
visible: (objectData) => {
return objectData?.invoiceType === 'purchase' || objectData?.vendor
}
},
{
name: 'orderType',
label: 'Order Type',
type: 'objectType',
masterFilter: ['purchaseOrder', 'salesOrder'],
required: true
},
{
name: 'order',
label: 'Order',
type: 'object',
objectType: (objectData) => {
return objectData?.orderType
},
masterFilter: (objectData) => {
return {
vendor: objectData?.vendor?._id
}
},
required: true,
showHyperlink: true
},
{
name: 'sentAt',
label: 'Sent At',
type: 'dateTime',
readOnly: true
},
{
name: 'paidAt',
label: 'Paid At',
type: 'dateTime',
readOnly: true
},
{
name: 'cancelledAt',
label: 'Cancelled At',
type: 'dateTime',
readOnly: true
},
{
name: 'overdueAt',
label: 'Overdue At',
type: 'dateTime',
readOnly: true
},
{
name: 'totalTaxAmount',
label: 'Total Tax Amount',
type: 'number',
prefix: '£',
roundNumber: 2,
readOnly: true,
columnWidth: 175
},
{
name: 'totalAmountWithTax',
label: 'Total Amount w/ Tax',
type: 'number',
prefix: '£',
readOnly: true,
columnWidth: 175,
roundNumber: 2
},
{
name: 'shippingAmount',
label: 'Shipping Amount',
type: 'number',
prefix: '£',
roundNumber: 2,
readOnly: true,
columnWidth: 150
},
{
name: 'shippingAmountWithTax',
label: 'Shipping Amount w/ Tax',
type: 'number',
prefix: '£',
readOnly: true,
roundNumber: 2,
columnWidth: 200
},
{
name: 'totalAmount',
label: 'Total Amount',
type: 'number',
prefix: '£',
roundNumber: 2,
readOnly: true,
columnWidth: 150
},
{
name: 'grandTotalAmount',
label: 'Grand Total Amount',
type: 'number',
prefix: '£',
roundNumber: 2,
columnWidth: 175,
readOnly: true
}
],
stats: [
{
name: 'draft.count',
label: 'Draft',
type: 'number',
color: 'default'
},
{
name: 'sent.count',
label: 'Sent',
type: 'number',
color: 'cyan'
},
{
name: 'partiallyPaid.count',
label: 'Partially Paid',
type: 'number',
color: 'processing'
},
{
name: 'paid.count',
label: 'Paid',
type: 'number',
color: 'success'
},
{
name: 'overdue.count',
label: 'Overdue',
type: 'number',
color: 'error'
},
{
name: 'cancelled.count',
label: 'Cancelled',
type: 'number',
color: 'default'
}
]
}

View File

@ -155,7 +155,7 @@ export const Job = {
name: 'printing.count',
label: 'Printing',
type: 'number',
color: 'processing'
color: 'info'
},
{
name: 'failed.count',

View File

@ -1,9 +1,5 @@
import OrderItemIcon from '../../components/Icons/OrderItemIcon'
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 OrderItem = {
name: 'orderItem',
@ -18,63 +14,6 @@ export const OrderItem = {
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/inventory/orderitems/info?orderItemId=${_id}`
},
{
name: 'edit',
label: 'Edit',
type: 'button',
icon: EditIcon,
url: (_id) =>
`/dashboard/inventory/orderitems/info?orderItemId=${_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/orderitems/info?orderItemId=${_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/orderitems/info?orderItemId=${_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/orderitems/info?orderItemId=${_id}&action=delete`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
},
disabled: (objectData) => {
return objectData?.state?.type != 'draft'
}
}
],
group: [],
@ -82,8 +21,7 @@ export const OrderItem = {
sorters: ['createdAt', 'updatedAt', 'itemAmount', 'quantity'],
columns: [
'_id',
'_reference',
'state',
'itemType',
'item',
'itemAmount',
@ -92,7 +30,6 @@ export const OrderItem = {
'taxRate',
'totalAmountWithTax',
'order',
'shipment',
'createdAt',
'updatedAt'
],
@ -151,16 +88,7 @@ export const OrderItem = {
type: 'object',
objectType: 'shipment',
showHyperlink: true,
required: false,
columnWidth: 250,
readOnly: (objectData) => {
return objectData?.state?.type != 'draft'
},
masterFilter: (objectData) => {
return {
order: objectData?.order?._id
}
}
required: true
},
{
name: 'itemType',
@ -168,7 +96,7 @@ export const OrderItem = {
type: 'objectType',
masterFilter: ['part', 'packaging', 'filament'],
required: true,
columnWidth: 175
columnWidth: 125
},
{
name: 'item',

View File

@ -378,7 +378,7 @@ export const Printer = {
name: 'printing.count',
label: 'Printing',
type: 'number',
color: 'processing'
color: 'info'
},
{
name: 'error.count',

View File

@ -1,10 +1,6 @@
import PurchaseOrderIcon from '../../components/Icons/PurchaseOrderIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import PlusIcon from '../../components/Icons/PlusIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import EditIcon from '../../components/Icons/EditIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
import BinIcon from '../../components/Icons/BinIcon'
export const PurchaseOrder = {
name: 'purchaseOrder',
@ -21,168 +17,19 @@ export const PurchaseOrder = {
url: (_id) =>
`/dashboard/inventory/purchaseorders/info?purchaseOrderId=${_id}`
},
{
name: 'edit',
label: 'Edit',
type: 'button',
icon: EditIcon,
url: (_id) =>
`/dashboard/inventory/purchaseorders/info?purchaseOrderId=${_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/purchaseorders/info?purchaseOrderId=${_id}&action=cancelEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'finishEdit',
label: 'Finish Edit',
type: 'button',
icon: CheckIcon,
url: (_id) =>
`/dashboard/inventory/purchaseorders/info?purchaseOrderId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'delete',
label: 'Delete',
type: 'button',
icon: BinIcon,
danger: true,
url: (_id) =>
`/dashboard/inventory/purchaseorders/info?purchaseOrderId=${_id}&action=delete`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
},
disabled: (objectData) => {
return objectData?.state?.type != 'draft'
}
},
{ type: 'divider' },
{
name: 'New Order Item',
label: 'New Order Item',
type: 'button',
icon: PlusIcon,
url: (_id) =>
`/dashboard/inventory/purchaseorders/info?purchaseOrderId=${_id}&action=newOrderItem`,
disabled: (objectData) => {
return objectData?.state?.type != 'draft'
}
},
{
name: 'New Shipment',
label: 'New Shipment',
type: 'button',
icon: PlusIcon,
url: (_id) =>
`/dashboard/inventory/purchaseorders/info?purchaseOrderId=${_id}&action=newShipment`,
disabled: (objectData) => {
return objectData?.state?.type != 'draft'
}
},
{
name: 'New Invoice',
label: 'New Invoice',
type: 'button',
icon: PlusIcon,
url: (_id) =>
`/dashboard/inventory/purchaseorders/info?purchaseOrderId=${_id}&action=newInvoice`,
disabled: (objectData) => {
return objectData?.state?.type != 'received'
}
},
{
type: 'divider'
},
{
name: 'post',
label: 'Post',
type: 'button',
icon: CheckIcon,
url: (_id) =>
`/dashboard/inventory/purchaseorders/info?purchaseOrderId=${_id}&action=post`,
visible: (objectData) => {
return objectData?.state?.type == 'draft'
}
},
{
name: 'acknowledge',
label: 'Acknowledge',
type: 'button',
icon: CheckIcon,
url: (_id) =>
`/dashboard/inventory/purchaseorders/info?purchaseOrderId=${_id}&action=acknowledge`,
visible: (objectData) => {
return objectData?.state?.type == 'sent'
}
},
{
name: 'complete',
label: 'Complete',
type: 'button',
icon: CheckIcon,
url: (_id) =>
`/dashboard/inventory/shipments/info?shipmentId=${_id}&action=complete`,
disabled: (objectData) => {
return objectData?.state?.type != 'received'
},
visible: (objectData) => {
return objectData?.state?.type == 'received'
}
},
{
name: 'cancel',
label: 'Cancel',
type: 'button',
icon: XMarkIcon,
danger: true,
url: (_id) =>
`/dashboard/inventory/purchaseorders/info?purchaseOrderId=${_id}&action=cancel`,
disabled: (objectData) => {
return objectData?.state?.type == 'cancelled'
},
visible: (objectData) => {
return (
objectData?.state?.type != 'draft' &&
objectData?.state?.type != 'completed' &&
objectData?.state?.type != 'received'
)
}
`/dashboard/inventory/purchaseorders/info?purchaseOrderId=${_id}&action=newOrderItem`
}
],
group: ['vendor'],
filters: ['vendor'],
sorters: ['createdAt', 'state', 'updatedAt'],
columns: [
'_id',
'_reference',
'state',
'vendor',
'totalAmount',
'totalAmountWithTax',
'totalTaxAmount',
'shippingAmount',
'shippingAmountWithTax',
'grandTotalAmount',
'createdAt',
'updatedAt',
'vendor'
],
columns: ['_id', 'createdAt', 'state', 'updatedAt', 'vendor'],
properties: [
{
name: '_id',
@ -215,7 +62,6 @@ export const PurchaseOrder = {
readOnly: true
},
{ name: 'state', label: 'State', type: 'state', readOnly: true },
{ name: 'postedAt', label: 'Posted At', type: 'dateTime', readOnly: true },
{
name: 'vendor',
label: 'Vendor',
@ -225,24 +71,10 @@ export const PurchaseOrder = {
showHyperlink: true
},
{
name: 'acknowledgedAt',
label: 'Acknowledged At',
type: 'dateTime',
readOnly: true
},
{
name: 'totalTaxAmount',
label: 'Total Tax Amount',
name: 'totalAmount',
label: 'Total Amount',
type: 'number',
prefix: '£',
roundNumber: 2,
readOnly: true,
columnWidth: 175
},
{
name: 'CompletedAt',
label: 'Completed At',
type: 'dateTime',
readOnly: true
},
{
@ -250,89 +82,14 @@ export const PurchaseOrder = {
label: 'Total Amount w/ Tax',
type: 'number',
prefix: '£',
readOnly: true,
columnWidth: 175,
roundNumber: 2
},
{
name: 'shippingAmount',
label: 'Shipping Amount',
type: 'number',
prefix: '£',
roundNumber: 2,
readOnly: true,
columnWidth: 150
},
{
name: 'shippingAmountWithTax',
label: 'Shipping Amount w/ Tax',
type: 'number',
prefix: '£',
readOnly: true,
roundNumber: 2,
columnWidth: 200
},
{
name: 'totalAmount',
label: 'Total Amount',
type: 'number',
prefix: '£',
roundNumber: 2,
readOnly: true,
columnWidth: 150
},
{
name: 'grandTotalAmount',
label: 'Grand Total Amount',
type: 'number',
prefix: '£',
roundNumber: 2,
columnWidth: 175,
readOnly: true
}
],
stats: [
{
name: 'draft.count',
label: 'Draft',
type: 'number',
color: 'default'
},
{
name: 'sent.count',
label: 'Sent',
name: 'totalTaxAmount',
label: 'Total Tax Amount',
type: 'number',
color: 'cyan'
},
{
name: 'acknowledged.count',
label: 'Acknowledged',
type: 'number',
color: 'processing'
},
{
name: 'partiallyShipped.count',
label: 'Partially Shipped',
type: 'number',
color: 'processing'
},
{
name: 'shipped.count',
label: 'Shipped',
type: 'number',
color: 'processing'
},
{
name: 'partiallyReceived.count',
label: 'Partially Received',
type: 'number',
color: 'success'
},
{
name: 'received.count',
label: 'Received',
type: 'number',
color: 'success'
prefix: '£',
readOnly: true
}
]
}

View File

@ -1,14 +1,10 @@
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: 'SHP',
prefix: 'SHM',
icon: ShipmentIcon,
actions: [
{
@ -18,126 +14,25 @@ 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: [],
filters: ['orderType', 'order', 'state', 'courierService'],
group: ['vendor', 'purchaseOrder'],
filters: ['vendor', 'purchaseOrder', 'state', 'courierService'],
sorters: [
'createdAt',
'state',
'updatedAt',
'shippedAt',
'expectedAt',
'deliveredAt'
'shippedDate',
'expectedDeliveryDate'
],
columns: [
'_id',
'_reference',
'createdAt',
'state',
'orderType',
'order',
'amount',
'amountWithTax',
'taxRate',
'taxAmount',
'trackingNumber',
'shippedAt',
'expectedAt',
'deliveredAt',
'updatedAt',
'createdAt'
'vendor',
'purchaseOrder',
'trackingNumber'
],
properties: [
{
@ -155,139 +50,231 @@ export const Shipment = {
type: 'dateTime',
readOnly: true
},
{
name: '_reference',
label: 'Reference',
type: 'reference',
objectType: 'shipment',
showCopy: true,
readOnly: true
},
{ name: 'state', label: 'State', type: 'state', readOnly: true },
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true
},
{ name: 'state', label: 'State', type: 'state', readOnly: true },
{
name: 'shippedAt',
label: 'Shipped At',
type: 'dateTime',
required: false
},
{
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',
name: 'purchaseOrder',
label: 'Purchase Order',
required: true,
type: 'object',
showHyperlink: true,
objectType: (objectData) => {
return objectData?.orderType
}
objectType: 'purchaseOrder',
showHyperlink: true
},
{
name: 'deliveredAt',
label: 'Delivered At',
type: 'dateTime',
required: false
name: 'vendor',
label: 'Vendor',
required: true,
type: 'object',
objectType: 'vendor',
showHyperlink: true
},
{
name: 'courierService',
label: 'Courier Service',
required: true,
required: false,
type: 'object',
objectType: 'courierService'
},
{
name: 'trackingNumber',
label: 'Tracking Number',
type: 'text',
type: 'string',
required: false
},
{
name: 'taxRate',
label: 'Tax Rate',
type: 'object',
objectType: 'taxRate',
showHyperlink: true
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
}
}
},
{
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: 'taxRecord',
label: 'Tax Record',
type: 'object',
objectType: 'taxRecord',
showHyperlink: true
},
{
name: 'amount',
label: 'Amount',
type: 'number',
name: 'cost',
label: 'Cost',
type: 'netGross',
required: 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
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: 'amountWithTax',
label: 'Amount w/ Tax',
type: 'number',
required: true,
prefix: '£',
min: 0,
step: 0.01,
readOnly: true,
columnWidth: 175,
value: (objectData) => {
if (objectData?._isEditing == true) {
return (
(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)
}
return 0
}
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
}
]
}

View File

@ -1,29 +0,0 @@
import { lazy } from 'react'
import { Route } from 'react-router-dom'
const Invoices = lazy(
() => import('../components/Dashboard/Finance/Invoices.jsx')
)
const InvoiceInfo = lazy(
() => import('../components/Dashboard/Finance/Invoices/InvoiceInfo.jsx')
)
const FinanceOverview = lazy(
() => import('../components/Dashboard/Finance/FinanceOverview.jsx')
)
const FinanceRoutes = [
<Route
key='overview'
path='finance/overview'
element={<FinanceOverview />}
/>,
<Route key='invoices' path='finance/invoices' element={<Invoices />} />,
<Route
key='invoices-info'
path='finance/invoices/info'
element={<InvoiceInfo />}
/>
]
export default FinanceRoutes

View File

@ -1,5 +1,4 @@
export { default as ProductionRoutes } from './ProductionRoutes'
export { default as InventoryRoutes } from './InventoryRoutes'
export { default as FinanceRoutes } from './FinanceRoutes'
export { default as ManagementRoutes } from './ManagementRoutes'
export { default as DeveloperRoutes } from './DeveloperRoutes'