This commit is contained in:
Tom Butcher 2025-12-03 03:42:15 +00:00
commit 8ad9a777a4
95 changed files with 5806 additions and 1902 deletions

View File

@ -318,3 +318,35 @@ body {
.ant-badge.ant-badge-status { .ant-badge.ant-badge-status {
line-height: 18.5px; line-height: 18.5px;
} }
.simplebar-track.simplebar-vertical {
right: -16px;
width: 8px !important;
}
.simplebar-scrollbar:before {
background: #78787854 !important;
}
.printer-alerts-display-popover .ant-popover-inner {
padding: 0 !important;
margin: 0 24px !important;
}
.child-table-rollups *::-webkit-scrollbar:horizontal {
height: 0px;
}
.rollup-table .ant-table-container {
border-start-start-radius: 0px !important;
border-start-end-radius: 0px !important;
}
.rollup-table .ant-table {
border-radius: 0px !important;
}
.ant-select-selection-item .ant-tag,
.ant-select-tree-title .ant-tag {
background: transparent !important;
}

View File

@ -59,6 +59,7 @@
"react-responsive": "^10.0.1", "react-responsive": "^10.0.1",
"react-router-dom": "^7.8.2", "react-router-dom": "^7.8.2",
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"simplebar-react": "^3.3.2",
"socket.io-client": "*", "socket.io-client": "*",
"standard": "^17.1.2", "standard": "^17.1.2",
"styled-components": "^6.1.19", "styled-components": "^6.1.19",

View File

@ -13,6 +13,7 @@ import '../assets/stylesheets/App.css'
import { PrintServerProvider } from './components/Dashboard/context/PrintServerContext.jsx' import { PrintServerProvider } from './components/Dashboard/context/PrintServerContext.jsx'
import { AuthProvider } from './components/Dashboard/context/AuthContext.jsx' import { AuthProvider } from './components/Dashboard/context/AuthContext.jsx'
import { SpotlightProvider } from './components/Dashboard/context/SpotlightContext.jsx' import { SpotlightProvider } from './components/Dashboard/context/SpotlightContext.jsx'
import { ActionsModalProvider } from './components/Dashboard/context/ActionsModalContext.jsx'
import { import {
ThemeProvider, ThemeProvider,
@ -21,6 +22,7 @@ import {
import AppError from './components/App/AppError' import AppError from './components/App/AppError'
import { ApiServerProvider } from './components/Dashboard/context/ApiServerContext.jsx' import { ApiServerProvider } from './components/Dashboard/context/ApiServerContext.jsx'
import { ElectronProvider } from './components/Dashboard/context/ElectronContext.jsx' import { ElectronProvider } from './components/Dashboard/context/ElectronContext.jsx'
import { MessageProvider } from './components/Dashboard/context/MessageContext.jsx'
import AuthCallback from './components/App/AuthCallback.jsx' import AuthCallback from './components/App/AuthCallback.jsx'
import { import {
@ -53,6 +55,8 @@ const AppContent = () => {
<PrintServerProvider> <PrintServerProvider>
<ApiServerProvider> <ApiServerProvider>
<SpotlightProvider> <SpotlightProvider>
<ActionsModalProvider>
<MessageProvider>
<Routes> <Routes>
<Route <Route
path='/' path='/'
@ -67,7 +71,10 @@ const AppContent = () => {
/> />
} }
/> />
<Route path='/auth/callback' element={<AuthCallback />} /> <Route
path='/auth/callback'
element={<AuthCallback />}
/>
<Route <Route
path='/dashboard' path='/dashboard'
element={ element={
@ -89,6 +96,8 @@ const AppContent = () => {
} }
/> />
</Routes> </Routes>
</MessageProvider>
</ActionsModalProvider>
</SpotlightProvider> </SpotlightProvider>
</ApiServerProvider> </ApiServerProvider>
</PrintServerProvider> </PrintServerProvider>

View File

@ -34,9 +34,15 @@ const routeKeyMap = {
const DeveloperSidebar = (props) => { const DeveloperSidebar = (props) => {
const location = useLocation() const location = useLocation()
const selectedKey = (() => { const selectedKey = (() => {
const match = Object.keys(routeKeyMap).find((path) => const match = Object.keys(routeKeyMap).find((path) => {
location.pathname.startsWith(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] : 'sessionstorage' return match ? routeKeyMap[match] : 'sessionstorage'
})() })()

View File

@ -18,6 +18,7 @@ import NoteIcon from '../../../Icons/NoteIcon'
import AuditLogIcon from '../../../Icons/AuditLogIcon' import AuditLogIcon from '../../../Icons/AuditLogIcon'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder'
import DocumentPrintButton from '../../common/DocumentPrintButton' import DocumentPrintButton from '../../common/DocumentPrintButton'
import ScrollBox from '../../common/ScrollBox'
const FilamentStockInfo = () => { const FilamentStockInfo = () => {
const location = useLocation() const location = useLocation()
@ -112,12 +113,12 @@ const FilamentStockInfo = () => {
}} }}
editLoading={objectFormState.editLoading} editLoading={objectFormState.editLoading}
formValid={objectFormState.formValid} formValid={objectFormState.formValid}
disabled={objectFormState.lock?.locked || objectFormState.loading} disabled={true}
loading={objectFormState.editLoading} loading={objectFormState.editLoading}
/> />
</Space> </Space>
</Flex> </Flex>
<div style={{ height: '100%', overflow: 'auto' }}> <ScrollBox>
<Flex vertical gap={'large'}> <Flex vertical gap={'large'}>
<ActionHandler <ActionHandler
actions={actions} actions={actions}
@ -199,7 +200,7 @@ const FilamentStockInfo = () => {
)} )}
</InfoCollapse> </InfoCollapse>
</Flex> </Flex>
</div> </ScrollBox>
</Flex> </Flex>
</> </>
) )

View File

@ -202,7 +202,7 @@ const LoadFilamentStock = ({
) : null} ) : null}
{targetTemperature > 0 && {targetTemperature > 0 &&
currentTemperature >= targetTemperature && currentTemperature >= targetTemperature - 2 &&
filamentSensorDetected == false ? ( filamentSensorDetected == false ? (
<Alert <Alert
message={'Insert filament to continue'} message={'Insert filament to continue'}

View File

@ -1,18 +1,9 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { useState } from 'react'
import { useMediaQuery } from 'react-responsive'
import { Typography, Flex, Steps, Divider } from 'antd'
import ObjectInfo from '../../common/ObjectInfo' import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm' import NewObjectForm from '../../common/NewObjectForm'
import NewObjectButtons from '../../common/NewObjectButtons' import WizardView from '../../common/WizardView'
const { Title } = Typography
const NewFilamentStock = ({ onOk, reset }) => { const NewFilamentStock = ({ onOk, reset }) => {
const [currentStep, setCurrentStep] = useState(0)
const isMobile = useMediaQuery({ maxWidth: 768 })
return ( return (
<NewObjectForm <NewObjectForm
type={'filamentStock'} type={'filamentStock'}
@ -30,7 +21,6 @@ const NewFilamentStock = ({ onOk, reset }) => {
column={1} column={1}
bordered={false} bordered={false}
isEditing={true} isEditing={true}
initial={true}
required={true} required={true}
objectData={objectData} objectData={objectData}
/> />
@ -56,43 +46,16 @@ const NewFilamentStock = ({ onOk, reset }) => {
} }
] ]
return ( return (
<Flex gap='middle'> <WizardView
{!isMobile && ( steps={steps}
<div style={{ minWidth: '160px' }}> loading={submitLoading}
<Steps formValid={formValid}
current={currentStep} title='New Filament Stock'
items={steps}
direction='vertical'
style={{ width: 'fit-content' }}
/>
</div>
)}
{!isMobile && (
<Divider type='vertical' style={{ height: 'unset' }} />
)}
<Flex vertical gap='middle' style={{ flexGrow: 1 }}>
<Title level={2} style={{ margin: 0 }}>
New Filament Stock
</Title>
<div style={{ minHeight: '260px', marginBottom: 8 }}>
{steps[currentStep].content}
</div>
<NewObjectButtons
currentStep={currentStep}
totalSteps={steps.length}
onPrevious={() => setCurrentStep((prev) => prev - 1)}
onNext={() => setCurrentStep((prev) => prev + 1)}
onSubmit={() => { onSubmit={() => {
handleSubmit() handleSubmit()
onOk() onOk()
}} }}
formValid={formValid}
submitLoading={submitLoading}
/> />
</Flex>
</Flex>
) )
}} }}
</NewObjectForm> </NewObjectForm>

View File

@ -6,6 +6,7 @@ import PartStockIcon from '../../Icons/PartStockIcon'
import ProductStockIcon from '../../Icons/ProductStockIcon' import ProductStockIcon from '../../Icons/ProductStockIcon'
import StockEventIcon from '../../Icons/StockEventIcon' import StockEventIcon from '../../Icons/StockEventIcon'
import StockAuditIcon from '../../Icons/StockAuditIcon' import StockAuditIcon from '../../Icons/StockAuditIcon'
import PurchaseOrderIcon from '../../Icons/PurchaseOrderIcon'
const items = [ const items = [
{ {
@ -34,6 +35,13 @@ const items = [
path: '/dashboard/inventory/productstocks' path: '/dashboard/inventory/productstocks'
}, },
{ type: 'divider' }, { type: 'divider' },
{
key: 'purchaseorders',
label: 'Purchase Orders',
icon: <PurchaseOrderIcon />,
path: '/dashboard/inventory/purchaseorders'
},
{ type: 'divider' },
{ {
key: 'stockevents', key: 'stockevents',
label: 'Stock Events', label: 'Stock Events',
@ -54,16 +62,23 @@ const routeKeyMap = {
'/dashboard/inventory/partstocks': 'partstocks', '/dashboard/inventory/partstocks': 'partstocks',
'/dashboard/inventory/productstocks': 'productstocks', '/dashboard/inventory/productstocks': 'productstocks',
'/dashboard/inventory/stockevents': 'stockevents', '/dashboard/inventory/stockevents': 'stockevents',
'/dashboard/inventory/stockaudits': 'stockaudits' '/dashboard/inventory/stockaudits': 'stockaudits',
'/dashboard/inventory/purchaseorders': 'purchaseorders'
} }
const InventorySidebar = (props) => { const InventorySidebar = (props) => {
const location = useLocation() const location = useLocation()
const selectedKey = (() => { const selectedKey = (() => {
const match = Object.keys(routeKeyMap).find((path) => const match = Object.keys(routeKeyMap).find((path) => {
location.pathname.startsWith(path) const pathSplit = path.split('/')
) const locationPathSplit = location.pathname.split('/')
return match ? routeKeyMap[match] : 'filaments' 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} /> return <DashboardSidebar items={items} selectedKey={selectedKey} {...props} />

View File

@ -1,119 +1,64 @@
import { useState, useEffect } from 'react'
import { Form, Input, Button, Space, Select, InputNumber } from 'antd'
import axios from 'axios'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import config from '../../../../config' import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
const NewPartStock = ({ onOk, reset }) => { const NewPartStock = ({ onOk, reset }) => {
const [form] = Form.useForm()
const [parts, setParts] = useState([])
const [loading, setLoading] = useState(false)
useEffect(() => {
// Reset form when reset prop changes
if (reset) {
form.resetFields()
}
}, [reset, form])
useEffect(() => {
// Fetch parts for the select dropdown
const fetchParts = async () => {
try {
const response = await axios.get(`${config.backendUrl}/parts`, {
headers: {
Accept: 'application/json'
},
withCredentials: true
})
setParts(response.data)
} catch (error) {
console.error('Error fetching parts:', error)
}
}
fetchParts()
}, [])
const onFinish = async (values) => {
setLoading(true)
try {
await axios.post(
`${config.backendUrl}/partstocks`,
{
part: values.part,
startingLots: values.startingLots,
currentLots: values.startingLots, // Initially current lots equals starting lots
notes: values.notes
},
{
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
withCredentials: true
}
)
onOk()
} catch (error) {
console.error('Error creating part stock:', error)
} finally {
setLoading(false)
}
}
return ( return (
<Form <NewObjectForm
form={form} type={'partStock'}
layout='vertical' reset={reset}
onFinish={onFinish} defaultValues={{ state: { type: 'new' } }}
style={{ maxWidth: '100%' }}
> >
<Form.Item {({ handleSubmit, submitLoading, objectData, formValid }) => {
name='part' const steps = [
label='Part' {
rules={[{ required: true, message: 'Please select a part' }]} title: 'Required',
> key: 'required',
<Select content: (
placeholder='Select a part' <ObjectInfo
options={parts.map((part) => ({ type='partStock'
value: part._id, column={1}
label: part.name bordered={false}
}))} isEditing={true}
required={true}
objectData={objectData}
/> />
</Form.Item> )
},
<Form.Item {
name='startingLots' title: 'Summary',
label='Starting Lots' key: 'summary',
rules={[ content: (
{ required: true, message: 'Please enter the starting lots' }, <ObjectInfo
{ type: 'number', min: 1, message: 'Lots must be at least 1' } type='partStock'
]} column={1}
> bordered={false}
<InputNumber visibleProperties={{
style={{ width: '100%' }} _id: false,
placeholder='Enter starting lots' createdAt: false,
min={1} updatedAt: false
}}
isEditing={false}
objectData={objectData}
/> />
</Form.Item> )
}
<Form.Item name='notes' label='Notes'> ]
<Input.TextArea return (
placeholder='Enter any additional notes' <WizardView
autoSize={{ minRows: 3, maxRows: 6 }} steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Part Stock'
onSubmit={() => {
handleSubmit()
onOk()
}}
/> />
</Form.Item> )
}}
<Form.Item> </NewObjectForm>
<Space>
<Button type='primary' htmlType='submit' loading={loading}>
Create Part Stock
</Button>
<Button onClick={() => form.resetFields()}>Reset</Button>
</Space>
</Form.Item>
</Form>
) )
} }
@ -122,8 +67,4 @@ NewPartStock.propTypes = {
reset: PropTypes.bool reset: PropTypes.bool
} }
NewPartStock.defaultProps = {
reset: false
}
export default NewPartStock export default NewPartStock

View File

@ -0,0 +1,209 @@
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'
const log = loglevel.getLogger('PartStockInfo')
log.setLevel(config.logLevel)
const PartStockInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const partStockId = new URLSearchParams(location.search).get('partStockId')
const [collapseState, updateCollapseState] = useCollapseState(
'PartStockInfo',
{
info: true,
stocks: true,
notes: true,
auditLogs: true
}
)
const [objectFormState, setEditFormState] = useState({
isEditing: false,
editLoading: false,
formValid: false,
locked: false,
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
}
}
return (
<>
<Flex
gap='large'
vertical='true'
style={{
maxHeight: '100%',
minHeight: 0
}}
>
<Flex justify={'space-between'}>
<Space size='middle'>
<Space size='small'>
<ObjectActions
type='partStock'
id={partStockId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Part Stock Information' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='partStock'
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}
>
<InfoCollapse
title='Part Stock Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) => updateCollapseState('info', expanded)}
collapseKey='info'
>
<ObjectForm
id={partStockId}
type='partStock'
style={{ height: '100%' }}
ref={objectFormRef}
onStateChange={(state) => {
console.log('Got edit form state change', state)
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => {
return (
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='partStock'
objectData={objectData}
visibleProperties={{
content: false,
testObject: false
}}
/>
)
}}
</ObjectForm>
</InfoCollapse>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={partStockId} type='partStock' />
</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': partStockId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
</>
)
}
export default PartStockInfo

View File

@ -0,0 +1,101 @@
import { useState, useRef } from 'react'
import { Button, Flex, Space, Modal, Dropdown, message } from 'antd'
import NewPurchaseOrder from './PurchaseOrders/NewPurchaseOrder'
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 PurchaseOrders = () => {
const [messageApi, contextHolder] = message.useMessage()
const [newPurchaseOrderOpen, setNewPurchaseOrderOpen] = useState(false)
const tableRef = useRef()
const [viewMode, setViewMode] = useViewMode('purchaseOrders')
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('purchaseOrders')
const actionItems = {
items: [
{
label: 'New Purchase Order',
key: 'newPurchaseOrder',
icon: <PlusIcon />
},
{ type: 'divider' },
{
label: 'Reload List',
key: 'reloadList',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reloadList') {
tableRef.current?.reload()
} else if (key === 'newPurchaseOrder') {
setNewPurchaseOrderOpen(true)
}
}
}
return (
<>
<Flex vertical={'true'} gap='large'>
{contextHolder}
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<ColumnViewButton
type='purchaseOrder'
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='purchaseOrder'
cards={viewMode === 'cards'}
/>
</Flex>
<Modal
open={newPurchaseOrderOpen}
styles={{ content: { paddingBottom: '24px' } }}
footer={null}
width={800}
onCancel={() => {
setNewPurchaseOrderOpen(false)
}}
destroyOnHidden={true}
>
<NewPurchaseOrder
onOk={() => {
setNewPurchaseOrderOpen(false)
messageApi.success('New purchase order created successfully.')
tableRef.current?.reload()
}}
reset={newPurchaseOrderOpen}
/>
</Modal>
</>
)
}
export default PurchaseOrders

View File

@ -0,0 +1,89 @@
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 }) => {
return (
<NewObjectForm
type={'purchaseOrder'}
reset={reset}
defaultValues={{ state: { type: 'new' } }}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='purchaseOrder'
column={1}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
visibleProperties={{
items: false,
cost: false
}}
/>
)
},
{
title: 'Items',
key: 'items',
content: (
<ObjectProperty
{...getModelProperty('purchaseOrder', 'items')}
isEditing={true}
objectData={objectData}
loading={submitLoading}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='purchaseOrder'
column={1}
bordered={false}
visibleProperties={{
_id: false,
createdAt: false,
updatedAt: false,
items: false
}}
isEditing={false}
objectData={objectData}
/>
)
}
]
return (
<WizardView
steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Purchase Order'
onSubmit={() => {
handleSubmit()
onOk()
}}
/>
)
}}
</NewObjectForm>
)
}
NewPurchaseOrder.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool
}
export default NewPurchaseOrder

View File

@ -0,0 +1,231 @@
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 { getModelProperty } from '../../../../database/ObjectModels.js'
import ObjectProperty from '../../common/ObjectProperty.jsx'
import OrderItemsIcon from '../../../Icons/OrderItemsIcon.jsx'
const log = loglevel.getLogger('PurchaseOrderInfo')
log.setLevel(config.logLevel)
const PurchaseOrderInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
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: () => {
objectFormRef?.current?.startEditing?.()
return false
},
cancelEdit: () => {
objectFormRef?.current?.cancelEditing?.()
return true
},
finishEdit: () => {
objectFormRef?.current?.handleUpdate?.()
return true
},
delete: () => {
objectFormRef?.current?.handleDelete?.()
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: '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'
objectData={objectData}
visibleProperties={{
items: false
}}
/>
</InfoCollapse>
<InfoCollapse
title='Purchase Order Items'
icon={<OrderItemsIcon />}
active={collapseState.info}
onToggle={(expanded) =>
updateCollapseState('info', expanded)
}
collapseKey='info'
>
<ObjectProperty
{...getModelProperty('purchaseOrder', 'items')}
isEditing={isEditing}
objectData={objectData}
loading={loading}
/>
</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>
</>
)
}
export default PurchaseOrderInfo

View File

@ -1,143 +1,34 @@
import { useState, useContext, useRef, useEffect } from 'react' // src/stockAudits.js
import { useNavigate } from 'react-router-dom'
import { Button, Flex, Space, message, Dropdown, Typography } from 'antd'
import { AuthContext } from '../context/AuthContext' import { useState, useRef } from 'react'
import { PrintServerContext } from '../context/PrintServerContext'
import IdDisplay from '../common/IdDisplay' import { Button, Flex, Space, Modal, Dropdown, message } from 'antd'
import StockAuditIcon from '../../Icons/StockAuditIcon'
import InfoCircleIcon from '../../Icons/InfoCircleIcon' import NewStockAudit from './StockAudits/NewStockAudit'
import PlusIcon from '../../Icons/PlusIcon' import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon' import ReloadIcon from '../../Icons/ReloadIcon'
import TimeDisplay from '../common/TimeDisplay' import useColumnVisibility from '../hooks/useColumnVisibility'
import ObjectTable from '../common/ObjectTable' import ObjectTable from '../common/ObjectTable'
import ListIcon from '../../Icons/ListIcon'
import config from '../../../config' import GridIcon from '../../Icons/GridIcon'
import useViewMode from '../hooks/useViewMode'
const { Text } = Typography import ColumnViewButton from '../common/ColumnViewButton'
const StockAudits = () => { const StockAudits = () => {
const [messageApi, contextHolder] = message.useMessage() const [messageApi, contextHolder] = message.useMessage()
const navigate = useNavigate()
const { printServer } = useContext(PrintServerContext)
const [initialized, setInitialized] = useState(false)
const tableRef = useRef() const tableRef = useRef()
const { authenticated } = useContext(AuthContext) const [newStockAuditOpen, setNewStockAuditOpen] = useState(false)
useEffect(() => { const [viewMode, setViewMode] = useViewMode('stockAudits')
if (printServer && !initialized) {
setInitialized(true)
printServer.on('notify_stockaudit_update', (updateData) => {
if (tableRef.current) {
tableRef.current.updateData(updateData._id, updateData)
}
})
}
return () => { const [columnVisibility, setColumnVisibility] =
if (printServer && initialized) { useColumnVisibility('stockAudits')
printServer.off('notify_stockaudit_update')
}
}
}, [printServer, initialized])
const getStockAuditActionItems = (id) => {
return {
items: [
{
label: 'Info',
key: 'info',
icon: <InfoCircleIcon />
}
],
onClick: ({ key }) => {
if (key === 'info') {
navigate(`/dashboard/inventory/stockaudits/info?stockAuditId=${id}`)
}
}
}
}
const columns = [
{
title: '',
dataIndex: '',
key: 'icon',
width: 40,
fixed: 'left',
render: () => <StockAuditIcon />
},
{
title: 'ID',
dataIndex: '_id',
key: 'id',
width: 180,
render: (text) => (
<IdDisplay id={text} type={'stockaudit'} longId={false} />
)
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
width: 120,
render: (status) => <Text>{status}</Text>
},
{
title: 'Created At',
dataIndex: 'createdAt',
key: 'createdAt',
width: 180,
render: (createdAt) => {
if (createdAt) {
return <TimeDisplay dateTime={createdAt} />
}
return 'n/a'
}
},
{
title: 'Updated At',
dataIndex: 'updatedAt',
key: 'updatedAt',
width: 180,
render: (updatedAt) => {
if (updatedAt) {
return <TimeDisplay dateTime={updatedAt} />
}
return 'n/a'
}
},
{
title: 'Actions',
key: 'actions',
fixed: 'right',
width: 150,
render: (text, record) => {
return (
<Space gap='small'>
<Button
icon={<InfoCircleIcon />}
onClick={() =>
navigate(
`/dashboard/inventory/stockaudits/info?stockAuditId=${record._id}`
)
}
/>
<Dropdown menu={getStockAuditActionItems(record._id)}>
<Button>Actions</Button>
</Dropdown>
</Space>
)
}
}
]
const actionItems = { const actionItems = {
items: [ items: [
{ {
label: 'New Stock Audit', label: 'New Stock audit',
key: 'newStockAudit', key: 'newStockAudit',
icon: <PlusIcon /> icon: <PlusIcon />
}, },
@ -152,8 +43,7 @@ const StockAudits = () => {
if (key === 'reloadList') { if (key === 'reloadList') {
tableRef.current?.reload() tableRef.current?.reload()
} else if (key === 'newStockAudit') { } else if (key === 'newStockAudit') {
// TODO: Implement new stock audit creation setNewStockAuditOpen(true)
messageApi.info('New stock audit creation not implemented yet')
} }
} }
} }
@ -162,18 +52,54 @@ const StockAudits = () => {
<> <>
<Flex vertical={'true'} gap='large'> <Flex vertical={'true'} gap='large'>
{contextHolder} {contextHolder}
<Space> <Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}> <Dropdown menu={actionItems}>
<Button>Actions</Button> <Button>Actions</Button>
</Dropdown> </Dropdown>
<ColumnViewButton
type='stockAudit'
loading={false}
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
</Space> </Space>
<Space>
<Button
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
onClick={() =>
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
}
/>
</Space>
</Flex>
<ObjectTable <ObjectTable
ref={tableRef} ref={tableRef}
columns={columns} visibleColumns={columnVisibility}
url={`${config.backendUrl}/stockaudits`} type='stockAudit'
authenticated={authenticated} cards={viewMode === 'cards'}
/> />
</Flex> </Flex>
<Modal
open={newStockAuditOpen}
styles={{ content: { paddingBottom: '24px' } }}
footer={null}
width={800}
onCancel={() => {
setNewStockAuditOpen(false)
}}
destroyOnHidden={true}
>
<NewStockAudit
onOk={() => {
setNewStockAuditOpen(false)
messageApi.success('New stock audit created successfully.')
tableRef.current?.reload()
}}
reset={newStockAuditOpen}
/>
</Modal>
</> </>
) )
} }

View File

@ -0,0 +1,70 @@
import PropTypes from 'prop-types'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
const NewStockAudit = ({ onOk, reset }) => {
return (
<NewObjectForm
type={'stockAudit'}
reset={reset}
defaultValues={{ state: { type: 'new' } }}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='stockAudit'
column={1}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='stockAudit'
column={1}
bordered={false}
visibleProperties={{
_id: false,
createdAt: false,
updatedAt: false
}}
isEditing={false}
objectData={objectData}
/>
)
}
]
return (
<WizardView
steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Stock Audit'
onSubmit={() => {
handleSubmit()
onOk()
}}
/>
)
}}
</NewObjectForm>
)
}
NewStockAudit.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool
}
export default NewStockAudit

View File

@ -1,214 +1,207 @@
import { useEffect, useState, useContext } from 'react' import { useRef, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import axios from 'axios' import { Space, Flex, Card } from 'antd'
import { import { LoadingOutlined } from '@ant-design/icons'
Card, import loglevel from 'loglevel'
Descriptions, import config from '../../../../config.js'
Button, import useCollapseState from '../../hooks/useCollapseState.js'
Space, import NotesPanel from '../../common/NotesPanel.jsx'
message, import InfoCollapse from '../../common/InfoCollapse.jsx'
Typography, import ObjectInfo from '../../common/ObjectInfo.jsx'
Table, import ViewButton from '../../common/ViewButton.jsx'
Tag import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
} from 'antd' import NoteIcon from '../../../Icons/NoteIcon.jsx'
import { import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
ArrowLeftOutlined, import ObjectForm from '../../common/ObjectForm.jsx'
LoadingOutlined, import EditButtons from '../../common/EditButtons.jsx'
ClockCircleOutlined import LockIndicator from '../../common/LockIndicator.jsx'
} from '@ant-design/icons' 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 { AuthContext } from '../../context/AuthContext' const log = loglevel.getLogger('StockAuditInfo')
import IdDisplay from '../../common/IdDisplay' log.setLevel(config.logLevel)
import TimeDisplay from '../../common/TimeDisplay'
import config from '../../../../config'
import XMarkCircleIcon from '../../../Icons/XMarkCircleIcon'
import CheckCircleIcon from '../../../Icons/CheckCircleIcon'
const { Text, Title } = Typography
const StockAuditInfo = () => { const StockAuditInfo = () => {
const [messageApi, contextHolder] = message.useMessage()
const location = useLocation() const location = useLocation()
const navigate = useNavigate() const objectFormRef = useRef(null)
const { authenticated } = useContext(AuthContext) const actionHandlerRef = useRef(null)
const [stockAudit, setStockAudit] = useState(null)
const [loading, setLoading] = useState(true)
const stockAuditId = new URLSearchParams(location.search).get('stockAuditId') const stockAuditId = new URLSearchParams(location.search).get('stockAuditId')
const [collapseState, updateCollapseState] = useCollapseState(
useEffect(() => { 'StockAuditInfo',
const fetchStockAudit = async () => {
if (!stockAuditId) {
messageApi.error('No stock audit ID provided')
navigate('/dashboard/inventory/stockaudits')
return
}
try {
const response = await axios.get(
`${config.backendUrl}/stockaudits/${stockAuditId}`,
{ {
headers: { info: true,
Accept: 'application/json' stocks: true,
notes: true,
auditLogs: true
}
)
const [objectFormState, setEditFormState] = useState({
isEditing: false,
editLoading: false,
formValid: false,
locked: false,
loading: false,
objectData: {}
})
const actions = {
reload: () => {
objectFormRef?.current.handleFetchObject()
return true
}, },
withCredentials: true edit: () => {
} objectFormRef?.current.startEditing()
) return false
setStockAudit(response.data)
setLoading(false)
} catch (err) {
console.error(err)
messageApi.error('Failed to fetch stock audit details')
navigate('/dashboard/inventory/stockaudits')
}
}
if (authenticated) {
fetchStockAudit()
}
}, [authenticated, stockAuditId, messageApi, navigate])
const getStatusTag = (status) => {
switch (status?.toLowerCase()) {
case 'completed':
return (
<Tag icon={<CheckCircleIcon />} color='success'>
Completed
</Tag>
)
case 'in_progress':
return (
<Tag icon={<ClockCircleOutlined />} color='processing'>
In Progress
</Tag>
)
case 'failed':
return (
<Tag icon={<XMarkCircleIcon />} color='error'>
Failed
</Tag>
)
default:
return (
<Tag icon={<ClockCircleOutlined />} color='default'>
Unknown
</Tag>
)
}
}
const auditItemsColumns = [
{
title: 'Item ID',
dataIndex: '_id',
key: 'id',
width: 180,
render: (text) => (
<IdDisplay id={text} type={'stockaudititem'} longId={false} />
)
}, },
{ cancelEdit: () => {
title: 'Item Type', objectFormRef?.current.cancelEditing()
dataIndex: 'itemType', return true
key: 'itemType',
width: 120
}, },
{ finishEdit: () => {
title: 'Expected Weight', objectFormRef?.current.handleUpdate()
dataIndex: 'expectedWeight', return true
key: 'expectedWeight',
width: 120,
render: (weight) => `${weight.toFixed(2)}g`
},
{
title: 'Actual Weight',
dataIndex: 'actualWeight',
key: 'actualWeight',
width: 120,
render: (weight) => `${weight.toFixed(2)}g`
},
{
title: 'Difference',
key: 'difference',
width: 120,
render: (_, record) => {
const diff = record.actualWeight - record.expectedWeight
return (
<Text type={diff === 0 ? 'success' : 'danger'}>
{diff.toFixed(2)}g
</Text>
)
} }
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
width: 120,
render: (status) => getStatusTag(status)
}
]
if (loading) {
return (
<div style={{ textAlign: 'center', padding: '50px' }}>
<LoadingOutlined style={{ fontSize: 24 }} spin />
<Text style={{ marginLeft: 16 }}>Loading stock audit details...</Text>
</div>
)
}
if (!stockAudit) {
return null
} }
return ( return (
<> <>
{contextHolder} <Flex
<Space direction='vertical' size='large' style={{ width: '100%' }}> gap='large'
<Space> vertical='true'
<Button style={{
icon={<ArrowLeftOutlined />} maxHeight: '100%',
onClick={() => navigate('/dashboard/inventory/stockaudits')} minHeight: 0
}}
> >
Back to Stock Audits <Flex justify={'space-between'}>
</Button> <Space size='middle'>
<Space size='small'>
<ObjectActions
type='stockAudit'
id={stockAuditId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Stock Audit Information' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='stockAudit'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space> </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}
>
<InfoCollapse
title='Stock Audit Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) => updateCollapseState('info', expanded)}
collapseKey='info'
>
<ObjectForm
id={stockAuditId}
type='stockAudit'
style={{ height: '100%' }}
ref={objectFormRef}
onStateChange={(state) => {
console.log('Got edit form state change', state)
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => {
return (
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='stockAudit'
objectData={objectData}
visibleProperties={{
content: false,
testObject: false
}}
/>
)
}}
</ObjectForm>
</InfoCollapse>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card> <Card>
<Title level={4}>Stock Audit Details</Title> <NotesPanel _id={stockAuditId} type='stockAudit' />
<Descriptions bordered>
<Descriptions.Item label='ID'>
<IdDisplay
id={stockAudit._id}
type={'stockaudit'}
longId={true}
/>
</Descriptions.Item>
<Descriptions.Item label='Status'>
{getStatusTag(stockAudit.status)}
</Descriptions.Item>
<Descriptions.Item label='Created At'>
<TimeDisplay dateTime={stockAudit.createdAt} showSince={true} />
</Descriptions.Item>
<Descriptions.Item label='Updated At'>
<TimeDisplay dateTime={stockAudit.updatedAt} showSince={true} />
</Descriptions.Item>
</Descriptions>
</Card> </Card>
</InfoCollapse>
<Card title='Audit Items'> <InfoCollapse
<Table title='Audit Logs'
dataSource={stockAudit.items || []} icon={<AuditLogIcon />}
columns={auditItemsColumns} active={collapseState.auditLogs}
rowKey='_id' onToggle={(expanded) =>
pagination={false} updateCollapseState('auditLogs', expanded)
scroll={{ y: 'calc(100vh - 500px)' }} }
collapseKey='auditLogs'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='auditLog'
masterFilter={{ 'parent._id': stockAuditId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/> />
</Card> )}
</Space> </InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
</> </>
) )
} }

View File

@ -0,0 +1,98 @@
import { useState, useRef } from 'react'
import { Button, Flex, Space, Modal, Dropdown, message } from 'antd'
import NewCourierService from './CourierServices/NewCourierService'
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 CourierServices = () => {
const [messageApi, contextHolder] = message.useMessage()
const [newCourierServiceOpen, setNewCourierServiceOpen] = useState(false)
const tableRef = useRef()
const [viewMode, setViewMode] = useViewMode('courierService')
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('courierService')
const actionItems = {
items: [
{
label: 'New Courier Service',
key: 'newCourierService',
icon: <PlusIcon />
},
{ type: 'divider' },
{
label: 'Reload List',
key: 'reloadList',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reloadList') {
tableRef.current?.reload()
} else if (key === 'newCourierService') {
setNewCourierServiceOpen(true)
}
}
}
return (
<>
<Flex vertical={'true'} gap='large'>
{contextHolder}
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<ColumnViewButton
type='courierService'
loading={false}
collapseState={columnVisibility}
updateCollapseState={setColumnVisibility}
/>
</Space>
<Space>
<Button
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
onClick={() =>
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
}
/>
</Space>
</Flex>
<ObjectTable
ref={tableRef}
visibleColumns={columnVisibility}
type='courierService'
cards={viewMode === 'cards'}
/>
</Flex>
<Modal
open={newCourierServiceOpen}
onCancel={() => setNewCourierServiceOpen(false)}
footer={null}
destroyOnHidden={true}
width={700}
>
<NewCourierService
onOk={() => {
setNewCourierServiceOpen(false)
messageApi.success('New courier service created successfully.')
tableRef.current?.reload()
}}
reset={!newCourierServiceOpen}
/>
</Modal>
</>
)
}
export default CourierServices

View File

@ -0,0 +1,198 @@
import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card } from 'antd'
import loglevel from 'loglevel'
import config from '../../../../config'
import useCollapseState from '../../hooks/useCollapseState'
import NotesPanel from '../../common/NotesPanel'
import InfoCollapse from '../../common/InfoCollapse'
import ObjectInfo from '../../common/ObjectInfo'
import ViewButton from '../../common/ViewButton'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import ObjectForm from '../../common/ObjectForm'
import EditButtons from '../../common/EditButtons'
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'
const log = loglevel.getLogger('CourierServiceInfo')
log.setLevel(config.logLevel)
const CourierServiceInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const courierServiceId = new URLSearchParams(location.search).get(
'courierServiceId'
)
const [collapseState, updateCollapseState] = useCollapseState(
'CourierServiceInfo',
{
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
}
}
return (
<>
<Flex
gap='large'
vertical='true'
style={{ maxHeight: '100%', minHeight: 0 }}
>
<Flex justify={'space-between'}>
<Space size='middle'>
<Space size='small'>
<ObjectActions
type='courierService'
id={courierServiceId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Courier Service Information' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='courierService'
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}
>
<InfoCollapse
title='Courier Service Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) => updateCollapseState('info', expanded)}
collapseKey='info'
>
<ObjectForm
id={courierServiceId}
type='courierService'
style={{ height: '100%' }}
ref={objectFormRef}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => (
<ObjectInfo
loading={loading}
isEditing={isEditing}
type='courierService'
objectData={objectData}
/>
)}
</ObjectForm>
</InfoCollapse>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={courierServiceId} type='courierService' />
</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': courierServiceId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
</>
)
}
export default CourierServiceInfo

View File

@ -0,0 +1,86 @@
import PropTypes from 'prop-types'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
const NewCourierService = ({ onOk }) => {
return (
<NewObjectForm
type={'courierService'}
defaultValues={{
active: true,
tracked: false
}}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='courierService'
column={1}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
/>
)
},
{
title: 'Optional',
key: 'optional',
content: (
<ObjectInfo
type='courierService'
column={1}
bordered={false}
isEditing={true}
required={false}
objectData={objectData}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='courierService'
column={1}
bordered={false}
visibleProperties={{
_id: false,
createdAt: false,
updatedAt: false
}}
isEditing={false}
objectData={objectData}
/>
)
}
]
return (
<WizardView
steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Courier Service'
onSubmit={() => {
handleSubmit()
onOk()
}}
/>
)
}}
</NewObjectForm>
)
}
NewCourierService.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool
}
export default NewCourierService

View File

@ -0,0 +1,97 @@
import { useState, useRef } from 'react'
import { Button, Flex, Space, Modal, Dropdown, message } from 'antd'
import NewCourier from './Couriers/NewCourier'
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 Couriers = () => {
const [messageApi, contextHolder] = message.useMessage()
const [newCourierOpen, setNewCourierOpen] = useState(false)
const tableRef = useRef()
const [viewMode, setViewMode] = useViewMode('courier')
const [columnVisibility, setColumnVisibility] = useColumnVisibility('courier')
const actionItems = {
items: [
{
label: 'New Courier',
key: 'newCourier',
icon: <PlusIcon />
},
{ type: 'divider' },
{
label: 'Reload List',
key: 'reloadList',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reloadList') {
tableRef.current?.reload()
} else if (key === 'newCourier') {
setNewCourierOpen(true)
}
}
}
return (
<>
<Flex vertical={'true'} gap='large'>
{contextHolder}
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<ColumnViewButton
type='courier'
loading={false}
collapseState={columnVisibility}
updateCollapseState={setColumnVisibility}
/>
</Space>
<Space>
<Button
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
onClick={() =>
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
}
/>
</Space>
</Flex>
<ObjectTable
ref={tableRef}
visibleColumns={columnVisibility}
type='courier'
cards={viewMode === 'cards'}
/>
</Flex>
<Modal
open={newCourierOpen}
onCancel={() => setNewCourierOpen(false)}
footer={null}
destroyOnHidden={true}
width={700}
>
<NewCourier
onOk={() => {
setNewCourierOpen(false)
messageApi.success('New courier created successfully.')
tableRef.current?.reload()
}}
reset={!newCourierOpen}
/>
</Modal>
</>
)
}
export default Couriers

View File

@ -0,0 +1,216 @@
import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card } from 'antd'
import loglevel from 'loglevel'
import config from '../../../../config'
import useCollapseState from '../../hooks/useCollapseState'
import NotesPanel from '../../common/NotesPanel'
import InfoCollapse from '../../common/InfoCollapse'
import ObjectInfo from '../../common/ObjectInfo'
import ViewButton from '../../common/ViewButton'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import ObjectForm from '../../common/ObjectForm'
import EditButtons from '../../common/EditButtons'
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 CourierServiceIcon from '../../../Icons/CourierServiceIcon.jsx'
const log = loglevel.getLogger('CourierInfo')
log.setLevel(config.logLevel)
const CourierInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const courierId = new URLSearchParams(location.search).get('courierId')
const [collapseState, updateCollapseState] = useCollapseState('CourierInfo', {
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
}
}
return (
<>
<Flex
gap='large'
vertical='true'
style={{ maxHeight: '100%', minHeight: 0 }}
>
<Flex justify={'space-between'}>
<Space size='middle'>
<Space size='small'>
<ObjectActions
type='courier'
id={courierId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Courier Information' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='courier'
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}
>
<InfoCollapse
title='Courier Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) => updateCollapseState('info', expanded)}
collapseKey='info'
>
<ObjectForm
id={courierId}
type='courier'
style={{ height: '100%' }}
ref={objectFormRef}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => (
<ObjectInfo
loading={loading}
isEditing={isEditing}
type='courier'
objectData={objectData}
/>
)}
</ObjectForm>
</InfoCollapse>
</ActionHandler>
<InfoCollapse
title='Courier Services'
icon={<CourierServiceIcon />}
active={collapseState.courierServices}
onToggle={(expanded) =>
updateCollapseState('courierServices', expanded)
}
collapseKey='courierServices'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='courierService'
masterFilter={{ 'courier._id': courierId }}
visibleColumns={{
courier: false,
'courier._id': false
}}
/>
)}
</InfoCollapse>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={courierId} type='courier' />
</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': courierId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
</>
)
}
export default CourierInfo

View File

@ -0,0 +1,80 @@
import PropTypes from 'prop-types'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
const NewCourier = ({ onOk }) => {
return (
<NewObjectForm type={'courier'}>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='courier'
column={1}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
/>
)
},
{
title: 'Optional',
key: 'optional',
content: (
<ObjectInfo
type='courier'
column={1}
bordered={false}
isEditing={true}
required={false}
objectData={objectData}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='courier'
column={1}
bordered={false}
visibleProperties={{
_id: false,
createdAt: false,
updatedAt: false
}}
isEditing={false}
objectData={objectData}
/>
)
}
]
return (
<WizardView
steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Courier'
onSubmit={() => {
handleSubmit()
onOk()
}}
/>
)
}}
</NewObjectForm>
)
}
NewCourier.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool
}
export default NewCourier

View File

@ -20,6 +20,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx' import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
const log = loglevel.getLogger('DocumentJobInfo') const log = loglevel.getLogger('DocumentJobInfo')
log.setLevel(config.logLevel) log.setLevel(config.logLevel)
@ -120,7 +121,7 @@ const DocumentJobInfo = () => {
/> />
</Space> </Space>
</Flex> </Flex>
<div style={{ height: '100%', overflow: 'auto' }}> <ScrollBox>
<Flex vertical gap={'large'}> <Flex vertical gap={'large'}>
<ActionHandler <ActionHandler
actions={actions} actions={actions}
@ -186,7 +187,7 @@ const DocumentJobInfo = () => {
)} )}
</InfoCollapse> </InfoCollapse>
</Flex> </Flex>
</div> </ScrollBox>
</Flex> </Flex>
</> </>
) )

View File

@ -20,6 +20,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx' import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
const log = loglevel.getLogger('DocumentPrinterInfo') const log = loglevel.getLogger('DocumentPrinterInfo')
log.setLevel(config.logLevel) log.setLevel(config.logLevel)
@ -129,7 +130,7 @@ const DocumentPrinterInfo = () => {
</Space> </Space>
</Flex> </Flex>
<div style={{ height: '100%', overflowY: 'scroll' }}> <ScrollBox>
<Flex vertical gap={'large'}> <Flex vertical gap={'large'}>
<ActionHandler <ActionHandler
actions={actions} actions={actions}
@ -199,7 +200,7 @@ const DocumentPrinterInfo = () => {
)} )}
</InfoCollapse> </InfoCollapse>
</Flex> </Flex>
</div> </ScrollBox>
</Flex> </Flex>
</> </>
) )

View File

@ -20,6 +20,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx' import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
const log = loglevel.getLogger('DocumentSizeInfo') const log = loglevel.getLogger('DocumentSizeInfo')
log.setLevel(config.logLevel) log.setLevel(config.logLevel)
@ -120,7 +121,7 @@ const DocumentSizeInfo = () => {
/> />
</Space> </Space>
</Flex> </Flex>
<div style={{ height: '100%', overflow: 'auto' }}> <ScrollBox>
<Flex vertical gap={'large'}> <Flex vertical gap={'large'}>
<ActionHandler <ActionHandler
actions={actions} actions={actions}
@ -186,7 +187,7 @@ const DocumentSizeInfo = () => {
)} )}
</InfoCollapse> </InfoCollapse>
</Flex> </Flex>
</div> </ScrollBox>
</Flex> </Flex>
</> </>
) )

View File

@ -1,19 +1,9 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { useState } from 'react'
import { useMediaQuery } from 'react-responsive'
import { Typography, Flex, Steps, Divider } from 'antd'
import ObjectInfo from '../../common/ObjectInfo' import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm' import NewObjectForm from '../../common/NewObjectForm'
import NewObjectButtons from '../../common/NewObjectButtons' import WizardView from '../../common/WizardView'
const { Title } = Typography
const NewDocumentSize = ({ onOk }) => { const NewDocumentSize = ({ onOk }) => {
const [currentStep, setCurrentStep] = useState(0)
const isMobile = useMediaQuery({ maxWidth: 768 })
return ( return (
<NewObjectForm type={'documentSize'}> <NewObjectForm type={'documentSize'}>
{({ handleSubmit, submitLoading, objectData, formValid }) => { {({ handleSubmit, submitLoading, objectData, formValid }) => {
@ -52,43 +42,16 @@ const NewDocumentSize = ({ onOk }) => {
} }
] ]
return ( return (
<Flex gap='middle'> <WizardView
{!isMobile && ( steps={steps}
<div style={{ minWidth: '160px' }}> loading={submitLoading}
<Steps formValid={formValid}
current={currentStep} title='New Document Size'
items={steps}
direction='vertical'
style={{ width: 'fit-content' }}
/>
</div>
)}
{!isMobile && (
<Divider type='vertical' style={{ height: 'unset' }} />
)}
<Flex vertical gap='middle' style={{ flexGrow: 1 }}>
<Title level={2} style={{ margin: 0 }}>
New Document Size
</Title>
<div style={{ minHeight: '260px', marginBottom: 8 }}>
{steps[currentStep].content}
</div>
<NewObjectButtons
currentStep={currentStep}
totalSteps={steps.length}
onPrevious={() => setCurrentStep((prev) => prev - 1)}
onNext={() => setCurrentStep((prev) => prev + 1)}
onSubmit={() => { onSubmit={() => {
handleSubmit() handleSubmit()
onOk() onOk()
}} }}
formValid={formValid}
submitLoading={submitLoading}
/> />
</Flex>
</Flex>
) )
}} }}
</NewObjectForm> </NewObjectForm>

View File

@ -13,6 +13,7 @@ import EditButtons from '../../common/EditButtons.jsx'
import LockIndicator from '../../common/LockIndicator.jsx' import LockIndicator from '../../common/LockIndicator.jsx'
import ActionHandler from '../../common/ActionHandler.jsx' import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx' import ObjectActions from '../../common/ObjectActions.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
import TemplateEditor from '../../common/TemplateEditor.jsx' import TemplateEditor from '../../common/TemplateEditor.jsx'
@ -122,7 +123,7 @@ const DocumentTemplateDesign = () => {
</Space> </Space>
</Flex> </Flex>
<div style={{ height: '100%', overflowY: 'scroll' }}> <ScrollBox>
<Flex vertical gap={'large'}> <Flex vertical gap={'large'}>
<ActionHandler <ActionHandler
actions={actions} actions={actions}
@ -164,7 +165,7 @@ const DocumentTemplateDesign = () => {
</Card> </Card>
</InfoCollapse> </InfoCollapse>
</Flex> </Flex>
</div> </ScrollBox>
</Flex> </Flex>
) )
} }

View File

@ -20,6 +20,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx' import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
const log = loglevel.getLogger('DocumentTemplateInfo') const log = loglevel.getLogger('DocumentTemplateInfo')
log.setLevel(config.logLevel) log.setLevel(config.logLevel)
@ -126,7 +127,7 @@ const DocumentTemplateInfo = () => {
</Space> </Space>
</Flex> </Flex>
<div style={{ height: '100%', overflowY: 'scroll' }}> <ScrollBox>
<Flex vertical gap={'large'}> <Flex vertical gap={'large'}>
<ActionHandler <ActionHandler
actions={actions} actions={actions}
@ -201,7 +202,7 @@ const DocumentTemplateInfo = () => {
)} )}
</InfoCollapse> </InfoCollapse>
</Flex> </Flex>
</div> </ScrollBox>
</Flex> </Flex>
</> </>
) )

View File

@ -21,6 +21,7 @@ import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import FilamentIcon from '../../../Icons/FilamentIcon.jsx' import FilamentIcon from '../../../Icons/FilamentIcon.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
const log = loglevel.getLogger('FilamentInfo') const log = loglevel.getLogger('FilamentInfo')
log.setLevel(config.logLevel) log.setLevel(config.logLevel)
@ -126,7 +127,7 @@ const FilamentInfo = () => {
</Space> </Space>
</Flex> </Flex>
<div style={{ height: '100%', overflowY: 'scroll' }}> <ScrollBox>
<Flex vertical gap={'large'}> <Flex vertical gap={'large'}>
<ActionHandler <ActionHandler
actions={actions} actions={actions}
@ -218,7 +219,7 @@ const FilamentInfo = () => {
)} )}
</InfoCollapse> </InfoCollapse>
</Flex> </Flex>
</div> </ScrollBox>
</Flex> </Flex>
</> </>
) )

View File

@ -23,6 +23,7 @@ import FileIcon from '../../../Icons/FileIcon.jsx'
import FilePreview from '../../common/FilePreview.jsx' import FilePreview from '../../common/FilePreview.jsx'
import MissingPlaceholder from '../../common/MissingPlaceholder.jsx' import MissingPlaceholder from '../../common/MissingPlaceholder.jsx'
import { ApiServerContext } from '../../context/ApiServerContext.jsx' import { ApiServerContext } from '../../context/ApiServerContext.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
const log = loglevel.getLogger('FileInfo') const log = loglevel.getLogger('FileInfo')
log.setLevel(config.logLevel) log.setLevel(config.logLevel)
@ -130,7 +131,7 @@ const FileInfo = () => {
/> />
</Space> </Space>
</Flex> </Flex>
<div style={{ height: '100%', overflow: 'auto' }}> <ScrollBox>
<Flex vertical gap={'large'}> <Flex vertical gap={'large'}>
<ActionHandler <ActionHandler
actions={actions} actions={actions}
@ -214,7 +215,7 @@ const FileInfo = () => {
)} )}
</InfoCollapse> </InfoCollapse>
</Flex> </Flex>
</div> </ScrollBox>
</Flex> </Flex>
</> </>
) )

View File

@ -23,6 +23,7 @@ import HostOTP from './HostOtp.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import PrinterIcon from '../../../Icons/PrinterIcon.jsx' import PrinterIcon from '../../../Icons/PrinterIcon.jsx'
import DocumentPrinterIcon from '../../../Icons/DocumentPrinterIcon.jsx' import DocumentPrinterIcon from '../../../Icons/DocumentPrinterIcon.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
const log = loglevel.getLogger('HostInfo') const log = loglevel.getLogger('HostInfo')
log.setLevel(config.logLevel) log.setLevel(config.logLevel)
@ -132,7 +133,7 @@ const HostInfo = () => {
</Space> </Space>
</Flex> </Flex>
<div style={{ height: '100%', overflowY: 'scroll' }}> <ScrollBox>
<Flex vertical gap={'large'}> <Flex vertical gap={'large'}>
<ActionHandler <ActionHandler
actions={actions} actions={actions}
@ -245,7 +246,7 @@ const HostInfo = () => {
)} )}
</InfoCollapse> </InfoCollapse>
</Flex> </Flex>
</div> </ScrollBox>
</Flex> </Flex>
<Modal <Modal

View File

@ -17,6 +17,8 @@ import DocumentIcon from '../../Icons/DocumentIcon'
import DocumentSizeIcon from '../../Icons/DocumentSizeIcon' import DocumentSizeIcon from '../../Icons/DocumentSizeIcon'
import DocumentJobIcon from '../../Icons/DocumentJobIcon' import DocumentJobIcon from '../../Icons/DocumentJobIcon'
import FileIcon from '../../Icons/FileIcon' import FileIcon from '../../Icons/FileIcon'
import CourierIcon from '../../Icons/CourierIcon'
import CourierServiceIcon from '../../Icons/CourierServiceIcon'
const items = [ const items = [
{ {
@ -50,6 +52,19 @@ const items = [
path: '/dashboard/management/materials' path: '/dashboard/management/materials'
}, },
{ type: 'divider' }, { type: 'divider' },
{
key: 'couriers',
icon: <CourierIcon />,
label: 'Couriers',
path: '/dashboard/management/couriers'
},
{
key: 'courierServices',
icon: <CourierServiceIcon />,
label: 'Courier Services',
path: '/dashboard/management/courierservices'
},
{ type: 'divider' },
{ {
key: 'noteTypes', key: 'noteTypes',
icon: <NoteTypeIcon />, icon: <NoteTypeIcon />,
@ -139,6 +154,8 @@ const routeKeyMap = {
'/dashboard/management/users': 'users', '/dashboard/management/users': 'users',
'/dashboard/management/products': 'products', '/dashboard/management/products': 'products',
'/dashboard/management/vendors': 'vendors', '/dashboard/management/vendors': 'vendors',
'/dashboard/management/couriers': 'couriers',
'/dashboard/management/courierservices': 'courierServices',
'/dashboard/management/materials': 'materials', '/dashboard/management/materials': 'materials',
'/dashboard/management/notetypes': 'noteTypes', '/dashboard/management/notetypes': 'noteTypes',
'/dashboard/management/settings': 'settings', '/dashboard/management/settings': 'settings',
@ -154,9 +171,15 @@ const routeKeyMap = {
const ManagementSidebar = (props) => { const ManagementSidebar = (props) => {
const location = useLocation() const location = useLocation()
const selectedKey = (() => { const selectedKey = (() => {
const match = Object.keys(routeKeyMap).find((path) => const match = Object.keys(routeKeyMap).find((path) => {
location.pathname.startsWith(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] : 'filaments' return match ? routeKeyMap[match] : 'filaments'
})() })()

View File

@ -16,6 +16,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx' import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
const NoteTypeInfo = () => { const NoteTypeInfo = () => {
const location = useLocation() const location = useLocation()
@ -109,7 +110,7 @@ const NoteTypeInfo = () => {
/> />
</Space> </Space>
</Flex> </Flex>
<div style={{ height: '100%', overflow: 'auto' }}> <ScrollBox>
<Flex vertical gap={'large'}> <Flex vertical gap={'large'}>
<ActionHandler <ActionHandler
actions={actions} actions={actions}
@ -164,7 +165,7 @@ const NoteTypeInfo = () => {
)} )}
</InfoCollapse> </InfoCollapse>
</Flex> </Flex>
</div> </ScrollBox>
</Flex> </Flex>
</> </>
) )

View File

@ -19,6 +19,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx' import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
const log = loglevel.getLogger('NoteInfo') const log = loglevel.getLogger('NoteInfo')
log.setLevel(config.logLevel) log.setLevel(config.logLevel)
@ -118,7 +119,7 @@ const NoteInfo = () => {
/> />
</Space> </Space>
</Flex> </Flex>
<div style={{ height: '100%', overflow: 'auto' }}> <ScrollBox>
<Flex vertical gap={'large'}> <Flex vertical gap={'large'}>
<ActionHandler <ActionHandler
actions={actions} actions={actions}
@ -183,7 +184,7 @@ const NoteInfo = () => {
)} )}
</InfoCollapse> </InfoCollapse>
</Flex> </Flex>
</div> </ScrollBox>
</Flex> </Flex>
</> </>
) )

View File

@ -17,6 +17,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx' import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
const PartInfo = () => { const PartInfo = () => {
const location = useLocation() const location = useLocation()
@ -109,7 +110,7 @@ const PartInfo = () => {
/> />
</Space> </Space>
</Flex> </Flex>
<div style={{ height: '100%', overflow: 'auto' }}> <ScrollBox>
<Flex vertical gap={'large'}> <Flex vertical gap={'large'}>
<ActionHandler <ActionHandler
actions={actions} actions={actions}
@ -174,7 +175,7 @@ const PartInfo = () => {
)} )}
</InfoCollapse> </InfoCollapse>
</Flex> </Flex>
</div> </ScrollBox>
</Flex> </Flex>
</> </>
) )

View File

@ -12,12 +12,15 @@ import LockIndicator from '../../common/LockIndicator.jsx'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx' import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import ProductIcon from '../../../Icons/ProductIcon.jsx'
import ObjectTable from '../../common/ObjectTable.jsx' import ObjectTable from '../../common/ObjectTable.jsx'
import ActionHandler from '../../common/ActionHandler.jsx' import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx' import ObjectActions from '../../common/ObjectActions.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
import ObjectProperty from '../../common/ObjectProperty.jsx'
import { getModelProperty } from '../../../../database/ObjectModels.js'
import PartIcon from '../../../Icons/PartIcon.jsx'
const ProductInfo = () => { const ProductInfo = () => {
const location = useLocation() const location = useLocation()
@ -111,19 +114,12 @@ const ProductInfo = () => {
/> />
</Space> </Space>
</Flex> </Flex>
<div style={{ height: '100%', overflow: 'auto' }}> <ScrollBox>
<Flex vertical gap={'large'}> <Flex vertical gap={'large'}>
<ActionHandler <ActionHandler
actions={actions} actions={actions}
loading={objectFormState.loading} loading={objectFormState.loading}
ref={actionHandlerRef} ref={actionHandlerRef}
>
<InfoCollapse
title='Product Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) => updateCollapseState('info', expanded)}
collapseKey='info'
> >
<ObjectForm <ObjectForm
id={productId} id={productId}
@ -135,32 +131,47 @@ const ProductInfo = () => {
}} }}
> >
{({ loading, isEditing, objectData }) => ( {({ loading, isEditing, objectData }) => (
<Flex vertical gap={'large'}>
<InfoCollapse
title='Product Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) =>
updateCollapseState('info', expanded)
}
collapseKey='info'
>
<ObjectInfo <ObjectInfo
loading={loading} loading={loading}
isEditing={isEditing} isEditing={isEditing}
type='product' type='product'
objectData={objectData} objectData={objectData}
visibleProperties={{
parts: false
}}
/> />
)}
</ObjectForm>
</InfoCollapse> </InfoCollapse>
</ActionHandler>
<InfoCollapse <InfoCollapse
title='Product Parts' title='Product Parts'
icon={<ProductIcon />} icon={<PartIcon />}
active={collapseState.parts} active={collapseState.parts}
onToggle={(expanded) => updateCollapseState('parts', expanded)} onToggle={(expanded) =>
updateCollapseState('parts', expanded)
}
collapseKey='parts' collapseKey='parts'
> >
<ObjectTable <ObjectProperty
type='part' {...getModelProperty('product', 'parts')}
visibleColumns={{ isEditing={isEditing}
product: false, objectData={objectData}
'product._id': false loading={loading}
}}
masterFilter={{ 'product._id': productId }}
/> />
</InfoCollapse> </InfoCollapse>
</Flex>
)}
</ObjectForm>
</ActionHandler>
<InfoCollapse <InfoCollapse
title='Notes' title='Notes'
icon={<NoteIcon />} icon={<NoteIcon />}
@ -192,7 +203,7 @@ const ProductInfo = () => {
)} )}
</InfoCollapse> </InfoCollapse>
</Flex> </Flex>
</div> </ScrollBox>
</Flex> </Flex>
</> </>
) )

View File

@ -18,6 +18,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx' import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
const UserInfo = () => { const UserInfo = () => {
const location = useLocation() const location = useLocation()
@ -110,7 +111,7 @@ const UserInfo = () => {
/> />
</Space> </Space>
</Flex> </Flex>
<div style={{ height: '100%', overflow: 'auto' }}> <ScrollBox>
<Flex vertical gap={'large'}> <Flex vertical gap={'large'}>
<ActionHandler <ActionHandler
actions={actions} actions={actions}
@ -176,7 +177,7 @@ const UserInfo = () => {
)} )}
</InfoCollapse> </InfoCollapse>
</Flex> </Flex>
</div> </ScrollBox>
</Flex> </Flex>
</> </>
) )

View File

@ -19,6 +19,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx' import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
const log = loglevel.getLogger('VendorInfo') const log = loglevel.getLogger('VendorInfo')
log.setLevel(config.logLevel) log.setLevel(config.logLevel)
@ -118,7 +119,7 @@ const VendorInfo = () => {
/> />
</Space> </Space>
</Flex> </Flex>
<div style={{ height: '100%', overflow: 'auto' }}> <ScrollBox>
<Flex vertical gap={'large'}> <Flex vertical gap={'large'}>
<ActionHandler <ActionHandler
actions={actions} actions={actions}
@ -183,7 +184,7 @@ const VendorInfo = () => {
)} )}
</InfoCollapse> </InfoCollapse>
</Flex> </Flex>
</div> </ScrollBox>
</Flex> </Flex>
</> </>
) )

View File

@ -8,6 +8,7 @@ import useCollapseState from '../../hooks/useCollapseState.js'
import NotesPanel from '../../common/NotesPanel.jsx' import NotesPanel from '../../common/NotesPanel.jsx'
import InfoCollapse from '../../common/InfoCollapse.jsx' import InfoCollapse from '../../common/InfoCollapse.jsx'
import ObjectInfo from '../../common/ObjectInfo.jsx' import ObjectInfo from '../../common/ObjectInfo.jsx'
import ObjectProperty from '../../common/ObjectProperty.jsx'
import ViewButton from '../../common/ViewButton.jsx' import ViewButton from '../../common/ViewButton.jsx'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx' import NoteIcon from '../../../Icons/NoteIcon.jsx'
@ -23,6 +24,9 @@ import EyeIcon from '../../../Icons/EyeIcon.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import MissingPlaceholder from '../../common/MissingPlaceholder.jsx' import MissingPlaceholder from '../../common/MissingPlaceholder.jsx'
import FilePreview from '../../common/FilePreview.jsx' import FilePreview from '../../common/FilePreview.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
import { getModelProperty } from '../../../../database/ObjectModels.js'
import PartIcon from '../../../Icons/PartIcon.jsx'
const log = loglevel.getLogger('GCodeFileInfo') const log = loglevel.getLogger('GCodeFileInfo')
log.setLevel(config.logLevel) log.setLevel(config.logLevel)
@ -36,7 +40,8 @@ const GCodeFileInfo = () => {
'GCodeFileInfo', 'GCodeFileInfo',
{ {
info: true, info: true,
stocks: true, parts: true,
preview: true,
notes: true, notes: true,
auditLogs: true auditLogs: true
} }
@ -93,6 +98,7 @@ const GCodeFileInfo = () => {
disabled={objectFormState.loading} disabled={objectFormState.loading}
items={[ items={[
{ key: 'info', label: 'GCode File Information' }, { key: 'info', label: 'GCode File Information' },
{ key: 'parts', label: 'Parts' },
{ key: 'preview', label: 'GCode File Preview' }, { key: 'preview', label: 'GCode File Preview' },
{ key: 'notes', label: 'Notes' }, { key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' } { key: 'auditLogs', label: 'Audit Logs' }
@ -128,7 +134,7 @@ const GCodeFileInfo = () => {
</Space> </Space>
</Flex> </Flex>
<div style={{ height: '100%', overflowY: 'scroll' }}> <ScrollBox>
<Flex vertical gap={'large'}> <Flex vertical gap={'large'}>
<ActionHandler <ActionHandler
actions={actions} actions={actions}
@ -163,7 +169,25 @@ const GCodeFileInfo = () => {
isEditing={isEditing} isEditing={isEditing}
type='gcodeFile' type='gcodeFile'
objectData={objectData} objectData={objectData}
visibleProperties={{}} visibleProperties={{
parts: false
}}
/>
</InfoCollapse>
<InfoCollapse
title='Parts'
icon={<PartIcon />}
active={collapseState.parts}
onToggle={(expanded) =>
updateCollapseState('parts', expanded)
}
collapseKey='parts'
>
<ObjectProperty
{...getModelProperty('gcodeFile', 'parts')}
isEditing={isEditing}
objectData={objectData}
loading={loading}
/> />
</InfoCollapse> </InfoCollapse>
<InfoCollapse <InfoCollapse
@ -224,7 +248,7 @@ const GCodeFileInfo = () => {
)} )}
</InfoCollapse> </InfoCollapse>
</Flex> </Flex>
</div> </ScrollBox>
</Flex> </Flex>
</> </>
) )

View File

@ -22,6 +22,7 @@ import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import JobIcon from '../../../Icons/JobIcon.jsx' import JobIcon from '../../../Icons/JobIcon.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import DeployJob from './DeployJob.jsx' import DeployJob from './DeployJob.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
const log = loglevel.getLogger('JobInfo') const log = loglevel.getLogger('JobInfo')
log.setLevel(config.logLevel) log.setLevel(config.logLevel)
@ -132,7 +133,7 @@ const JobInfo = () => {
</Space> </Space>
</Flex> </Flex>
<div style={{ height: '100%', overflowY: 'scroll' }}> <ScrollBox>
<Flex vertical gap={'large'}> <Flex vertical gap={'large'}>
<ActionHandler <ActionHandler
actions={actions} actions={actions}
@ -214,7 +215,7 @@ const JobInfo = () => {
)} )}
</InfoCollapse> </InfoCollapse>
</Flex> </Flex>
</div> </ScrollBox>
</Flex> </Flex>
<Modal <Modal
destroyOnHidden destroyOnHidden

View File

@ -30,6 +30,7 @@ import { ApiServerContext } from '../../context/ApiServerContext.jsx'
import LoadFilamentStock from '../../Inventory/FilamentStocks/LoadFilamentStock.jsx' import LoadFilamentStock from '../../Inventory/FilamentStocks/LoadFilamentStock.jsx'
import UnloadFilamentStock from '../../Inventory/FilamentStocks/UnloadFilamentStock.jsx' import UnloadFilamentStock from '../../Inventory/FilamentStocks/UnloadFilamentStock.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
const log = loglevel.getLogger('ControlPrinter') const log = loglevel.getLogger('ControlPrinter')
log.setLevel(config.logLevel) log.setLevel(config.logLevel)
@ -240,7 +241,10 @@ const ControlPrinter = () => {
visibleState={collapseState} visibleState={collapseState}
updateVisibleState={updateCollapseState} updateVisibleState={updateCollapseState}
/> />
<AlertsDisplay alerts={objectFormState.objectData?.alerts} /> <AlertsDisplay
alerts={objectFormState.objectData?.alerts}
printerId={printerId}
/>
</Space> </Space>
</Space> </Space>
<Space> <Space>
@ -263,7 +267,7 @@ const ControlPrinter = () => {
</Space> </Space>
</Flex> </Flex>
<div style={{ height: '100%', overflowY: 'scroll' }}> <ScrollBox>
<Flex vertical gap={'large'}> <Flex vertical gap={'large'}>
<ActionHandler <ActionHandler
actions={actions} actions={actions}
@ -320,7 +324,9 @@ const ControlPrinter = () => {
currentJob: false, currentJob: false,
'currentJob._id': false, 'currentJob._id': false,
currentSubJob: false, currentSubJob: false,
'currentSubJob._id': false 'currentSubJob._id': false,
createdAt: false,
updatedAt: false
}} }}
objectData={printerObjectData} objectData={printerObjectData}
type='printer' type='printer'
@ -486,7 +492,7 @@ const ControlPrinter = () => {
</Card> </Card>
</InfoCollapse> </InfoCollapse>
</Flex> </Flex>
</div> </ScrollBox>
</Flex> </Flex>
<Modal <Modal
open={loadFilamentStockOpen} open={loadFilamentStockOpen}

View File

@ -20,6 +20,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx' import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
const log = loglevel.getLogger('PrinterInfo') const log = loglevel.getLogger('PrinterInfo')
log.setLevel(config.logLevel) log.setLevel(config.logLevel)
@ -122,7 +123,7 @@ const PrinterInfo = () => {
</Space> </Space>
</Flex> </Flex>
<div style={{ height: '100%', overflowY: 'scroll' }}> <ScrollBox>
<Flex vertical gap={'large'}> <Flex vertical gap={'large'}>
<ActionHandler <ActionHandler
actions={actions} actions={actions}
@ -202,7 +203,7 @@ const PrinterInfo = () => {
)} )}
</InfoCollapse> </InfoCollapse>
</Flex> </Flex>
</div> </ScrollBox>
</Flex> </Flex>
</> </>
) )

View File

@ -51,9 +51,15 @@ const routeKeyMap = {
const ProductionSidebar = (props) => { const ProductionSidebar = (props) => {
const location = useLocation() const location = useLocation()
const selectedKey = (() => { const selectedKey = (() => {
const match = Object.keys(routeKeyMap).find((path) => const match = Object.keys(routeKeyMap).find((path) => {
location.pathname.startsWith(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 match ? routeKeyMap[match] : 'overview'
})() })()

View File

@ -0,0 +1,208 @@
import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import useCollapseState from '../../hooks/useCollapseState'
import NotesPanel from '../../common/NotesPanel'
import InfoCollapse from '../../common/InfoCollapse'
import ObjectInfo from '../../common/ObjectInfo'
import ViewButton from '../../common/ViewButton'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import ObjectForm from '../../common/ObjectForm'
import EditButtons from '../../common/EditButtons'
import LockIndicator from '../../common/LockIndicator.jsx'
import ActionHandler from '../../common/ActionHandler'
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 StockEventIcon from '../../../Icons/StockEventIcon.jsx'
const SubJobInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const subJobId = new URLSearchParams(location.search).get('subJobId')
const [collapseState, updateCollapseState] = useCollapseState('SubJobInfo', {
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
}
}
return (
<>
<Flex
gap='large'
vertical='true'
style={{ maxHeight: '100%', minHeight: 0 }}
>
<Flex justify={'space-between'}>
<Space size='middle'>
<Space size='small'>
<ObjectActions
type='subJob'
id={subJobId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Sub Job Information' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='subJob'
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}
>
<InfoCollapse
title='Sub Job Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) => updateCollapseState('info', expanded)}
collapseKey='info'
>
<ObjectForm
id={subJobId}
type='subJob'
style={{ height: '100%' }}
ref={objectFormRef}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => (
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='subJob'
objectData={objectData}
/>
)}
</ObjectForm>
</InfoCollapse>
</ActionHandler>
<InfoCollapse
title='Sub Job Stock Events'
icon={<StockEventIcon />}
active={collapseState.events}
onToggle={(expanded) => updateCollapseState('events', expanded)}
collapseKey='events'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='stockEvent'
masterFilter={{ 'owner._id': subJobId }}
visibleColumns={{
'owner._id': false,
'owner.type': false,
owner: false
}}
/>
)}
</InfoCollapse>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={subJobId} type='subJob' />
</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': subJobId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
</>
)
}
export default SubJobInfo

View File

@ -1,15 +1,29 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Flex, Alert } from 'antd' import { createElement } from 'react'
import { Flex, Alert, Button, Dropdown, Popover } from 'antd'
import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon' import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon'
import InfoCircleIcon from '../../Icons/InfoCircleIcon' import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import { CaretDownOutlined } from '@ant-design/icons'
const AlertsDisplay = ({ alerts = [] }) => { import { useMediaQuery } from 'react-responsive'
import { getModelByName } from '../../../database/ObjectModels'
import { useNavigate } from 'react-router-dom'
const AlertsDisplay = ({
alerts = [],
printerId,
showDismiss = true,
showActions = true
}) => {
const isMobile = useMediaQuery({ maxWidth: 768 })
const getAlertType = (type, priority) => { const getAlertType = (type, priority) => {
if (type === 'error' || priority === '9') return 'error' if (type === 'error' || priority === '9') return 'error'
if (type === 'warning' || priority === '8') return 'warning' if (type === 'warning' || priority === '8') return 'warning'
return 'info' return 'info'
} }
const printerModel = getModelByName('printer')
const navigate = useNavigate()
const getAlertIcon = (type, priority) => { const getAlertIcon = (type, priority) => {
if (type === 'error' || priority === '9') return <ExclamationOctagonIcon /> if (type === 'error' || priority === '9') return <ExclamationOctagonIcon />
if (type === 'warning' || priority === '8') if (type === 'warning' || priority === '8')
@ -17,34 +31,185 @@ const AlertsDisplay = ({ alerts = [] }) => {
return <InfoCircleIcon /> return <InfoCircleIcon />
} }
// Recursively filter the printer model actions by a set of allowed action keys
const filterActionsByKeys = (actions, allowedKeys) => {
if (!Array.isArray(actions)) return []
const filtered = actions
.map((action) => {
if (action.type === 'divider') {
return { type: 'divider' }
}
const actionKey = action.key || action.name
let children = []
if (Array.isArray(action.children)) {
children = filterActionsByKeys(action.children, allowedKeys)
}
const isAllowed = actionKey && allowedKeys.has(actionKey)
if (!isAllowed && children.length === 0) {
return null
}
return {
...action,
children
}
})
.filter((action) => action !== null)
// Clean up dividers: remove leading/trailing and consecutive dividers
const cleaned = []
for (const action of filtered) {
if (action.type === 'divider') {
if (cleaned.length === 0) continue
if (cleaned[cleaned.length - 1].type === 'divider') continue
}
cleaned.push(action)
}
if (cleaned[cleaned.length - 1]?.type === 'divider') {
cleaned.pop()
}
return cleaned
}
// Map filtered printer actions to AntD Dropdown menu items (including children)
const mapActionsToMenuItems = (actions) => {
if (!Array.isArray(actions)) return []
return actions.map((action) => {
if (action.type === 'divider') {
return { type: 'divider' }
}
const item = {
key: action.key || action.name,
label: action.label,
icon: action.icon ? createElement(action.icon) : undefined
}
if (Array.isArray(action.children) && action.children.length > 0) {
item.children = mapActionsToMenuItems(action.children)
}
return item
})
}
if (alerts.length == 0) { if (alerts.length == 0) {
return null return null
} }
const alertElements = alerts.map((alert, index) => {
const printerActions = printerModel?.actions || []
const alertActionKeys = Array.isArray(alert?.actions)
? alert.actions
.map((action) =>
typeof action === 'string'
? action
: action?.key || action?.name || null
)
.filter((key) => key != null)
: []
const allowedKeys = new Set(alertActionKeys)
const filteredActions = filterActionsByKeys(printerActions, allowedKeys)
const findActionByKey = (actions, key) => {
if (!Array.isArray(actions)) return null
for (const action of actions) {
if (action.type === 'divider') continue
const actionKey = action.key || action.name
if (actionKey === key) {
return action
}
if (Array.isArray(action.children) && action.children.length > 0) {
const found = findActionByKey(action.children, key)
if (found) return found
}
}
return null
}
const menu = {
items: mapActionsToMenuItems(filteredActions),
onClick: ({ key }) => {
const action = findActionByKey(filteredActions, key)
if (action?.url) {
navigate(action.url(printerId))
} else {
console.warn('No action found for key:', key)
}
}
}
return ( return (
<Flex gap='small'>
{alerts.map((alert, index) => (
<Alert <Alert
key={`${alert.createdAt}-${index}`} key={`${alert.createdAt}-${index}-${alert._id}`}
message={alert.message} message={alert.message}
style={{ padding: '4px 10px 4px 8px' }} style={{ padding: '4px 10px 4px 8px' }}
type={getAlertType(alert.type, alert.priority)} type={getAlertType(alert.type, alert.priority)}
icon={getAlertIcon(alert.type, alert.priority)} icon={getAlertIcon(alert.type, alert.priority)}
showIcon showIcon
closable={showDismiss && alert.canDismiss}
onClose={() => {
console.log('Closing alert:', alert._id)
}}
action={
showActions ? (
<Dropdown menu={menu} on>
<Button size='small' type='text' style={{ marginLeft: '5px' }}>
<CaretDownOutlined />
</Button>
</Dropdown>
) : null
}
/> />
))}
</Flex>
) )
})
if (isMobile) {
return (
<Popover
content={alertElements}
trigger='hover'
arrow={false}
placement='bottom'
classNames={{
root: 'printer-alerts-display-popover'
}}
>
<Button>Alerts</Button>
</Popover>
)
}
return <Flex gap='small'>{alertElements}</Flex>
} }
AlertsDisplay.propTypes = { AlertsDisplay.propTypes = {
printerId: PropTypes.string.isRequired,
showActions: PropTypes.bool.isRequired,
showDismiss: PropTypes.bool.isRequired,
alerts: PropTypes.arrayOf( alerts: PropTypes.arrayOf(
PropTypes.shape({ PropTypes.shape({
priority: PropTypes.string.isRequired, canDismiss: PropTypes.bool.isRequired,
_id: PropTypes.string.isRequired,
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
createdAt: PropTypes.string.isRequired, createdAt: PropTypes.string.isRequired,
updatedAt: PropTypes.string.isRequired, updatedAt: PropTypes.string.isRequired,
message: PropTypes.string.isRequired message: PropTypes.string,
actions: PropTypes.arrayOf(PropTypes.string)
}) })
).isRequired ).isRequired
} }

View File

@ -153,7 +153,7 @@ const DashboardNavigation = () => {
fontSize: '46px', fontSize: '46px',
height: '16px', height: '16px',
marginLeft: '15px', marginLeft: '15px',
marginRight: '5px' marginRight: '8px'
}} }}
/> />
)} )}
@ -313,7 +313,7 @@ const DashboardNavigation = () => {
{isElectron ? ( {isElectron ? (
<Flex <Flex
className='ant-menu-horizontal ant-menu-light electron-navigation-wrapper' className='ant-menu-horizontal ant-menu-light electron-navigation-wrapper'
style={{ lineHeight: '40px', padding: '0 4px 0 4px' }} style={{ lineHeight: '40px', padding: '0 2px 0 2px' }}
> >
{navigationContents} {navigationContents}
</Flex> </Flex>

View File

@ -36,7 +36,7 @@ const DashboardWindowButtons = () => {
<Flex align='center'> <Flex align='center'>
{platform == 'darwin' ? ( {platform == 'darwin' ? (
isFullScreen == false ? ( isFullScreen == false ? (
<div style={{ width: '65px' }} /> <div style={{ width: '80px' }} />
) : null ) : null
) : ( ) : (
<div style={{ width: '95px' }}> <div style={{ width: '95px' }}>

View File

@ -1,4 +1,4 @@
import { Upload, Button, Flex, Typography, Space } from 'antd' import { Upload, Button, Flex, Typography, Space, Progress, Card } from 'antd'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { ApiServerContext } from '../context/ApiServerContext' import { ApiServerContext } from '../context/ApiServerContext'
import UploadIcon from '../../Icons/UploadIcon' import UploadIcon from '../../Icons/UploadIcon'
@ -6,6 +6,7 @@ import { useContext, useState, useEffect } from 'react'
import ObjectSelect from './ObjectSelect' import ObjectSelect from './ObjectSelect'
import FileList from './FileList' import FileList from './FileList'
import PlusIcon from '../../Icons/PlusIcon' import PlusIcon from '../../Icons/PlusIcon'
import { LoadingOutlined } from '@ant-design/icons'
const { Text } = Typography const { Text } = Typography
@ -18,6 +19,8 @@ const FileUpload = ({
showInfo showInfo
}) => { }) => {
const { uploadFile } = useContext(ApiServerContext) const { uploadFile } = useContext(ApiServerContext)
const [uploading, setUploading] = useState(false)
const [uploadProgress, setUploadProgress] = useState(0)
// Track current files using useState // Track current files using useState
const [currentFiles, setCurrentFiles] = useState(() => { const [currentFiles, setCurrentFiles] = useState(() => {
@ -56,7 +59,11 @@ const FileUpload = ({
const handleFileUpload = async (file) => { const handleFileUpload = async (file) => {
try { try {
const uploadedFile = await uploadFile(file) setUploading(true)
const uploadedFile = await uploadFile(file, {}, (progress) => {
setUploadProgress(progress)
})
setUploading(false)
if (uploadedFile) { if (uploadedFile) {
if (multiple) { if (multiple) {
// For multiple files, add to existing array // For multiple files, add to existing array
@ -95,7 +102,7 @@ const FileUpload = ({
return ( return (
<Flex gap={'small'} vertical> <Flex gap={'small'} vertical>
{hasNoItems ? ( {hasNoItems && uploading == false ? (
<Flex gap={'small'} align='center'> <Flex gap={'small'} align='center'>
<Space.Compact style={{ flexGrow: 1 }}> <Space.Compact style={{ flexGrow: 1 }}>
<ObjectSelect <ObjectSelect
@ -123,6 +130,29 @@ const FileUpload = ({
</Upload> </Upload>
</Flex> </Flex>
) : null} ) : null}
{uploading == true ? (
<Card styles={{ body: { padding: '10px 15px' } }}>
<Flex gap={'small'} align='center'>
<Text>Uploading...</Text>
{uploadProgress > 0 ? (
<>
{uploadProgress >= 0 && uploadProgress < 100 ? (
<>
<Progress
percent={uploadProgress}
showInfo={false}
style={{ width: '100px', flexGrow: 1 }}
status='active'
/>
<Text>{uploadProgress}%</Text>
</>
) : null}
{uploadProgress == 100 ? <LoadingOutlined /> : null}
</>
) : null}
</Flex>
</Card>
) : null}
<FileList <FileList
files={currentFiles} files={currentFiles}
multiple={multiple} multiple={multiple}

View File

@ -0,0 +1,43 @@
import PropTypes from 'prop-types'
import { Flex, Typography } from 'antd'
import CopyButton from './CopyButton'
const { Text } = Typography
const MiscId = ({ value, showCopy = true }) => {
if (!value) {
return <Text type='secondary'>n/a</Text>
}
return (
<Flex
align={'end'}
className='miscid'
style={{ minWidth: '0px', width: '100%' }}
>
<Text
code
ellipsis
style={showCopy ? { marginRight: 6, minWidth: '0px' } : undefined}
>
{value}
</Text>
{showCopy && (
<CopyButton
text={value}
tooltip='Copy ID'
style={{ marginLeft: 0 }}
iconStyle={{ fontSize: '14px' }}
/>
)}
</Flex>
)
}
MiscId.propTypes = {
value: PropTypes.string,
showCopy: PropTypes.bool
}
export default MiscId

View File

@ -3,8 +3,20 @@ import { Form, message } from 'antd'
import { ApiServerContext } from '../context/ApiServerContext' import { ApiServerContext } from '../context/ApiServerContext'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import merge from 'lodash/merge' import merge from 'lodash/merge'
import set from 'lodash/set'
import { getModelByName } from '../../../database/ObjectModels' import { getModelByName } from '../../../database/ObjectModels'
const buildObjectFromEntries = (entries = []) => {
return entries.reduce((acc, entry) => {
const { namePath, value } = entry || {}
if (!Array.isArray(namePath) || value === undefined) {
return acc
}
set(acc, namePath, value)
return acc
}, {})
}
/** /**
* NewObjectForm is a reusable form component for creating new objects. * NewObjectForm is a reusable form component for creating new objects.
* It handles form validation, submission, and error handling logic. * It handles form validation, submission, and error handling logic.
@ -30,18 +42,50 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
const model = getModelByName(type) const model = getModelByName(type)
// Function to calculate computed values from model properties // Function to calculate computed values from model properties
const calculateComputedValues = useCallback((currentData, model) => { const calculateComputedValues = useCallback(
if (!model || !model.properties) return {} (currentData, modelDefinition) => {
if (!modelDefinition || !Array.isArray(modelDefinition.properties)) {
return []
}
const computedValues = {} const normalizedPath = (name, parentPath = []) => {
if (Array.isArray(name)) {
return [...parentPath, ...name]
}
if (typeof name === 'number') {
return [...parentPath, name]
}
if (typeof name === 'string' && name.length > 0) {
return [...parentPath, ...name.split('.')]
}
return parentPath
}
const getValueAtPath = (dataSource, path) => {
if (!Array.isArray(path) || path.length === 0) {
return dataSource
}
return path.reduce((acc, key) => {
if (acc == null) return acc
return acc[key]
}, dataSource)
}
const computedEntries = []
const processProperty = (property, scopeData, parentPath = []) => {
if (!property?.name) return
const propertyPath = normalizedPath(property.name, parentPath)
model.properties.forEach((property) => {
// Check if this property has a computed value function
if (property.value && typeof property.value === 'function') { if (property.value && typeof property.value === 'function') {
try { try {
const computedValue = property.value(currentData) const computedValue = property.value(scopeData || {})
if (computedValue !== undefined) { if (computedValue !== undefined) {
computedValues[property.name] = computedValue computedEntries.push({
namePath: propertyPath,
value: computedValue
})
} }
} catch (error) { } catch (error) {
console.warn( console.warn(
@ -50,21 +94,50 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
) )
} }
} }
if (
Array.isArray(property.properties) &&
property.properties.length > 0
) {
if (property.type === 'objectChildren') {
const childValues = getValueAtPath(currentData, propertyPath)
if (Array.isArray(childValues)) {
childValues.forEach((childData = {}, index) => {
property.properties.forEach((childProperty) => {
processProperty(childProperty, childData || {}, [
...propertyPath,
index
])
})
})
}
} else {
const nestedScope = getValueAtPath(currentData, propertyPath) || {}
property.properties.forEach((childProperty) => {
processProperty(childProperty, nestedScope || {}, propertyPath)
})
}
}
}
modelDefinition.properties.forEach((property) => {
processProperty(property, currentData)
}) })
return computedValues return computedEntries
}, []) },
[]
)
// Set initial form values when defaultValues change // Set initial form values when defaultValues change
useEffect(() => { useEffect(() => {
if (Object.keys(defaultValues).length > 0) { if (Object.keys(defaultValues).length > 0) {
// Calculate computed values for initial data // Calculate computed values for initial data
const computedValues = calculateComputedValues(defaultValues, model) const computedEntries = calculateComputedValues(defaultValues, model)
const initialFormData = { ...defaultValues, ...computedValues } const computedValuesObject = buildObjectFromEntries(computedEntries)
const initialFormData = merge({}, defaultValues, computedValuesObject)
form.setFieldsValue(initialFormData) form.setFieldsValue(initialFormData)
setObjectData((prev) => { setObjectData((prev) => merge({}, prev, initialFormData))
return merge({}, prev, initialFormData)
})
} }
}, [form, defaultValues, calculateComputedValues, model]) }, [form, defaultValues, calculateComputedValues, model])
@ -102,18 +175,31 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
form={form} form={form}
layout='vertical' layout='vertical'
style={style} style={style}
onValuesChange={(values) => { onValuesChange={(_changedValues, allFormValues) => {
// Calculate computed values based on current form data // Calculate computed values based on current form data
const currentFormData = { ...objectData, ...values } const currentFormData = merge({}, objectData || {}, allFormValues)
const computedValues = calculateComputedValues(currentFormData, model) const computedEntries = calculateComputedValues(currentFormData, model)
// Update form with computed values if any were calculated if (Array.isArray(computedEntries) && computedEntries.length > 0) {
if (Object.keys(computedValues).length > 0) { computedEntries.forEach(({ namePath, value }) => {
form.setFieldsValue(computedValues) if (!Array.isArray(namePath) || value === undefined) return
const currentValue = form.getFieldValue(namePath)
if (currentValue !== value) {
if (typeof form.setFieldValue === 'function') {
form.setFieldValue(namePath, value)
} else {
const fallbackPayload = buildObjectFromEntries([
{ namePath, value }
])
form.setFieldsValue(fallbackPayload)
}
}
})
} }
// Merge all values (user input + computed values) // Merge all values (user input + computed values)
const allValues = { ...values, ...computedValues } const computedValuesObject = buildObjectFromEntries(computedEntries)
const allValues = merge({}, allFormValues, computedValuesObject)
setObjectData((prev) => { setObjectData((prev) => {
return merge({}, prev, allValues) return merge({}, prev, allValues)
}) })

View File

@ -3,6 +3,8 @@ import { Dropdown, Button } from 'antd'
import { getModelByName } from '../../../database/ObjectModels' import { getModelByName } from '../../../database/ObjectModels'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { useNavigate, useLocation } from 'react-router-dom' import { useNavigate, useLocation } from 'react-router-dom'
import { useActionsModal } from '../context/ActionsModalContext'
import KeyboardShortcut from './KeyboardShortcut'
// Recursively filter actions based on visibleActions // Recursively filter actions based on visibleActions
function filterActionsByVisibility(actions, visibleActions) { function filterActionsByVisibility(actions, visibleActions) {
@ -43,6 +45,7 @@ function mapActionsToMenuItems(actions, currentUrlWithActions, id, objectData) {
const actionUrl = action.url ? action.url(id) : undefined const actionUrl = action.url ? action.url(id) : undefined
var disabled = actionUrl && actionUrl === currentUrlWithActions var disabled = actionUrl && actionUrl === currentUrlWithActions
var visible = true
if (action.disabled) { if (action.disabled) {
if (typeof action.disabled === 'function') { if (typeof action.disabled === 'function') {
@ -52,6 +55,14 @@ function mapActionsToMenuItems(actions, currentUrlWithActions, id, objectData) {
} }
} }
if (action.visible) {
if (typeof action.visible === 'function') {
visible = action.visible(objectData)
} else {
visible = action.visible
}
}
const item = { const item = {
key: action.key || action.name, key: action.key || action.name,
label: action.label, label: action.label,
@ -67,7 +78,9 @@ function mapActionsToMenuItems(actions, currentUrlWithActions, id, objectData) {
objectData objectData
) )
} }
if (visible == true) {
return item return item
}
}) })
} }
@ -91,6 +104,7 @@ const ObjectActions = ({
const actions = model.actions || [] const actions = model.actions || []
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const { showActionsModal } = useActionsModal()
// Get current url without 'action' param // Get current url without 'action' param
const currentUrlWithoutActions = stripActionParam( const currentUrlWithoutActions = stripActionParam(
@ -140,11 +154,20 @@ const ObjectActions = ({
} }
return ( return (
<KeyboardShortcut
shortcut='alt+a'
onTrigger={() => showActionsModal(id, type, objectData)}
>
<Dropdown menu={menu} {...dropdownProps}> <Dropdown menu={menu} {...dropdownProps}>
<Button {...buttonProps} disabled={disabled}> <Button
{...buttonProps}
disabled={disabled}
onClick={() => showActionsModal(id, type, objectData)}
>
Actions Actions
</Button> </Button>
</Dropdown> </Dropdown>
</KeyboardShortcut>
) )
} }

View File

@ -0,0 +1,474 @@
import { useMemo, useEffect, useRef } from 'react'
import PropTypes from 'prop-types'
import { Table, Skeleton, Card, Button, Flex, Form, Typography } from 'antd'
import PlusIcon from '../../Icons/PlusIcon'
import ObjectProperty from './ObjectProperty'
import { LoadingOutlined } from '@ant-design/icons'
const { Text } = Typography
const DEFAULT_COLUMN_WIDTHS = {
text: 200,
number: 120,
dateTime: 200,
state: 200,
id: 180,
bool: 120,
tags: 200
}
const getDefaultWidth = (type) => {
return DEFAULT_COLUMN_WIDTHS[type] || 200
}
const createSkeletonRows = (rowCount, keyPrefix, keyName) => {
return Array.from({ length: rowCount }).map((_, index) => {
const skeletonKey = `${keyPrefix}-${index}`
const row = {
isSkeleton: true,
_objectChildTableKey: skeletonKey
}
if (typeof keyName === 'string') {
row[keyName] = skeletonKey
}
return row
})
}
const ObjectChildTable = ({
maxWidth = '100%',
properties = [],
columns = [],
visibleColumns = {},
objectData = null,
scrollHeight = 240,
size = 'small',
loading = false,
rowKey = '_id',
skeletonRows = 5,
additionalColumns = [],
emptyText = 'No items',
isEditing = false,
formListName,
value = [],
rollups = [],
onChange,
...tableProps
}) => {
const mainTableWrapperRef = useRef(null)
const rollupTableWrapperRef = useRef(null)
const propertyMap = useMemo(() => {
const map = new Map()
properties.forEach((property) => {
if (property?.name) {
map.set(property.name, property)
}
})
return map
}, [properties])
const orderedPropertyNames = useMemo(() => {
if (columns && columns.length > 0) {
return columns
}
return properties.map((property) => property.name).filter(Boolean)
}, [columns, properties])
const resolvedProperties = useMemo(() => {
const explicit = orderedPropertyNames
.map((name) => propertyMap.get(name))
.filter(Boolean)
const remaining = properties.filter(
(property) => !orderedPropertyNames.includes(property.name)
)
return [...explicit, ...remaining].filter((property) => {
if (!property?.name) return false
if (
visibleColumns &&
Object.prototype.hasOwnProperty.call(visibleColumns, property.name)
) {
return visibleColumns[property.name] !== false
}
return true
})
}, [orderedPropertyNames, propertyMap, properties, visibleColumns])
// When used inside antd Form.Item without Form.List, `value` will be the controlled array.
const itemsSource = useMemo(() => {
return value ?? []
}, [value])
// When used with antd Form.List, grab the form instance so we can read
// the latest row values and pass them into ObjectProperty as objectData.
// Assumes this component is rendered within a Form context when editing.
const formInstance = Form.useFormInstance()
const listNamePath = useMemo(() => {
if (!formListName) return null
return Array.isArray(formListName) ? formListName : [formListName]
}, [formListName])
const tableColumns = useMemo(() => {
const propertyColumns = resolvedProperties.map((property) => ({
title: property.label || property.name,
dataIndex: property.name,
key: property.name,
width: property.columnWidth || getDefaultWidth(property.type),
render: (_text, record) => {
if (record?.isSkeleton) {
return (
<Skeleton.Input active size='small' style={{ width: '100%' }} />
)
}
return (
<ObjectProperty
{...property}
longId={false}
objectData={record}
isEditing={isEditing}
/>
)
}
}))
return [...propertyColumns, ...additionalColumns]
}, [resolvedProperties, additionalColumns, isEditing])
const skeletonData = useMemo(() => {
return createSkeletonRows(
skeletonRows,
'object-child-table-skeleton',
typeof rowKey === 'string' ? rowKey : null
)
}, [skeletonRows, rowKey])
const dataSource = useMemo(() => {
if (loading && (!itemsSource || itemsSource.length === 0)) {
return skeletonData
}
return itemsSource
}, [itemsSource, loading, skeletonData])
const resolvedRowKey =
typeof rowKey === 'function' ? rowKey : (_record, index) => index
const scrollConfig =
scrollHeight != null
? { y: scrollHeight, x: 'max-content' }
: { x: 'max-content' }
const handleAddItem = () => {
const newItem = {}
resolvedProperties.forEach((property) => {
if (
property?.name &&
!Object.prototype.hasOwnProperty.call(newItem, property.name)
) {
newItem[property.name] = null
}
})
const currentItems = Array.isArray(itemsSource) ? itemsSource : []
const newItems = [...currentItems, newItem]
if (typeof onChange === 'function') {
onChange(newItems)
}
}
const rollupDataSource = useMemo(() => {
if (!rollups || rollups.length === 0) return []
// Single summary row where each rollup value is placed under
// the column that matches its `property` field.
const summaryRow = {}
properties.forEach((property) => {
const rollup = rollups.find(
(r) => r.property && r.property === property.name
)
if (rollup && typeof rollup.value === 'function') {
try {
const updatedObjectData = { ...objectData }
console.log('UPDATED OBJECT DATA', value)
updatedObjectData[property.name] = value
summaryRow[property.name] = rollup.value(updatedObjectData)
} catch (e) {
// Fail quietly but log for debugging
console.error('Error computing rollup', rollup.name, e)
summaryRow[property.name] = null
}
} else {
summaryRow[property.name] = null
}
})
return [summaryRow]
}, [properties, rollups, objectData])
const rollupColumns = useMemo(() => {
return properties.map((property, index) => {
const nextProperty = properties[index + 1]
var nextRollup = null
if (nextProperty) {
nextRollup = rollups?.find(
(r) => r.property && r.property === nextProperty.name
)
}
const rollupLabel = nextRollup?.label
return {
title: <Text>{property.label || property.name}</Text>,
dataIndex: property.name,
key: property.name,
width: property.columnWidth || getDefaultWidth(property.type),
render: (_text, record) => {
return (
<Flex justify={'space-between'}>
<Text>
{property?.prefix}
{record[property.name]}
{property?.suffix}
</Text>
{rollupLabel && <Text type='secondary'>{rollupLabel}:</Text>}
</Flex>
)
}
}
})
}, [properties, rollups])
const hasRollups = useMemo(
() => Array.isArray(rollups) && rollups.length > 0,
[rollups]
)
useEffect(() => {
if (!hasRollups || isEditing == null) return
const mainWrapper = mainTableWrapperRef.current
const rollupWrapper = rollupTableWrapperRef.current
if (!mainWrapper || !rollupWrapper) return
const mainBody =
mainWrapper.querySelector('.ant-table-body') ||
mainWrapper.querySelector('.ant-table-content')
const rollupBody =
rollupWrapper.querySelector('.ant-table-body') ||
rollupWrapper.querySelector('.ant-table-content')
if (!mainBody || !rollupBody) return
let isSyncing = false
const syncScroll = (source, target) => {
if (!target) return
isSyncing = true
target.scrollLeft = source.scrollLeft
window.requestAnimationFrame(() => {
isSyncing = false
})
}
const handleMainScroll = () => {
if (isSyncing) return
syncScroll(mainBody, rollupBody)
}
const handleRollupScroll = () => {
if (isSyncing) return
syncScroll(rollupBody, mainBody)
}
mainBody.addEventListener('scroll', handleMainScroll)
rollupBody.addEventListener('scroll', handleRollupScroll)
return () => {
mainBody.removeEventListener('scroll', handleMainScroll)
rollupBody.removeEventListener('scroll', handleRollupScroll)
}
}, [hasRollups, isEditing])
const rollupTable = hasRollups ? (
<div ref={rollupTableWrapperRef}>
<Table
dataSource={rollupDataSource}
showHeader={false}
columns={rollupColumns}
loading={loading}
pagination={false}
size={size}
rowKey={resolvedRowKey}
scroll={scrollConfig}
locale={{ emptyText }}
style={{ maxWidth, minWidth: 0 }}
className='rollup-table'
/>
</div>
) : null
const tableComponent = (
<Flex vertical>
<div ref={mainTableWrapperRef}>
<Table
style={{ maxWidth, minWidth: 0 }}
dataSource={dataSource}
columns={tableColumns}
loading={{ spinning: loading, indicator: <LoadingOutlined spin /> }}
size={size}
rowKey={resolvedRowKey}
scroll={scrollConfig}
locale={{ emptyText }}
pagination={false}
className={hasRollups ? 'child-table-rollups' : 'child-table'}
{...tableProps}
/>
</div>
{rollupTable}
</Flex>
)
// When editing and a Form.List name is provided, bind rows via Form.List
// instead of the manual value/onChange mechanism.
if (isEditing === true && formListName) {
return (
<Form.List name={formListName}>
{(fields, { add }) => {
const listDataSource = fields.map((field, index) => ({
_field: field,
_index: index,
key: field.key
}))
const listColumns = resolvedProperties.map((property) => ({
title: property.label || property.name,
dataIndex: property.name,
key: property.name,
width: property.columnWidth || getDefaultWidth(property.type),
render: (_text, record) => {
const field = record?._field
if (!field) return null
// Resolve the most up-to-date row data for this index from the form
let rowObjectData = undefined
if (formInstance && listNamePath) {
const namePath = [...listNamePath, field.name]
rowObjectData = formInstance.getFieldValue(namePath)
}
return (
<ObjectProperty
{...property}
// Bind directly to this list item + property via NamePath
name={[field.name, property.name]}
longId={false}
isEditing={true}
objectData={rowObjectData}
/>
)
}
}))
const listTable = (
<Flex vertical>
<div ref={mainTableWrapperRef}>
<Table
dataSource={listDataSource}
columns={[...listColumns, ...additionalColumns]}
pagination={false}
size={size}
loading={loading}
rowKey={(record) => record.key ?? record._index}
scroll={scrollConfig}
locale={{ emptyText }}
className={hasRollups ? 'child-table-rollups' : 'child-table'}
style={{ maxWidth, minWidth: 0 }}
{...tableProps}
/>
</div>
{rollupTable}
</Flex>
)
const handleAddListItem = () => {
const newItem = {}
resolvedProperties.forEach((property) => {
if (property?.name) {
newItem[property.name] = null
}
})
add(newItem)
}
return (
<Card style={{ minWidth: 0 }}>
<Flex vertical gap={'middle'}>
<Flex justify={'space-between'}>
<Button>Actions</Button>
<Button
type='primary'
icon={<PlusIcon />}
onClick={handleAddListItem}
/>
</Flex>
{listTable}
</Flex>
</Card>
)
}}
</Form.List>
)
}
if (isEditing === true) {
return (
<Card>
<Flex vertical gap={'middle'}>
<Flex justify={'space-between'}>
<Button>Actions</Button>
<Button
type='primary'
icon={<PlusIcon />}
onClick={handleAddItem}
/>
</Flex>
{tableComponent}
</Flex>
</Card>
)
}
return tableComponent
}
ObjectChildTable.propTypes = {
properties: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
label: PropTypes.string,
type: PropTypes.string
})
).isRequired,
columns: PropTypes.arrayOf(PropTypes.string),
visibleColumns: PropTypes.object,
scrollHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
size: PropTypes.string,
loading: PropTypes.bool,
rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
skeletonRows: PropTypes.number,
additionalColumns: PropTypes.arrayOf(PropTypes.object),
emptyText: PropTypes.node,
isEditing: PropTypes.bool,
formListName: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
value: PropTypes.arrayOf(PropTypes.object),
onChange: PropTypes.func,
maxWidth: PropTypes.string,
rollups: PropTypes.arrayOf(PropTypes.object),
objectData: PropTypes.object
}
export default ObjectChildTable

View File

@ -13,8 +13,20 @@ import { AuthContext } from '../context/AuthContext'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import DeleteObjectModal from './DeleteObjectModal' import DeleteObjectModal from './DeleteObjectModal'
import merge from 'lodash/merge' import merge from 'lodash/merge'
import set from 'lodash/set'
import { getModelByName } from '../../../database/ObjectModels' import { getModelByName } from '../../../database/ObjectModels'
const buildObjectFromEntries = (entries = []) => {
return entries.reduce((acc, entry) => {
const { namePath, value } = entry || {}
if (!Array.isArray(namePath) || value === undefined) {
return acc
}
set(acc, namePath, value)
return acc
}, {})
}
/** /**
* ObjectForm is a reusable form component for editing any object type. * ObjectForm is a reusable form component for editing any object type.
* It handles fetching, updating, locking, unlocking, and validation logic. * It handles fetching, updating, locking, unlocking, and validation logic.
@ -37,6 +49,7 @@ const ObjectForm = forwardRef(
const [lock, setLock] = useState({}) const [lock, setLock] = useState({})
const [initialized, setInitialized] = useState(false) const [initialized, setInitialized] = useState(false)
const [isEditing, setIsEditing] = useState(false) const [isEditing, setIsEditing] = useState(false)
const isEditingRef = useRef(false)
const [formValid, setFormValid] = useState(false) const [formValid, setFormValid] = useState(false)
const [form] = Form.useForm() const [form] = Form.useForm()
@ -115,18 +128,50 @@ const ObjectForm = forwardRef(
}) })
// Function to calculate computed values from model properties // Function to calculate computed values from model properties
const calculateComputedValues = useCallback((currentData, model) => { const calculateComputedValues = useCallback(
if (!model || !model.properties) return {} (currentData, modelDefinition) => {
if (!modelDefinition || !Array.isArray(modelDefinition.properties)) {
return []
}
const computedValues = {} const normalizedPath = (name, parentPath = []) => {
if (Array.isArray(name)) {
return [...parentPath, ...name]
}
if (typeof name === 'number') {
return [...parentPath, name]
}
if (typeof name === 'string' && name.length > 0) {
return [...parentPath, ...name.split('.')]
}
return parentPath
}
const getValueAtPath = (dataSource, path) => {
if (!Array.isArray(path) || path.length === 0) {
return dataSource
}
return path.reduce((acc, key) => {
if (acc == null) return acc
return acc[key]
}, dataSource)
}
const computedEntries = []
const processProperty = (property, scopeData, parentPath = []) => {
if (!property?.name) return
const propertyPath = normalizedPath(property.name, parentPath)
model.properties.forEach((property) => {
// Check if this property has a computed value function
if (property.value && typeof property.value === 'function') { if (property.value && typeof property.value === 'function') {
try { try {
const computedValue = property.value(currentData) const computedValue = property.value(scopeData || {})
if (computedValue !== undefined) { if (computedValue !== undefined) {
computedValues[property.name] = computedValue computedEntries.push({
namePath: propertyPath,
value: computedValue
})
} }
} catch (error) { } catch (error) {
console.warn( console.warn(
@ -135,10 +180,41 @@ const ObjectForm = forwardRef(
) )
} }
} }
if (
Array.isArray(property.properties) &&
property.properties.length > 0
) {
if (property.type === 'objectChildren') {
const childValues = getValueAtPath(currentData, propertyPath)
if (Array.isArray(childValues)) {
childValues.forEach((childData = {}, index) => {
property.properties.forEach((childProperty) => {
processProperty(childProperty, childData || {}, [
...propertyPath,
index
])
})
})
}
} else {
const nestedScope =
getValueAtPath(currentData, propertyPath) || {}
property.properties.forEach((childProperty) => {
processProperty(childProperty, nestedScope || {}, propertyPath)
})
}
}
}
modelDefinition.properties.forEach((property) => {
processProperty(property, currentData)
}) })
return computedValues return computedEntries
}, []) },
[]
)
// Validate form on change (debounced to avoid heavy work on every keystroke) // Validate form on change (debounced to avoid heavy work on every keystroke)
useEffect(() => { useEffect(() => {
@ -146,7 +222,8 @@ const ObjectForm = forwardRef(
const currentFormValues = form.getFieldsValue() const currentFormValues = form.getFieldsValue()
const mergedObjectData = { const mergedObjectData = {
...serverObjectData.current, ...serverObjectData.current,
...currentFormValues ...currentFormValues,
_isEditing: isEditingRef.current
} }
form form
@ -198,12 +275,13 @@ const ObjectForm = forwardRef(
const lockEvent = await fetchObjectLock(id, type) const lockEvent = await fetchObjectLock(id, type)
setLock(lockEvent) setLock(lockEvent)
onStateChangeRef.current({ lock: lockEvent }) onStateChangeRef.current({ lock: lockEvent })
setObjectData(data) setObjectData({ ...data, _isEditing: isEditingRef.current })
serverObjectData.current = data serverObjectData.current = data
// Calculate and set computed values on initial load // Calculate and set computed values on initial load
const computedValues = calculateComputedValues(data, model) const computedEntries = calculateComputedValues(data, model)
const initialFormData = { ...data, ...computedValues } const computedValuesObject = buildObjectFromEntries(computedEntries)
const initialFormData = merge({}, data, computedValuesObject)
form.setFieldsValue(initialFormData) form.setFieldsValue(initialFormData)
setFetchLoading(false) setFetchLoading(false)
@ -275,24 +353,37 @@ const ObjectForm = forwardRef(
const startEditing = () => { const startEditing = () => {
setIsEditing(true) setIsEditing(true)
onStateChangeRef.current({ isEditing: true }) isEditingRef.current = true
console.log('IS EDITING TRUE')
setObjectData((prev) => ({ ...prev, _isEditing: isEditingRef.current }))
onStateChangeRef.current({
isEditing: true,
objectData: { ...objectData, _isEditing: isEditingRef.current }
})
lockObject(id, type) lockObject(id, type)
} }
const cancelEditing = () => { const cancelEditing = () => {
if (serverObjectData.current) { if (serverObjectData.current) {
// Recalculate computed values when canceling // Recalculate computed values when canceling
const computedValues = calculateComputedValues( const computedEntries = calculateComputedValues(
serverObjectData.current, serverObjectData.current,
model model
) )
const resetFormData = { ...serverObjectData.current, ...computedValues } const computedValuesObject = buildObjectFromEntries(computedEntries)
const resetFormData = merge(
form.setFieldsValue(resetFormData) {},
setObjectData(resetFormData) serverObjectData.current,
} computedValuesObject
)
setIsEditing(false) setIsEditing(false)
onStateChangeRef.current({ isEditing: false }) isEditingRef.current = false
form.setFieldsValue(resetFormData)
console.log('IS EDITING FALSE')
setObjectData({ ...resetFormData, _isEditing: isEditingRef.current })
}
onStateChangeRef.current({ isEditing: isEditingRef.current })
unlockObject(id, type) unlockObject(id, type)
} }
@ -302,9 +393,15 @@ const ObjectForm = forwardRef(
setEditLoading(true) setEditLoading(true)
onStateChangeRef.current({ editLoading: true }) onStateChangeRef.current({ editLoading: true })
await updateObject(id, type, value) await updateObject(id, type, value)
setObjectData({ ...objectData, ...value })
setIsEditing(false) setIsEditing(false)
onStateChangeRef.current({ isEditing: false }) isEditingRef.current = false
onStateChangeRef.current({ isEditing: isEditingRef.current })
setObjectData({
...objectData,
...value,
_isEditing: isEditingRef.current
})
messageApi.success('Information updated successfully') messageApi.success('Information updated successfully')
} catch (err) { } catch (err) {
console.error(err) console.error(err)
@ -374,37 +471,51 @@ const ObjectForm = forwardRef(
form={form} form={form}
layout='vertical' layout='vertical'
style={style} style={style}
onValuesChange={(values) => { onValuesChange={(changedValues, allFormValues) => {
// Use the full form snapshot (allFormValues) so list fields (Form.List)
// come through as complete arrays instead of sparse arrays like
// [null, null, { quantity: 5 }].
if (onEdit != undefined) { if (onEdit != undefined) {
onEdit(values) onEdit(allFormValues)
} }
// Calculate computed values based on current form data // Calculate computed values based on current form data
const currentFormData = { ...objectData, ...values } const currentFormData = {
const computedValues = calculateComputedValues( ...(serverObjectData.current || {}),
...allFormValues
}
const computedEntries = calculateComputedValues(
currentFormData, currentFormData,
model model
) )
// Update form with computed values if any were calculated and they changed if (Array.isArray(computedEntries) && computedEntries.length > 0) {
if (Object.keys(computedValues).length > 0) { computedEntries.forEach(({ namePath, value }) => {
const currentComputedValues = form.getFieldsValue( if (!Array.isArray(namePath) || value === undefined) return
Object.keys(computedValues) const currentValue = form.getFieldValue(namePath)
) if (currentValue !== value) {
const hasDiff = Object.keys(computedValues).some( if (typeof form.setFieldValue === 'function') {
(key) => currentComputedValues[key] !== computedValues[key] form.setFieldValue(namePath, value)
) } else {
const fallbackPayload = buildObjectFromEntries([
if (hasDiff) { { namePath, value }
form.setFieldsValue(computedValues) ])
form.setFieldsValue(fallbackPayload)
} }
} }
})
}
// Merge all values (user input + computed values) const computedValuesObject = buildObjectFromEntries(computedEntries)
const allValues = { ...values, ...computedValues } const mergedFormValues = merge(
{},
allFormValues,
computedValuesObject
)
mergedFormValues._isEditing = isEditingRef.current
setObjectData((prev) => { setObjectData((prev) => {
return { ...prev, ...allValues } return { ...prev, ...mergedFormValues }
}) })
}} }}
> >

View File

@ -43,6 +43,8 @@ import AlertsDisplay from './AlertsDisplay'
import FileUpload from './FileUpload' import FileUpload from './FileUpload'
import DataTree from './DataTree' import DataTree from './DataTree'
import FileList from './FileList' import FileList from './FileList'
import ObjectChildTable from './ObjectChildTable'
import MiscId from './MiscId'
const { Text } = Typography const { Text } = Typography
@ -86,6 +88,11 @@ const ObjectProperty = ({
roundNumber = false, roundNumber = false,
showHyperlink, showHyperlink,
showSince, showSince,
properties = [],
onChange = null,
maxWidth = '100%',
loading = false,
rollups = [],
...rest ...rest
}) => { }) => {
if (value && typeof value == 'function' && objectData) { if (value && typeof value == 'function' && objectData) {
@ -379,6 +386,18 @@ const ObjectProperty = ({
case 'objectList': { case 'objectList': {
return <ObjectList value={value} objectType={objectType} /> return <ObjectList value={value} objectType={objectType} />
} }
case 'objectChildren': {
return (
<ObjectChildTable
value={value}
properties={properties}
objectData={objectData}
maxWidth={maxWidth}
loading={loading}
rollups={rollups}
/>
)
}
case 'state': { case 'state': {
if (value && value?.type) { if (value && value?.type) {
return <StateDisplay {...rest} state={value} /> return <StateDisplay {...rest} state={value} />
@ -419,6 +438,9 @@ const ObjectProperty = ({
) )
} }
} }
case 'miscId': {
return <MiscId value={value} {...rest} />
}
case 'density': { case 'density': {
if (value != null) { if (value != null) {
return <Text {...textParams}>{`${value} g/cm³`}</Text> return <Text {...textParams}>{`${value} g/cm³`}</Text>
@ -432,7 +454,14 @@ const ObjectProperty = ({
} }
case 'alerts': { case 'alerts': {
if (value != null && value?.length != 0) { if (value != null && value?.length != 0) {
return <AlertsDisplay alerts={value} /> return (
<AlertsDisplay
alerts={value}
printerId={objectData._id}
showDismiss={false}
showActions={false}
/>
)
} else { } else {
return ( return (
<Text type='secondary' {...textParams}> <Text type='secondary' {...textParams}>
@ -546,6 +575,11 @@ const ObjectProperty = ({
margin: 0, margin: 0,
...(mergedFormItemProps.style || {}) ...(mergedFormItemProps.style || {})
} }
if (typeof onChange === 'function') {
mergedFormItemProps.onChange = onChange
}
switch (type) { switch (type) {
case 'netGross': case 'netGross':
return ( return (
@ -736,7 +770,7 @@ const ObjectProperty = ({
case 'objectType': case 'objectType':
return ( return (
<Form.Item name={formItemName} {...mergedFormItemProps}> <Form.Item name={formItemName} {...mergedFormItemProps}>
<ObjectTypeSelect disabled={disabled} /> <ObjectTypeSelect disabled={disabled} masterFilter={masterFilter} />
</Form.Item> </Form.Item>
) )
case 'objectList': case 'objectList':
@ -775,6 +809,18 @@ const ObjectProperty = ({
/> />
</Form.Item> </Form.Item>
) )
case 'objectChildren': {
return (
<ObjectChildTable
value={value}
properties={properties}
objectData={objectData}
isEditing={true}
formListName={formItemName}
rollups={rollups}
/>
)
}
default: default:
return ( return (
<Form.Item name={formItemName} {...mergedFormItemProps}> <Form.Item name={formItemName} {...mergedFormItemProps}>
@ -815,7 +861,9 @@ ObjectProperty.propTypes = {
showPreview: PropTypes.bool, showPreview: PropTypes.bool,
showHyperlink: PropTypes.bool, showHyperlink: PropTypes.bool,
options: PropTypes.array, options: PropTypes.array,
showSince: PropTypes.bool showSince: PropTypes.bool,
loading: PropTypes.bool,
rollups: PropTypes.arrayOf(PropTypes.object)
} }
export default ObjectProperty export default ObjectProperty

View File

@ -14,8 +14,16 @@ import { AuthContext } from '../context/AuthContext'
import ObjectProperty from './ObjectProperty' import ObjectProperty from './ObjectProperty'
import { getModelByName } from '../../../database/ObjectModels' import { getModelByName } from '../../../database/ObjectModels'
import merge from 'lodash/merge' import merge from 'lodash/merge'
import { getModelProperty } from '../../../database/ObjectModels'
const { SHOW_CHILD } = TreeSelect const { SHOW_CHILD } = TreeSelect
// Helper to check if two values are equal (handling objects/ids)
const areValuesEqual = (v1, v2) => {
const id1 = v1 && typeof v1 === 'object' && v1._id ? v1._id : v1
const id2 = v2 && typeof v2 === 'object' && v2._id ? v2._id : v2
return String(id1) === String(id2)
}
const ObjectSelect = ({ const ObjectSelect = ({
type = 'unknown', type = 'unknown',
showSearch = false, showSearch = false,
@ -31,7 +39,7 @@ const ObjectSelect = ({
const { token } = useContext(AuthContext) const { token } = useContext(AuthContext)
// --- State --- // --- State ---
const [treeData, setTreeData] = useState([]) const [treeData, setTreeData] = useState([])
const [objectPropertiesTree, setObjectPropertiesTree] = useState({}) const [objectPropertiesTree, setObjectPropertiesTree] = useState([])
const [initialized, setInitialized] = useState(false) const [initialized, setInitialized] = useState(false)
const [error, setError] = useState(false) const [error, setError] = useState(false)
const properties = useMemo(() => getModelByName(type).group || [], [type]) const properties = useMemo(() => getModelByName(type).group || [], [type])
@ -69,6 +77,50 @@ const ObjectSelect = ({
[isMinimalObject, fetchObject, type] [isMinimalObject, fetchObject, type]
) )
const mergeGroups = useCallback((current, incoming) => {
if (!current) return incoming
if (!incoming) return current
if (!Array.isArray(current) || !Array.isArray(incoming)) return incoming
const merged = [...current]
// Helper to generate a unique key for a group node
const getGroupKey = (item) => {
const val = item.value
const valPart =
val && typeof val === 'object' && val._id
? val._id
: JSON.stringify(val)
return `${item.property}:${valPart}`
}
for (const item of incoming) {
if (item.property && item.value !== undefined) {
// It's a group node
const itemKey = getGroupKey(item)
const existingIdx = merged.findIndex(
(x) =>
x.property && x.value !== undefined && getGroupKey(x) === itemKey
)
if (existingIdx > -1) {
merged[existingIdx] = {
...merged[existingIdx],
children: mergeGroups(merged[existingIdx].children, item.children)
}
} else {
merged.push(item)
}
} else {
// It's a leaf object
if (!merged.some((x) => String(x._id) === String(item._id))) {
merged.push(item)
}
}
}
return merged
}, [])
// Fetch the object properties tree from the API // Fetch the object properties tree from the API
const handleFetchObjectsProperties = useCallback( const handleFetchObjectsProperties = useCallback(
async (customFilter = filter) => { async (customFilter = filter) => {
@ -78,11 +130,14 @@ const ObjectSelect = ({
filter: customFilter, filter: customFilter,
masterFilter masterFilter
}) })
if (Array.isArray(data)) { if (Array.isArray(data)) {
setObjectPropertiesTree((prev) => merge([], prev, data)) setObjectPropertiesTree((prev) => mergeGroups(prev, data))
} else { } else {
setObjectPropertiesTree((prev) => merge({}, prev, data)) // Fallback if API returns something unexpected
setObjectPropertiesTree((prev) => merge([], prev, data))
} }
setInitialLoading(false) setInitialLoading(false)
setError(false) setError(false)
return data return data
@ -92,24 +147,31 @@ const ObjectSelect = ({
return null return null
} }
}, },
[type, fetchObjectsByProperty, properties, filter, masterFilter] [
type,
fetchObjectsByProperty,
properties,
filter,
masterFilter,
mergeGroups
]
) )
// Convert the API response to AntD TreeSelect treeData // Convert the API response to AntD TreeSelect treeData
const buildTreeData = useCallback( const buildTreeData = useCallback(
(data, pIdx = 0, parentKeys = [], filterPath = []) => { (data, pIdx = 0, parentKeys = [], filterPath = []) => {
if (!data) return [] if (!data || !Array.isArray(data)) return []
if (Array.isArray(data)) { console.log(data, pIdx, properties.length)
// If we are past the grouping properties, these are leaf objects
if (pIdx >= properties.length) {
return data.map((object) => { return data.map((object) => {
setObjectList((prev) => { setObjectList((prev) => {
const filtered = prev.filter( if (prev.some((p) => p._id === object._id)) return prev
(prevObject) => prevObject._id != object._id return [...prev, object]
)
return [...filtered, object]
}) })
return { return {
title: ( title: (
<div style={{ paddingTop: '2px' }}> <div style={{ paddingTop: 0 }}>
<ObjectProperty <ObjectProperty
key={object._id} key={object._id}
type='object' type='object'
@ -123,48 +185,63 @@ const ObjectSelect = ({
value: object._id, value: object._id,
key: object._id, key: object._id,
isLeaf: true, isLeaf: true,
property: properties[pIdx - 1], // previous property
parentKeys, parentKeys,
filterPath filterPath
} }
}) })
} }
if (typeof data == 'object') {
const property = properties[pIdx] || null // Group Nodes
return Object.entries(data) return data
.map(([key, value]) => { .map((group) => {
if (property != null && typeof value === 'object') { // Only process if it looks like a group
const newFilterPath = filterPath.concat({ property, value: key }) if (!group.property) return null
return {
title: <ObjectProperty type={property} value={key} />, const { property, value, children } = group
value: parentKeys.concat(key).join('-'), var valueString = value
filterValue: key, if (value && typeof value === 'object' && value._id) {
key: parentKeys.concat(key).join('-'), valueString = value._id
}
if (Array.isArray(valueString)) {
valueString = valueString.join(',')
}
const nodeKey = parentKeys
.concat(property + ':' + valueString)
.join('-')
const newFilterPath = filterPath.concat({
property, property,
parentKeys: parentKeys.concat(key || '-'), value: valueString
})
const modelProperty = getModelProperty(type, property)
return {
title: <ObjectProperty {...modelProperty} value={value} />,
value: nodeKey,
key: nodeKey,
property,
filterValue: valueString,
parentKeys: parentKeys.concat(valueString),
filterPath: newFilterPath, filterPath: newFilterPath,
selectable: false, selectable: false,
isLeaf: false,
children: buildTreeData( children: buildTreeData(
value, children,
pIdx + 1, pIdx + 1,
parentKeys.concat(key), parentKeys.concat(valueString),
newFilterPath newFilterPath
), )
isLeaf: false
}
} }
}) })
.filter(Boolean) .filter(Boolean)
}
}, },
[properties, type] [properties, type]
) )
// --- loadData for async loading on expand --- // --- loadData for async loading on expand ---
const loadData = async (node) => { const loadData = async (node) => {
// node.property is the property name, node.value is the value // node.property is the property name, node.value is the value key
if (!node.property) return if (!node.property) return
if (type == 'unknown') return
// Build filter for this node by merging all parent property-value pairs // Build filter for this node by merging all parent property-value pairs
const customFilter = { ...filter } const customFilter = { ...filter }
if (Array.isArray(node.filterPath)) { if (Array.isArray(node.filterPath)) {
@ -172,26 +249,40 @@ const ObjectSelect = ({
customFilter[property] = value customFilter[property] = value
}) })
} }
// Ensure current node is in filter (should be covered by filterPath, but redundancy is safe)
customFilter[node.property] = node.filterValue customFilter[node.property] = node.filterValue
// Fetch children for this node // Fetch children for this node
const data = await handleFetchObjectsProperties(customFilter) const data = await handleFetchObjectsProperties(customFilter)
if (!data) return if (!data) return
// Extract only the children for the specific node that was expanded
let nodeSpecificData = data // Navigate to the specific node's children in the response
if (typeof data === 'object' && !Array.isArray(data)) { let nodeSpecificChildren = data
// If the API returns an object with multiple keys, get only the data for this node
nodeSpecificData = data[node.value] || {} if (node.filterPath && Array.isArray(node.filterPath)) {
for (const pathItem of node.filterPath) {
if (!Array.isArray(nodeSpecificChildren)) break
const match = nodeSpecificChildren.find(
(g) =>
g.property === pathItem.property &&
areValuesEqual(g.value, pathItem.value)
)
if (match) {
nodeSpecificChildren = match.children
} else {
nodeSpecificChildren = []
break
} }
}
}
// Build new tree children only for this specific node // Build new tree children only for this specific node
const children = buildTreeData( const children = buildTreeData(
nodeSpecificData, nodeSpecificChildren,
properties.indexOf(node.property) + 1, properties.indexOf(node.property) + 1,
node.parentKeys || [], node.parentKeys || [],
(node.filterPath || []).concat({ node.filterPath
property: node.property,
value: node.filterValue
})
) )
// Update treeData with new children for this node only // Update treeData with new children for this node only
setTreeData((prevTreeData) => { setTreeData((prevTreeData) => {
// Helper to recursively update the correct node // Helper to recursively update the correct node
@ -250,7 +341,8 @@ const ObjectSelect = ({
value && value &&
typeof value === 'object' && typeof value === 'object' &&
value !== null && value !== null &&
!initialized !initialized &&
type != 'unknown'
) { ) {
// Check if value is a minimal object and fetch full object if needed // Check if value is a minimal object and fetch full object if needed
const fullValue = await fetchFullObjectIfNeeded(value) const fullValue = await fetchFullObjectIfNeeded(value)
@ -260,7 +352,13 @@ const ObjectSelect = ({
properties.forEach((prop) => { properties.forEach((prop) => {
if (Object.prototype.hasOwnProperty.call(fullValue, prop)) { if (Object.prototype.hasOwnProperty.call(fullValue, prop)) {
const filterValue = fullValue[prop] const filterValue = fullValue[prop]
if (filterValue?.name) { if (
filterValue &&
typeof filterValue === 'object' &&
filterValue._id
) {
valueFilter[prop] = filterValue._id
} else if (filterValue?.name) {
valueFilter[prop] = filterValue.name valueFilter[prop] = filterValue.name
} else if (Array.isArray(filterValue)) { } else if (Array.isArray(filterValue)) {
valueFilter[prop] = filterValue.join(',') valueFilter[prop] = filterValue.join(',')
@ -275,12 +373,13 @@ const ObjectSelect = ({
setInitialized(true) setInitialized(true)
return return
} }
if (!initialized && token != null) { if (!initialized && token != null && type != 'unknown') {
handleFetchObjectsProperties() handleFetchObjectsProperties()
setInitialized(true) setInitialized(true)
} }
if (value == null) { if (value == null || type == 'unknown') {
setTreeSelectValue(null) setTreeSelectValue(null)
setInitialLoading(false)
setInitialized(true) setInitialized(true)
} }
} }
@ -292,7 +391,8 @@ const ObjectSelect = ({
handleFetchObjectsProperties, handleFetchObjectsProperties,
initialized, initialized,
token, token,
fetchFullObjectIfNeeded fetchFullObjectIfNeeded,
type
]) ])
const prevValuesRef = useRef({ type, masterFilter }) const prevValuesRef = useRef({ type, masterFilter })
@ -341,6 +441,14 @@ const ObjectSelect = ({
} }
}, [value]) }, [value])
const placeholder = useMemo(
() =>
type == 'unknown'
? 'n/a'
: `Select a ${getModelByName(type).label.toLowerCase()}...`,
[type]
)
// --- Error UI --- // --- Error UI ---
if (error) { if (error) {
return ( return (
@ -373,12 +481,12 @@ const ObjectSelect = ({
multiple={multiple} multiple={multiple}
loadData={loadData} loadData={loadData}
showCheckedStrategy={SHOW_CHILD} showCheckedStrategy={SHOW_CHILD}
placeholder={`Select a ${getModelByName(type).label.toLowerCase()}...`} placeholder={placeholder}
{...treeSelectProps} {...treeSelectProps}
{...rest} {...rest}
value={treeSelectValue} value={treeSelectValue}
onChange={onTreeSelectChange} onChange={onTreeSelectChange}
disabled={disabled} disabled={disabled || type == 'unknown'}
/> />
) )
} }

View File

@ -10,11 +10,18 @@ const ObjectTypeSelect = ({
placeholder = 'Select object type...', placeholder = 'Select object type...',
showSearch = true, showSearch = true,
allowClear = true, allowClear = true,
disabled = false disabled = false,
masterFilter = null
}) => { }) => {
// Create options from object models // Create options from object models
const options = objectModels const options = objectModels
.sort((a, b) => a.label.localeCompare(b.label)) .sort((a, b) => a.label.localeCompare(b.label))
.filter((model) => {
if (masterFilter == null || Object.keys(masterFilter).length == 0) {
return true
}
return masterFilter.includes(model?.name)
})
.map((model) => ({ .map((model) => ({
value: model.name, value: model.name,
label: <ObjectTypeDisplay objectType={model.name} />, label: <ObjectTypeDisplay objectType={model.name} />,
@ -46,7 +53,8 @@ ObjectTypeSelect.propTypes = {
placeholder: PropTypes.string, placeholder: PropTypes.string,
showSearch: PropTypes.bool, showSearch: PropTypes.bool,
allowClear: PropTypes.bool, allowClear: PropTypes.bool,
disabled: PropTypes.bool disabled: PropTypes.bool,
masterFilter: PropTypes.object
} }
export default ObjectTypeSelect export default ObjectTypeSelect

View File

@ -70,7 +70,7 @@ const PrinterTemperaturePanel = ({
}, [temperatureData.bed?.target]) }, [temperatureData.bed?.target])
useEffect(() => { useEffect(() => {
if (id && connected) { if (id && connected == true) {
const temperatureEventUnsubscribe = subscribeToObjectEvent( const temperatureEventUnsubscribe = subscribeToObjectEvent(
id, id,
'printer', 'printer',

View File

@ -58,6 +58,7 @@ const PropertyChanges = ({ type, value }) => {
longId={false} longId={false}
minimal={true} minimal={true}
objectData={value?.old} objectData={value?.old}
maxWidth='200px'
/> />
) : null} ) : null}
{value?.old && value?.new ? ( {value?.old && value?.new ? (
@ -71,6 +72,7 @@ const PropertyChanges = ({ type, value }) => {
longId={false} longId={false}
minimal={true} minimal={true}
objectData={value?.new} objectData={value?.new}
maxWidth='200px'
/> />
) : null} ) : null}
</Flex> </Flex>

View File

@ -0,0 +1,20 @@
import PropTypes from 'prop-types'
import SimpleBar from 'simplebar-react'
import 'simplebar-react/dist/simplebar.min.css'
const ScrollBox = ({ children, style, ...rest }) => {
return (
<div style={{ height: '100%', minHeight: '0' }}>
<SimpleBar style={{ height: '100%', ...style }} {...rest}>
{children}
</SimpleBar>
</div>
)
}
ScrollBox.propTypes = {
children: PropTypes.node,
style: PropTypes.object
}
export default ScrollBox

View File

@ -18,8 +18,12 @@ const StateDisplay = ({
'processing', 'processing',
'queued', 'queued',
'printing', 'printing',
'used' 'used',
'deploying'
] ]
const orangeProgressTypes = ['used', 'deploying', 'queued']
const activeProgressTypes = ['printing', 'deploying']
const currentState = state || { const currentState = state || {
type: 'unknown', type: 'unknown',
progress: 0 progress: 0
@ -39,8 +43,12 @@ const StateDisplay = ({
currentState?.progress > 0 ? ( currentState?.progress > 0 ? (
<Progress <Progress
percent={Math.round(currentState.progress * 100)} percent={Math.round(currentState.progress * 100)}
status={currentState.type === 'used' ? '' : 'active'} status={
strokeColor={currentState.type === 'used' ? 'orange' : ''} activeProgressTypes.includes(currentState.type) ? 'active' : ''
}
strokeColor={
orangeProgressTypes.includes(currentState.type) ? 'orange' : ''
}
style={{ width: '150px', marginBottom: '2px' }} style={{ width: '150px', marginBottom: '2px' }}
/> />
) : null} ) : null}

View File

@ -92,6 +92,10 @@ const StateTag = ({ state, showBadge = true, style = {} }) => {
status = 'warning' status = 'warning'
text = 'Used' text = 'Used'
break break
case 'unconsumed':
status = 'success'
text = 'Unconsumed'
break
default: default:
status = 'default' status = 'default'
text = state || 'Unknown' text = state || 'Unknown'

View File

@ -16,6 +16,7 @@ import CheckCircleIcon from '../../Icons/CheckCircleIcon.jsx'
import ObjectProperty from '../common/ObjectProperty.jsx' import ObjectProperty from '../common/ObjectProperty.jsx'
import TemplatePreview from './TemplatePreview.jsx' import TemplatePreview from './TemplatePreview.jsx'
import DataTree from './DataTree.jsx' import DataTree from './DataTree.jsx'
//import { useMediaQuery } from 'react-responsive'
const TemplateEditor = ({ const TemplateEditor = ({
objectData, objectData,
@ -28,6 +29,7 @@ const TemplateEditor = ({
const [testObjectViewMode, setTestObjectViewMode] = useState('Tree') const [testObjectViewMode, setTestObjectViewMode] = useState('Tree')
const [previewMessage, setPreviewMessage] = useState('No issues found.') const [previewMessage, setPreviewMessage] = useState('No issues found.')
const [previewError, setPreviewError] = useState(false) const [previewError, setPreviewError] = useState(false)
//const isMobile = useMediaQuery({ maxWidth: 768 })
const handlePreviewMessage = (message, isError) => { const handlePreviewMessage = (message, isError) => {
setPreviewMessage(message) setPreviewMessage(message)
@ -36,7 +38,7 @@ const TemplateEditor = ({
return ( return (
<> <>
<Splitter className={'farmcontrol-splitter'}> <Splitter className={'farmcontrol-splitter'} vertical={true}>
{collapseState.preview == true && ( {collapseState.preview == true && (
<Splitter.Panel style={{ height: '100%' }}> <Splitter.Panel style={{ height: '100%' }}>
<Card <Card

View File

@ -58,10 +58,10 @@ const WizardView = ({
gap={'middle'} gap={'middle'}
style={ style={
sideBarGrow == false sideBarGrow == false
? { width: '100%' } ? { width: '100%', minWidth: 0 }
: isMobile : isMobile
? { width: '100%' } ? { width: '100%', minWidth: 0 }
: { width: '400px' } : { width: '400px', minWidth: 0 }
} }
> >
<Flex vertical gap='middle' style={{ flexGrow: 1, width: '100%' }}> <Flex vertical gap='middle' style={{ flexGrow: 1, width: '100%' }}>

View File

@ -0,0 +1,316 @@
import { Input, Flex, List, Typography, Modal, Tag } from 'antd'
import {
createContext,
useContext,
useEffect,
useMemo,
useRef,
useState
} from 'react'
import PropTypes from 'prop-types'
import { useLocation, useNavigate } from 'react-router-dom'
import { getModelByName } from '../../../database/ObjectModels'
const ActionsModalContext = createContext()
// Remove the "action" query param from a URL so we don't navigate to the same URL again
const stripActionParam = (pathname, search) => {
const params = new URLSearchParams(search)
params.delete('action')
const query = params.toString()
return pathname + (query ? `?${query}` : '')
}
// Flatten nested actions (including children) into a single list
const flattenActions = (actions, parentLabel = '') => {
if (!Array.isArray(actions)) return []
const flat = []
actions.forEach((action) => {
if (!action || action.type === 'divider') {
return
}
const hasUrl = typeof action.url === 'function'
const hasChildren =
Array.isArray(action.children) && action.children.length > 0
const currentLabel = action.label || action.name || ''
const fullLabel = parentLabel
? `${parentLabel} / ${currentLabel}`
: currentLabel
// Only push actions that are actually runnable
if (hasUrl) {
flat.push({
...action,
key: action.key || action.name || fullLabel,
fullLabel
})
}
if (hasChildren) {
flat.push(...flattenActions(action.children, fullLabel))
}
})
return flat
}
const ActionsModalProvider = ({ children }) => {
const { Text } = Typography
const navigate = useNavigate()
const location = useLocation()
const [visible, setVisible] = useState(false)
const [query, setQuery] = useState('')
const [context, setContext] = useState({
id: null,
type: null,
objectData: null
})
const inputRef = useRef(null)
const showActionsModal = (id, type, objectData = null) => {
setContext({ id, type, objectData })
setQuery('')
setVisible(true)
}
const hideActionsModal = () => {
setVisible(false)
setQuery('')
}
// Focus and select text in input when modal becomes visible
useEffect(() => {
// Use a small timeout to ensure the modal is fully rendered and visible
setTimeout(() => {
if (visible) {
console.log('visible', visible)
console.log('inputRef.current', inputRef.current)
if (visible && inputRef.current) {
console.log('focusing input')
const input = inputRef.current.input
if (input) {
input.focus()
input.select() // Select all text
}
}
}
}, 50)
}, [visible])
const model = context.type ? getModelByName(context.type) : null
const ModelIcon = model?.icon || null
const modelLabel = model?.label || model?.name || ''
const flattenedActions = useMemo(
() => flattenActions(model?.actions || []),
[model]
)
const currentUrlWithoutActions = stripActionParam(
location.pathname,
location.search
)
const getActionDisabled = (action) => {
const { id, objectData } = context
if (!action) return true
let disabled = false
const url = action.url ? action.url(id) : undefined
// Match ObjectActions default disabling behaviour
if (url && url === currentUrlWithoutActions) {
disabled = true
}
if (typeof action.disabled !== 'undefined') {
if (typeof action.disabled === 'function') {
disabled = action.disabled(objectData)
} else {
disabled = action.disabled
}
}
return disabled
}
const getVisibleDisabled = (action) => {
const { objectData } = context
if (!action) return true
let visible = true
if (typeof action.visible !== 'undefined') {
if (typeof action.visible === 'function') {
visible = action.visible(objectData)
} else {
visible = action.visible
}
}
return visible
}
const normalizedQuery = query.trim().toLowerCase()
const filteredActions = flattenedActions.filter((action) => {
if (!normalizedQuery) return true
const haystack = [
action.fullLabel || '',
action.label || '',
action.name || '',
modelLabel
]
.join(' ')
.toLowerCase()
return haystack.includes(normalizedQuery)
})
const runAction = (action) => {
if (!action || typeof action.url !== 'function') return
if (getActionDisabled(action)) return
const { id } = context
const targetUrl = action.url(id)
if (targetUrl && targetUrl !== '#') {
navigate(targetUrl)
hideActionsModal()
}
}
const handleKeyDown = (e) => {
if (!filteredActions.length) return
// Enter triggers first visible action
if (e.key === 'Enter') {
e.preventDefault()
const first = filteredActions[0]
runAction(first)
return
}
// Number keys 1-9 trigger corresponding actions
if (/^[1-9]$/.test(e.key)) {
e.preventDefault()
const index = parseInt(e.key, 10)
if (index < filteredActions.length) {
const action = filteredActions[index]
runAction(action)
}
}
}
return (
<ActionsModalContext.Provider value={{ showActionsModal }}>
<Modal
open={visible}
onCancel={hideActionsModal}
closeIcon={null}
footer={null}
width={700}
styles={{ content: { padding: 0 } }}
destroyOnClose={true}
>
<Flex vertical>
<Input
ref={inputRef}
addonBefore={
<Text style={{ fontSize: '20px' }}>
<ModelIcon />
</Text>
}
placeholder='Search actions...'
size='large'
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
/>
{filteredActions.length > 0 && (
<div
style={{
marginLeft: '14px',
marginRight: '14px'
}}
>
<List
dataSource={filteredActions.filter((action) =>
getVisibleDisabled(action)
)}
renderItem={(action, index) => {
const Icon = action.icon
const disabled = getActionDisabled(action)
let shortcutText = ''
if (index === 0) {
shortcutText = 'ENTER'
} else if (index <= 9) {
shortcutText = index.toString()
}
return (
<List.Item
onClick={() => !disabled && runAction(action)}
style={{
cursor: disabled ? 'not-allowed' : 'pointer',
opacity: disabled ? 0.5 : 1
}}
>
<Flex
gap='middle'
style={{ width: '100%' }}
align='center'
justify='space-between'
>
<Flex
gap='small'
align='center'
style={{ flexGrow: 1, minWidth: 0 }}
>
{Icon ? <Icon style={{ fontSize: '18px' }} /> : null}
<Flex vertical style={{ flexGrow: 1, minWidth: 0 }}>
<Text
ellipsis
style={{
maxWidth: 320,
width: '100%'
}}
>
{action.fullLabel || action.label || action.name}
</Text>
</Flex>
</Flex>
<Flex gap='small' align='center'>
{action.danger && <Tag color='red'>Danger</Tag>}
{shortcutText && <Text keyboard>{shortcutText}</Text>}
</Flex>
</Flex>
</List.Item>
)
}}
/>
</div>
)}
</Flex>
</Modal>
{children}
</ActionsModalContext.Provider>
)
}
ActionsModalProvider.propTypes = {
children: PropTypes.node.isRequired
}
const useActionsModal = () => useContext(ActionsModalContext)
// eslint-disable-next-line react-refresh/only-export-components
export { ActionsModalProvider, ActionsModalContext, useActionsModal }

View File

@ -70,6 +70,11 @@ const ApiServerProvider = ({ children }) => {
[userProfile?._id] [userProfile?._id]
) )
const clearSubscriptions = useCallback(() => {
subscribedCallbacksRef.current.clear()
subscribedLockCallbacksRef.current.clear()
}, [])
const connectToServer = useCallback(() => { const connectToServer = useCallback(() => {
if (token && authenticated == true) { if (token && authenticated == true) {
logger.debug('Token is available, connecting to api server...') logger.debug('Token is available, connecting to api server...')
@ -101,6 +106,7 @@ const ApiServerProvider = ({ children }) => {
newSocket.on('disconnect', () => { newSocket.on('disconnect', () => {
logger.debug('Api Server disconnected') logger.debug('Api Server disconnected')
setError('Api Server disconnected') setError('Api Server disconnected')
clearSubscriptions()
setConnected(false) setConnected(false)
}) })
@ -108,16 +114,10 @@ const ApiServerProvider = ({ children }) => {
logger.error('Api Server connection error:', err) logger.error('Api Server connection error:', err)
messageApi.error('Api Server connection error: ' + err.message) messageApi.error('Api Server connection error: ' + err.message)
setError('Api Server connection error') setError('Api Server connection error')
clearSubscriptions()
setConnected(false) setConnected(false)
}) })
newSocket.on('bridge.notification', (data) => {
notificationApi[data.type]({
title: data.title,
message: data.message
})
})
newSocket.on('error', (err) => { newSocket.on('error', (err) => {
logger.error('Api Server error:', err) logger.error('Api Server error:', err)
setError('Api Server error') setError('Api Server error')
@ -445,6 +445,7 @@ const ApiServerProvider = ({ children }) => {
(id, objectType, eventType, callback) => { (id, objectType, eventType, callback) => {
if (socketRef.current && socketRef.current.connected == true) { if (socketRef.current && socketRef.current.connected == true) {
const callbacksRefKey = `${objectType}:${id}:events:${eventType}` const callbacksRefKey = `${objectType}:${id}:events:${eventType}`
// Remove callback from the subscribed callbacks map // Remove callback from the subscribed callbacks map
if (subscribedCallbacksRef.current.has(callbacksRefKey)) { if (subscribedCallbacksRef.current.has(callbacksRefKey)) {
const callbacks = subscribedCallbacksRef.current const callbacks = subscribedCallbacksRef.current
@ -452,6 +453,7 @@ const ApiServerProvider = ({ children }) => {
.filter((cb) => cb !== callback) .filter((cb) => cb !== callback)
if (callbacks.length === 0) { if (callbacks.length === 0) {
subscribedCallbacksRef.current.delete(callbacksRefKey) subscribedCallbacksRef.current.delete(callbacksRefKey)
console.log('Unsubscribing from object event:', callbacksRefKey)
socketRef.current.emit('unsubscribeObjectEvent', { socketRef.current.emit('unsubscribeObjectEvent', {
_id: id, _id: id,
objectType, objectType,
@ -479,6 +481,7 @@ const ApiServerProvider = ({ children }) => {
subscribedCallbacksRef.current.get(callbacksRefKey).length subscribedCallbacksRef.current.get(callbacksRefKey).length
if (callbacksLength <= 0) { if (callbacksLength <= 0) {
console.log('Subscribing to object event:', callbacksRefKey)
socketRef.current.emit( socketRef.current.emit(
'subscribeToObjectEvent', 'subscribeToObjectEvent',
{ {
@ -662,7 +665,12 @@ const ApiServerProvider = ({ children }) => {
`${config.backendUrl}/${type.toLowerCase()}s/properties`, `${config.backendUrl}/${type.toLowerCase()}s/properties`,
{ {
params: { params: {
...filter, ...Object.keys(filter).reduce((acc, key) => {
acc[key] = Array.isArray(filter[key])
? filter[key].join(',')
: filter[key]
return acc
}, {}),
properties: properties.join(','), // Convert array to comma-separated string properties: properties.join(','), // Convert array to comma-separated string
masterFilter: JSON.stringify(masterFilter) masterFilter: JSON.stringify(masterFilter)
}, },
@ -932,7 +940,11 @@ const ApiServerProvider = ({ children }) => {
} }
// Upload file to the API // Upload file to the API
const uploadFile = async (file, additionalData = {}) => { const uploadFile = async (
file,
additionalData = {},
progressCallback = null
) => {
const uploadUrl = `${config.backendUrl}/files` const uploadUrl = `${config.backendUrl}/files`
logger.debug('Uploading file:', file.name, 'to:', uploadUrl) logger.debug('Uploading file:', file.name, 'to:', uploadUrl)
@ -955,6 +967,9 @@ const ApiServerProvider = ({ children }) => {
(progressEvent.loaded * 100) / progressEvent.total (progressEvent.loaded * 100) / progressEvent.total
) )
logger.debug(`Upload progress: ${percentCompleted}%`) logger.debug(`Upload progress: ${percentCompleted}%`)
if (progressCallback) {
progressCallback(percentCompleted)
}
} }
}) })
@ -963,7 +978,7 @@ const ApiServerProvider = ({ children }) => {
} catch (err) { } catch (err) {
console.error('File upload error:', err) console.error('File upload error:', err)
showError(err, () => { showError(err, () => {
uploadFile(file, additionalData) uploadFile(file, additionalData, progressCallback)
}) })
return null return null
} }

View File

@ -0,0 +1,58 @@
import { createContext, useContext } from 'react'
import PropTypes from 'prop-types'
import { message } from 'antd'
const MessageContext = createContext()
export const MessageProvider = ({ children }) => {
const [msgApi, contextHolder] = message.useMessage()
const showMessage = (type, content, options = {}) => {
return msgApi.open({
type,
content,
...options
})
}
const showSuccess = (content, options = {}) =>
showMessage('success', content, options)
const showInfo = (content, options = {}) =>
showMessage('info', content, options)
const showWarning = (content, options = {}) =>
showMessage('warning', content, options)
const showError = (content, options = {}) =>
showMessage('error', content, options)
const showLoading = (content, options = {}) =>
showMessage('loading', content, options)
return (
<MessageContext.Provider
value={{
msgApi,
showSuccess,
showInfo,
showWarning,
showError,
showLoading
}}
>
{contextHolder}
{children}
</MessageContext.Provider>
)
}
MessageProvider.propTypes = {
children: PropTypes.node.isRequired
}
export const useMessageContext = () => {
const context = useContext(MessageContext)
if (!context) {
throw new Error('useMessageContext must be used within a MessageProvider')
}
return context
}
export { MessageContext }

View File

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

View File

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

View File

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

View File

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

View File

@ -7,6 +7,8 @@ import { Job } from './models/Job'
import { Product } from './models/Product' import { Product } from './models/Product'
import { Part } from './models/Part.js' import { Part } from './models/Part.js'
import { Vendor } from './models/Vendor' import { Vendor } from './models/Vendor'
import { Courier } from './models/Courier'
import { CourierService } from './models/CourierService'
import { File } from './models/File' import { File } from './models/File'
import { SubJob } from './models/SubJob' import { SubJob } from './models/SubJob'
import { Initial } from './models/Initial' import { Initial } from './models/Initial'
@ -15,6 +17,7 @@ import { StockEvent } from './models/StockEvent'
import { StockAudit } from './models/StockAudit' import { StockAudit } from './models/StockAudit'
import { PartStock } from './models/PartStock' import { PartStock } from './models/PartStock'
import { ProductStock } from './models/ProductStock' import { ProductStock } from './models/ProductStock'
import { PurchaseOrder } from './models/PurchaseOrder'
import { AuditLog } from './models/AuditLog' import { AuditLog } from './models/AuditLog'
import { User } from './models/User' import { User } from './models/User'
import { NoteType } from './models/NoteType' import { NoteType } from './models/NoteType'
@ -35,6 +38,8 @@ export const objectModels = [
Product, Product,
Part, Part,
Vendor, Vendor,
Courier,
CourierService,
File, File,
SubJob, SubJob,
Initial, Initial,
@ -43,6 +48,7 @@ export const objectModels = [
StockAudit, StockAudit,
PartStock, PartStock,
ProductStock, ProductStock,
PurchaseOrder,
AuditLog, AuditLog,
User, User,
NoteType, NoteType,
@ -64,6 +70,8 @@ export {
Product, Product,
Part, Part,
Vendor, Vendor,
Courier,
CourierService,
File, File,
SubJob, SubJob,
Initial, Initial,
@ -72,6 +80,7 @@ export {
StockAudit, StockAudit,
PartStock, PartStock,
ProductStock, ProductStock,
PurchaseOrder,
AuditLog, AuditLog,
User, User,
NoteType, NoteType,

View File

@ -0,0 +1,142 @@
import CourierIcon from '../../components/Icons/CourierIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import EditIcon from '../../components/Icons/EditIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon'
import BinIcon from '../../components/Icons/BinIcon'
export const Courier = {
name: 'courier',
label: 'Courier',
prefix: 'COR',
icon: CourierIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/management/couriers/info?courierId=${_id}`
},
{
name: 'reload',
label: 'Reload',
icon: ReloadIcon,
url: (_id) =>
`/dashboard/management/couriers/info?courierId=${_id}&action=reload`
},
{
name: 'edit',
label: 'Edit',
row: true,
icon: EditIcon,
url: (_id) =>
`/dashboard/management/couriers/info?courierId=${_id}&action=edit`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
}
},
{
name: 'finishEdit',
label: 'Save Edits',
icon: CheckIcon,
url: (_id) =>
`/dashboard/management/couriers/info?courierId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'cancelEdit',
label: 'Cancel Edits',
icon: XMarkIcon,
url: (_id) =>
`/dashboard/management/couriers/info?courierId=${_id}&action=cancelEdit`,
visible: (objectData) => {
console.log(objectData?._isEditing)
return objectData?._isEditing && objectData?._isEditing == true
}
},
{ type: 'divider' },
{
name: 'delete',
label: 'Delete',
icon: BinIcon,
danger: true,
url: (_id) =>
`/dashboard/management/couriers/info?courierId=${_id}&action=delete`
}
],
columns: ['name', '_id', 'country', 'email', 'website', 'createdAt'],
filters: ['name', '_id', 'country', 'email'],
sorters: ['name', 'country', 'email', 'createdAt', '_id'],
group: ['country'],
properties: [
{
name: '_id',
label: 'ID',
columnFixed: 'left',
type: 'id',
objectType: 'courier',
showCopy: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
{
name: 'name',
label: 'Name',
columnFixed: 'left',
required: true,
type: 'text'
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true
},
{
name: 'contact',
label: 'Contact',
type: 'text',
readOnly: false,
required: false
},
{
name: 'country',
label: 'Country',
type: 'country',
readOnly: false,
required: false
},
{
name: 'email',
label: 'Email',
columnWidth: 300,
type: 'email',
readOnly: false,
required: false
},
{
name: 'phone',
label: 'Phone',
type: 'phone',
readOnly: false,
required: false
},
{
name: 'website',
label: 'Website',
columnWidth: 300,
type: 'url',
readOnly: false,
required: false
}
]
}

View File

@ -0,0 +1,170 @@
import CourierServiceIcon from '../../components/Icons/CourierServiceIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import EditIcon from '../../components/Icons/EditIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon'
import BinIcon from '../../components/Icons/BinIcon'
export const CourierService = {
name: 'courierService',
label: 'Courier Service',
prefix: 'COS',
icon: CourierServiceIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) =>
`/dashboard/management/courierservices/info?courierServiceId=${_id}`
},
{
name: 'reload',
label: 'Reload',
icon: ReloadIcon,
url: (_id) =>
`/dashboard/management/courierservices/info?courierServiceId=${_id}&action=reload`
},
{
name: 'edit',
label: 'Edit',
row: true,
icon: EditIcon,
url: (_id) =>
`/dashboard/management/courierservices/info?courierServiceId=${_id}&action=edit`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
}
},
{
name: 'finishEdit',
label: 'Save Edits',
icon: CheckIcon,
url: (_id) =>
`/dashboard/management/courierservices/info?courierServiceId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'cancelEdit',
label: 'Cancel Edits',
icon: XMarkIcon,
url: (_id) =>
`/dashboard/management/courierservices/info?courierServiceId=${_id}&action=cancelEdit`,
visible: (objectData) => {
console.log(objectData?._isEditing)
return objectData?._isEditing && objectData?._isEditing == true
}
},
{ type: 'divider' },
{
name: 'delete',
label: 'Delete',
icon: BinIcon,
danger: true,
url: (_id) =>
`/dashboard/management/courierservices/info?courierServiceId=${_id}&action=delete`
}
],
columns: [
'name',
'_id',
'courier',
'courier._id',
'tracked',
'deliveryTime',
'active'
],
filters: ['name', '_id', 'courier._id', 'active', 'deliveryTime', 'tracked'],
sorters: [
'name',
'courier._id',
'active',
'tracked',
'estimatedDeliveryTime',
'createdAt',
'_id'
],
group: ['courier'],
properties: [
{
name: '_id',
label: 'ID',
columnFixed: 'left',
type: 'id',
objectType: 'courierService',
showCopy: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
{
name: 'name',
label: 'Name',
columnFixed: 'left',
required: true,
type: 'text'
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true
},
{
name: 'courier',
label: 'Courier',
type: 'object',
objectType: 'courier',
showHyperlink: true,
required: true
},
{
name: 'courier._id',
label: 'Courier ID',
type: 'id',
objectType: 'courier',
showHyperlink: true
},
{
name: 'deliveryTime',
label: 'Delivery Time',
type: 'number',
readOnly: false,
required: false,
suffix: 'days',
columnWidth: 175
},
{
name: 'active',
label: 'Active',
type: 'bool',
readOnly: false,
required: true,
columnWidth: 150
},
{
name: 'tracked',
label: 'Tracked',
type: 'bool',
readOnly: false,
required: true,
columnWidth: 150
},
{
name: 'website',
label: 'Website',
columnWidth: 300,
type: 'url',
readOnly: false,
required: false
}
]
}

View File

@ -1,6 +1,8 @@
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon' import ReloadIcon from '../../components/Icons/ReloadIcon'
import EditIcon from '../../components/Icons/EditIcon' import EditIcon from '../../components/Icons/EditIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
import DocumentJobIcon from '../../components/Icons/DocumentJobIcon' import DocumentJobIcon from '../../components/Icons/DocumentJobIcon'
import dayjs from 'dayjs' import dayjs from 'dayjs'
@ -33,7 +35,31 @@ export const DocumentJob = {
row: true, row: true,
icon: EditIcon, icon: EditIcon,
url: (_id) => url: (_id) =>
`/dashboard/management/documentjobs/info?documentJobId=${_id}&action=edit` `/dashboard/management/documentjobs/info?documentJobId=${_id}&action=edit`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
}
},
{
name: 'finishEdit',
label: 'Save Edits',
icon: CheckIcon,
url: (_id) =>
`/dashboard/management/documentjobs/info?documentJobId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'cancelEdit',
label: 'Cancel Edits',
icon: XMarkIcon,
url: (_id) =>
`/dashboard/management/documentjobs/info?documentJobId=${_id}&action=cancelEdit`,
visible: (objectData) => {
console.log(objectData?._isEditing)
return objectData?._isEditing && objectData?._isEditing == true
}
} }
], ],
columns: ['name', '_id', 'state', 'createdAt', 'updatedAt'], columns: ['name', '_id', 'state', 'createdAt', 'updatedAt'],

View File

@ -2,6 +2,8 @@ import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon' import ReloadIcon from '../../components/Icons/ReloadIcon'
import EditIcon from '../../components/Icons/EditIcon' import EditIcon from '../../components/Icons/EditIcon'
import DocumentPrinterIcon from '../../components/Icons/DocumentPrinterIcon' import DocumentPrinterIcon from '../../components/Icons/DocumentPrinterIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
export const DocumentPrinter = { export const DocumentPrinter = {
name: 'documentPrinter', name: 'documentPrinter',
@ -32,7 +34,31 @@ export const DocumentPrinter = {
row: true, row: true,
icon: EditIcon, icon: EditIcon,
url: (_id) => url: (_id) =>
`/dashboard/management/documentprinters/info?documentPrinterId=${_id}&action=edit` `/dashboard/management/documentprinters/info?documentPrinterId=${_id}&action=edit`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
}
},
{
name: 'finishEdit',
label: 'Save Edits',
icon: CheckIcon,
url: (_id) =>
`/dashboard/management/documentprinters/info?documentPrinterId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'cancelEdit',
label: 'Cancel Edits',
icon: XMarkIcon,
url: (_id) =>
`/dashboard/management/documentprinters/info?documentPrinterId=${_id}&action=cancelEdit`,
visible: (objectData) => {
console.log(objectData?._isEditing)
return objectData?._isEditing && objectData?._isEditing == true
}
} }
], ],
columns: [ columns: [
@ -101,6 +127,21 @@ export const DocumentPrinter = {
type: 'bool', type: 'bool',
required: true required: true
}, },
{
name: 'vendor',
label: 'Vendor',
type: 'object',
objectType: 'vendor',
required: false
},
{
name: 'vendor._id',
label: 'Vendor ID',
type: 'id',
objectType: 'vendor',
showHyperlink: true,
readOnly: true
},
{ {
name: 'host', name: 'host',
label: 'Host', label: 'Host',

View File

@ -1,6 +1,8 @@
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon' import ReloadIcon from '../../components/Icons/ReloadIcon'
import EditIcon from '../../components/Icons/EditIcon' import EditIcon from '../../components/Icons/EditIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
import DocumentSizeIcon from '../../components/Icons/DocumentSizeIcon' import DocumentSizeIcon from '../../components/Icons/DocumentSizeIcon'
export const DocumentSize = { export const DocumentSize = {
@ -32,7 +34,31 @@ export const DocumentSize = {
row: true, row: true,
icon: EditIcon, icon: EditIcon,
url: (_id) => url: (_id) =>
`/dashboard/management/documentsizes/info?documentSizeId=${_id}&action=edit` `/dashboard/management/documentsizes/info?documentSizeId=${_id}&action=edit`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
}
},
{
name: 'finishEdit',
label: 'Save Edits',
icon: CheckIcon,
url: (_id) =>
`/dashboard/management/documentsizes/info?documentSizeId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'cancelEdit',
label: 'Cancel Edits',
icon: XMarkIcon,
url: (_id) =>
`/dashboard/management/documentsizes/info?documentSizeId=${_id}&action=cancelEdit`,
visible: (objectData) => {
console.log(objectData?._isEditing)
return objectData?._isEditing && objectData?._isEditing == true
}
} }
], ],
columns: [ columns: [

View File

@ -1,6 +1,8 @@
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon' import ReloadIcon from '../../components/Icons/ReloadIcon'
import EditIcon from '../../components/Icons/EditIcon' import EditIcon from '../../components/Icons/EditIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
import DesignIcon from '../../components/Icons/DesignIcon' import DesignIcon from '../../components/Icons/DesignIcon'
import DocumentTemplateIcon from '../../components/Icons/DocumentTemplateIcon' import DocumentTemplateIcon from '../../components/Icons/DocumentTemplateIcon'
@ -41,7 +43,31 @@ export const DocumentTemplate = {
row: true, row: true,
icon: EditIcon, icon: EditIcon,
url: (_id) => url: (_id) =>
`/dashboard/management/documenttemplates/info?documentTemplateId=${_id}&action=edit` `/dashboard/management/documenttemplates/info?documentTemplateId=${_id}&action=edit`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
}
},
{
name: 'finishEdit',
label: 'Save Edits',
icon: CheckIcon,
url: (_id) =>
`/dashboard/management/documenttemplates/info?documentTemplateId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'cancelEdit',
label: 'Cancel Edits',
icon: XMarkIcon,
url: (_id) =>
`/dashboard/management/documenttemplates/info?documentTemplateId=${_id}&action=cancelEdit`,
visible: (objectData) => {
console.log(objectData?._isEditing)
return objectData?._isEditing && objectData?._isEditing == true
}
} }
], ],
columns: [ columns: [

View File

@ -2,6 +2,8 @@ import EditIcon from '../../components/Icons/EditIcon'
import FilamentIcon from '../../components/Icons/FilamentIcon' import FilamentIcon from '../../components/Icons/FilamentIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon' import ReloadIcon from '../../components/Icons/ReloadIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
export const Filament = { export const Filament = {
name: 'filament', name: 'filament',
@ -30,7 +32,31 @@ export const Filament = {
row: true, row: true,
icon: EditIcon, icon: EditIcon,
url: (_id) => url: (_id) =>
`/dashboard/management/filaments/info?filamentId=${_id}&action=edit` `/dashboard/management/filaments/info?filamentId=${_id}&action=edit`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
}
},
{
name: 'finishEdit',
label: 'Save Edits',
icon: CheckIcon,
url: (_id) =>
`/dashboard/management/filaments/info?filamentId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'cancelEdit',
label: 'Cancel Edits',
icon: XMarkIcon,
url: (_id) =>
`/dashboard/management/filaments/info?filamentId=${_id}&action=cancelEdit`,
visible: (objectData) => {
console.log(objectData?._isEditing)
return objectData?._isEditing && objectData?._isEditing == true
}
} }
], ],
columns: [ columns: [

View File

@ -2,6 +2,8 @@ import DownloadIcon from '../../components/Icons/DownloadIcon'
import FileIcon from '../../components/Icons/FileIcon' import FileIcon from '../../components/Icons/FileIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import EditIcon from '../../components/Icons/EditIcon' import EditIcon from '../../components/Icons/EditIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon' import ReloadIcon from '../../components/Icons/ReloadIcon'
import BinIcon from '../../components/Icons/BinIcon' import BinIcon from '../../components/Icons/BinIcon'
@ -31,7 +33,31 @@ export const File = {
label: 'Edit', label: 'Edit',
row: true, row: true,
icon: EditIcon, icon: EditIcon,
url: (_id) => `/dashboard/management/files/info?fileId=${_id}&action=edit` url: (_id) => `/dashboard/management/files/info?fileId=${_id}&action=edit`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
}
},
{
name: 'finishEdit',
label: 'Save Edits',
icon: CheckIcon,
url: (_id) =>
`/dashboard/management/files/info?fileId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'cancelEdit',
label: 'Cancel Edits',
icon: XMarkIcon,
url: (_id) =>
`/dashboard/management/files/info?fileId=${_id}&action=cancelEdit`,
visible: (objectData) => {
console.log(objectData?._isEditing)
return objectData?._isEditing && objectData?._isEditing == true
}
}, },
{ {
name: 'download', name: 'download',

View File

@ -1,5 +1,7 @@
import DownloadIcon from '../../components/Icons/DownloadIcon' import DownloadIcon from '../../components/Icons/DownloadIcon'
import EditIcon from '../../components/Icons/EditIcon' import EditIcon from '../../components/Icons/EditIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
import GCodeFileIcon from '../../components/Icons/GCodeFileIcon' import GCodeFileIcon from '../../components/Icons/GCodeFileIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon' import ReloadIcon from '../../components/Icons/ReloadIcon'
@ -39,7 +41,31 @@ export const GCodeFile = {
row: true, row: true,
icon: EditIcon, icon: EditIcon,
url: (_id) => url: (_id) =>
`/dashboard/production/gcodefiles/info?gcodeFileId=${_id}&action=edit` `/dashboard/production/gcodefiles/info?gcodeFileId=${_id}&action=edit`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
}
},
{
name: 'finishEdit',
label: 'Save Edits',
icon: CheckIcon,
url: (_id) =>
`/dashboard/production/gcodefiles/info?gcodeFileId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'cancelEdit',
label: 'Cancel Edits',
icon: XMarkIcon,
url: (_id) =>
`/dashboard/production/gcodefiles/info?gcodeFileId=${_id}&action=cancelEdit`,
visible: (objectData) => {
console.log(objectData?._isEditing)
return objectData?._isEditing && objectData?._isEditing == true
}
} }
], ],
@ -127,9 +153,11 @@ export const GCodeFile = {
name: 'cost', name: 'cost',
label: 'Cost', label: 'Cost',
type: 'number', type: 'number',
roundNumber: 2,
value: (objectData) => { value: (objectData) => {
return ( return (
objectData?.file?.metaData?.filamentUsedG * objectData?.filament?.cost objectData?.file?.metaData?.filamentUsedG *
(objectData?.filament?.cost / 1000)
) )
}, },
readOnly: true, readOnly: true,
@ -196,6 +224,51 @@ export const GCodeFile = {
label: 'Print Profile', label: 'Print Profile',
type: 'text', type: 'text',
readOnly: true readOnly: true
},
{
name: 'parts',
label: 'Parts',
type: 'objectChildren',
objectType: 'part',
properties: [
{
name: 'part',
label: 'Part',
type: 'object',
objectType: 'part',
required: true
},
{
name: 'part._id',
label: 'Part ID',
type: 'id',
objectType: 'part',
showHyperlink: true,
value: (objectData) => {
return objectData?.part?._id
}
},
{
name: 'quantity',
label: 'Quantity',
type: 'number',
required: true
}
],
rollups: [
{
name: 'totalQuantity',
label: 'Total',
type: 'number',
property: 'quantity',
value: (objectData) => {
return objectData?.parts?.reduce(
(acc, part) => acc + part.quantity,
0
)
}
}
]
} }
] ]
} }

View File

@ -2,6 +2,8 @@ import HostIcon from '../../components/Icons/HostIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon' import ReloadIcon from '../../components/Icons/ReloadIcon'
import EditIcon from '../../components/Icons/EditIcon' import EditIcon from '../../components/Icons/EditIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
import OTPIcon from '../../components/Icons/OTPIcon' import OTPIcon from '../../components/Icons/OTPIcon'
export const Host = { export const Host = {
@ -38,7 +40,32 @@ export const Host = {
label: 'Edit', label: 'Edit',
row: true, row: true,
icon: EditIcon, icon: EditIcon,
url: (_id) => `/dashboard/management/hosts/info?hostId=${_id}&action=edit` url: (_id) =>
`/dashboard/management/hosts/info?hostId=${_id}&action=edit`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
}
},
{
name: 'finishEdit',
label: 'Save Edits',
icon: CheckIcon,
url: (_id) =>
`/dashboard/management/hosts/info?hostId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'cancelEdit',
label: 'Cancel Edits',
icon: XMarkIcon,
url: (_id) =>
`/dashboard/management/hosts/info?hostId=${_id}&action=cancelEdit`,
visible: (objectData) => {
console.log(objectData?._isEditing)
return objectData?._isEditing && objectData?._isEditing == true
}
} }
], ],
columns: ['name', '_id', 'state', 'tags', 'connectedAt'], columns: ['name', '_id', 'state', 'tags', 'connectedAt'],
@ -54,8 +81,8 @@ export const Host = {
showCopy: true showCopy: true
}, },
{ {
name: 'connectedAt', name: 'createdAt',
label: 'Connected At', label: 'Created At',
type: 'dateTime', type: 'dateTime',
readOnly: true readOnly: true
}, },
@ -67,6 +94,12 @@ export const Host = {
columnWidth: 200, columnWidth: 200,
columnFixed: 'left' columnFixed: 'left'
}, },
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true
},
{ {
name: 'state', name: 'state',
label: 'State', label: 'State',
@ -76,10 +109,10 @@ export const Host = {
readOnly: true readOnly: true
}, },
{ {
name: 'active', name: 'connectedAt',
label: 'Active', label: 'Connected At',
type: 'bool', type: 'dateTime',
required: true readOnly: true
}, },
{ {
name: 'online', name: 'online',
@ -87,6 +120,13 @@ export const Host = {
type: 'bool', type: 'bool',
readOnly: true readOnly: true
}, },
{
name: 'active',
label: 'Active',
type: 'bool',
required: true
},
{ {
name: 'deviceInfo.os', name: 'deviceInfo.os',
label: 'Operating System', label: 'Operating System',
@ -158,6 +198,14 @@ export const Host = {
label: 'Tags', label: 'Tags',
type: 'tags', type: 'tags',
required: false required: false
},
{
name: 'files',
label: 'Files',
type: 'objectList',
objectType: 'file',
required: false,
readOnly: true
} }
] ]
} }

View File

@ -56,6 +56,12 @@ export const Job = {
objectType: 'job', objectType: 'job',
showCopy: true showCopy: true
}, },
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
{ {
name: 'state', name: 'state',
label: 'State', label: 'State',
@ -65,7 +71,39 @@ export const Job = {
showProgress: true, showProgress: true,
showId: false, showId: false,
showQuantity: false, showQuantity: false,
columnWidth: 150, columnWidth: 250,
readOnly: true
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true
},
{
name: 'quantity',
label: 'Quantity',
type: 'number',
columnWidth: 125,
required: true
},
{
name: 'startedAt',
label: 'Started At',
type: 'dateTime',
readOnly: true
},
{
name: 'printers',
label: 'Printers',
type: 'objectList',
objectType: 'printer',
required: true
},
{
name: 'finishedAt',
label: 'Finished At',
type: 'dateTime',
readOnly: true readOnly: true
}, },
{ {
@ -82,33 +120,6 @@ export const Job = {
type: 'id', type: 'id',
objectType: 'gcodeFile', objectType: 'gcodeFile',
showHyperlink: true showHyperlink: true
},
{
name: 'quantity',
label: 'Quantity',
type: 'number',
columnWidth: 125,
required: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
{
name: 'startedAt',
label: 'Started At',
type: 'dateTime',
readOnly: true
},
{
name: 'printers',
label: 'Printers',
type: 'objectList',
objectType: 'printer',
required: true,
span: 2
} }
] ]
} }

View File

@ -2,6 +2,8 @@ import NoteTypeIcon from '../../components/Icons/NoteTypeIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon' import ReloadIcon from '../../components/Icons/ReloadIcon'
import EditIcon from '../../components/Icons/EditIcon' import EditIcon from '../../components/Icons/EditIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
export const NoteType = { export const NoteType = {
name: 'noteType', name: 'noteType',
@ -30,7 +32,31 @@ export const NoteType = {
row: true, row: true,
icon: EditIcon, icon: EditIcon,
url: (_id) => url: (_id) =>
`/dashboard/management/notetypes/info?noteTypeId=${_id}&action=edit` `/dashboard/management/notetypes/info?noteTypeId=${_id}&action=edit`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
}
},
{
name: 'finishEdit',
label: 'Save Edits',
icon: CheckIcon,
url: (_id) =>
`/dashboard/management/notetypes/info?noteTypeId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'cancelEdit',
label: 'Cancel Edits',
icon: XMarkIcon,
url: (_id) =>
`/dashboard/management/notetypes/info?noteTypeId=${_id}&action=cancelEdit`,
visible: (objectData) => {
console.log(objectData?._isEditing)
return objectData?._isEditing && objectData?._isEditing == true
}
} }
], ],
columns: ['name', '_id', 'color', 'active', 'createdAt', 'updatedAt'], columns: ['name', '_id', 'color', 'active', 'createdAt', 'updatedAt'],

View File

@ -2,6 +2,8 @@ import EditIcon from '../../components/Icons/EditIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import PartIcon from '../../components/Icons/PartIcon' import PartIcon from '../../components/Icons/PartIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon' import ReloadIcon from '../../components/Icons/ReloadIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
export const Part = { export const Part = {
name: 'part', name: 'part',
@ -29,7 +31,32 @@ export const Part = {
label: 'Edit', label: 'Edit',
row: true, row: true,
icon: EditIcon, icon: EditIcon,
url: (_id) => `/dashboard/management/parts/info?partId=${_id}&action=edit` url: (_id) =>
`/dashboard/management/parts/info?partId=${_id}&action=edit`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
}
},
{
name: 'finishEdit',
label: 'Save Edits',
icon: CheckIcon,
url: (_id) =>
`/dashboard/management/parts/info?partId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'cancelEdit',
label: 'Cancel Edits',
icon: XMarkIcon,
url: (_id) =>
`/dashboard/management/parts/info?partId=${_id}&action=cancelEdit`,
visible: (objectData) => {
console.log(objectData?._isEditing)
return objectData?._isEditing && objectData?._isEditing == true
}
} }
], ],
columns: [ columns: [
@ -71,22 +98,6 @@ export const Part = {
type: 'dateTime', type: 'dateTime',
readOnly: true readOnly: true
}, },
{
name: 'product',
label: 'Product',
type: 'object',
required: true,
objectType: 'product'
},
{
name: 'product._id',
label: 'Product ID',
type: 'id',
readOnly: true,
showHyperlink: true,
objectType: 'product'
},
{ {
name: 'vendor', name: 'vendor',
label: 'Vendor', label: 'Vendor',
@ -111,11 +122,14 @@ export const Part = {
objectType: 'vendor' objectType: 'vendor'
}, },
{ {
name: 'globalPricing', name: 'cost',
label: 'Global Price', label: 'Cost',
columnWidth: 150, columnWidth: 150,
required: true, required: true,
type: 'bool' type: 'number',
prefix: '£',
min: 0,
step: 0.01
}, },
{ {
name: 'priceMode', name: 'priceMode',
@ -142,18 +156,21 @@ export const Part = {
step: 0.01 step: 0.01
}, },
{ {
name: 'amount', name: 'price',
label: 'Amount', label: 'Price',
required: true, required: true,
disabled: (objectData) => {
return (
objectData.globalPricing == true || objectData.priceMode == 'margin'
)
},
type: 'number', type: 'number',
prefix: '£', prefix: '£',
min: 0, min: 0,
step: 0.1 step: 0.1,
roundNumber: 2,
value: (objectData) => {
if (objectData?.priceMode == 'margin') {
return objectData?.cost * (1 + objectData?.margin / 100) || undefined
} else {
return objectData?.price || undefined
}
}
}, },
{ {
name: 'file', name: 'file',

View File

@ -2,7 +2,7 @@ import PartStockIcon from '../../components/Icons/PartStockIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const PartStock = { export const PartStock = {
name: 'partstock', name: 'partStock',
label: 'Part Stock', label: 'Part Stock',
prefix: 'PTS', prefix: 'PTS',
icon: PartStockIcon, icon: PartStockIcon,
@ -13,8 +13,124 @@ export const PartStock = {
default: true, default: true,
row: true, row: true,
icon: InfoCircleIcon, icon: InfoCircleIcon,
url: (_id) => `/dashboard/management/partstocks/info?partStockId=${_id}` url: (_id) => `/dashboard/inventory/partstocks/info?partStockId=${_id}`
} }
], ],
url: (id) => `/dashboard/management/partstocks/info?partStockId=${id}` url: (id) => `/dashboard/inventory/partstocks/info?partStockId=${id}`,
filters: ['_id', 'part', 'startingQuantity', 'currentQuantity'],
sorters: ['part', 'startingQuantity', 'currentQuantity'],
columns: [
'_id',
'state',
'startingQuantity',
'currentQuantity',
'part',
'part._id',
'createdAt',
'updatedAt'
],
properties: [
{
name: '_id',
label: 'ID',
type: 'id',
objectType: 'partStock',
showCopy: true,
readOnly: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
{
name: 'state',
label: 'State',
type: 'state',
readOnly: true,
columnWidth: 120
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true
},
{
name: 'sourceType',
label: 'Source Type',
type: 'objectType',
readOnly: false,
columnWidth: 200,
required: true,
masterFilter: ['subJob']
},
{
name: 'consumedAt',
label: 'Consumed At',
type: 'dateTime',
readOnly: true
},
{
name: 'part',
label: 'Part',
type: 'object',
objectType: 'part',
required: true
},
{
name: 'part._id',
label: 'Part ID',
type: 'id',
objectType: 'part',
readOnly: true,
showHyperlink: true
},
{
name: 'source',
label: 'Source',
type: 'object',
readOnly: false,
required: true,
columnWidth: 200,
objectType: (objectData) => {
return objectData?.sourceType
}
},
{
name: 'source._id',
label: 'Source ID',
type: 'id',
readOnly: true,
columnWidth: 200,
objectType: (objectData) => {
return objectData?.sourceType
}
},
{
name: 'currentQuantity',
label: 'Current Quantity',
type: 'number',
readOnly: true,
columnWidth: 200,
required: true,
value: (objectData) => {
if (objectData?.state?.type === 'new') {
return objectData?.startingQuantity
} else {
return objectData.currentQuantity
}
}
},
{
name: 'startingQuantity',
label: 'Starting Quantity',
type: 'number',
columnWidth: 200,
required: true
}
]
} }

View File

@ -2,10 +2,14 @@ import PrinterIcon from '../../components/Icons/PrinterIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon' import ReloadIcon from '../../components/Icons/ReloadIcon'
import EditIcon from '../../components/Icons/EditIcon' import EditIcon from '../../components/Icons/EditIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
import PlayCircleIcon from '../../components/Icons/PlayCircleIcon' import PlayCircleIcon from '../../components/Icons/PlayCircleIcon'
import PauseCircleIcon from '../../components/Icons/PauseCircleIcon' import PauseCircleIcon from '../../components/Icons/PauseCircleIcon'
import StopCircleIcon from '../../components/Icons/StopCircleIcon' import StopCircleIcon from '../../components/Icons/StopCircleIcon'
import FilamentStockIcon from '../../components/Icons/FilamentStockIcon' import FilamentStockIcon from '../../components/Icons/FilamentStockIcon'
import ControlIcon from '../../components/Icons/ControlIcon'
export const Printer = { export const Printer = {
name: 'printer', name: 'printer',
label: 'Printer', label: 'Printer',
@ -32,7 +36,7 @@ export const Printer = {
name: 'control', name: 'control',
label: 'Control', label: 'Control',
row: true, row: true,
icon: PlayCircleIcon, icon: ControlIcon,
url: (_id) => `/dashboard/production/printers/control?printerId=${_id}` url: (_id) => `/dashboard/production/printers/control?printerId=${_id}`
}, },
{ {
@ -41,7 +45,31 @@ export const Printer = {
row: true, row: true,
icon: EditIcon, icon: EditIcon,
url: (_id) => url: (_id) =>
`/dashboard/production/printers/info?printerId=${_id}&action=edit` `/dashboard/production/printers/info?printerId=${_id}&action=edit`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
}
},
{
name: 'finishEdit',
label: 'Save Edits',
icon: CheckIcon,
url: (_id) =>
`/dashboard/production/printers/info?printerId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'cancelEdit',
label: 'Cancel Edits',
icon: XMarkIcon,
url: (_id) =>
`/dashboard/production/printers/info?printerId=${_id}&action=cancelEdit`,
visible: (objectData) => {
console.log(objectData?._isEditing)
return objectData?._isEditing && objectData?._isEditing == true
}
}, },
{ type: 'divider' }, { type: 'divider' },
{ {
@ -98,12 +126,13 @@ export const Printer = {
label: 'Start Queue', label: 'Start Queue',
icon: PlayCircleIcon, icon: PlayCircleIcon,
disabled: (objectData) => { disabled: (objectData) => {
console.log(objectData?.subJobs?.length) console.log(objectData?.queue?.length)
return ( return (
objectData?.state?.type == 'error' || objectData?.state?.type == 'error' ||
objectData?.state?.type == 'printing' || objectData?.state?.type == 'printing' ||
objectData?.subJobs?.length == 0 || objectData?.state?.type == 'paused' ||
objectData?.subJobs?.length == undefined objectData?.queue?.length == 0 ||
objectData?.queue?.length == undefined
) )
}, },
url: (_id) => url: (_id) =>
@ -125,7 +154,7 @@ export const Printer = {
label: 'Resume Job', label: 'Resume Job',
icon: PlayCircleIcon, icon: PlayCircleIcon,
disabled: (objectData) => { disabled: (objectData) => {
return objectData?.state?.type != 'printing' return objectData?.state?.type != 'paused'
}, },
url: (_id) => url: (_id) =>
`/dashboard/production/printers/control?printerId=${_id}&action=resumeJob` `/dashboard/production/printers/control?printerId=${_id}&action=resumeJob`
@ -137,7 +166,7 @@ export const Printer = {
disabled: (objectData) => { disabled: (objectData) => {
return ( return (
objectData?.state?.type != 'printing' && objectData?.state?.type != 'printing' &&
objectData?.state?.type != 'error' objectData?.state?.type != 'paused'
) )
}, },
url: (_id) => url: (_id) =>
@ -149,20 +178,37 @@ export const Printer = {
name: 'filamentStock', name: 'filamentStock',
label: 'Filament Stock', label: 'Filament Stock',
icon: FilamentStockIcon, icon: FilamentStockIcon,
disabled: (objectData) => {
return objectData?.online == false
},
children: [ children: [
{ {
name: 'loadFilamentStock', name: 'loadFilamentStock',
label: 'Load Filament Stock', label: 'Load Filament Stock',
icon: FilamentStockIcon, icon: FilamentStockIcon,
url: (_id) => url: (_id) =>
`/dashboard/production/printers/control?printerId=${_id}&action=loadFilamentStock` `/dashboard/production/printers/control?printerId=${_id}&action=loadFilamentStock`,
disabled: (objectData) => {
return (
objectData?.state?.type == 'printing' ||
objectData?.state?.type == 'error' ||
objectData?.currentFilamentStock != null
)
}
}, },
{ {
name: 'unloadFilamentStock', name: 'unloadFilamentStock',
label: 'Unload Filament Stock', label: 'Unload Filament Stock',
icon: FilamentStockIcon, icon: FilamentStockIcon,
url: (_id) => url: (_id) =>
`/dashboard/production/printers/control?printerId=${_id}&action=unloadFilamentStock` `/dashboard/production/printers/control?printerId=${_id}&action=unloadFilamentStock`,
disabled: (objectData) => {
return (
objectData?.state?.type == 'printing' ||
objectData?.state?.type == 'error' ||
objectData?.currentFilamentStock == null
)
}
} }
] ]
} }
@ -214,7 +260,8 @@ export const Printer = {
type: 'state', type: 'state',
objectType: 'printer', objectType: 'printer',
showName: false, showName: false,
readOnly: true readOnly: true,
columnWidth: 250
}, },
{ {
name: 'connectedAt', name: 'connectedAt',
@ -354,7 +401,7 @@ export const Printer = {
required: false required: false
}, },
{ {
name: 'subJobs', name: 'queue',
label: 'Queue', label: 'Queue',
type: 'objectList', type: 'objectList',
objectType: 'subJob', objectType: 'subJob',

View File

@ -2,6 +2,8 @@ import ProductIcon from '../../components/Icons/ProductIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon' import ReloadIcon from '../../components/Icons/ReloadIcon'
import EditIcon from '../../components/Icons/EditIcon' import EditIcon from '../../components/Icons/EditIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
export const Product = { export const Product = {
name: 'product', name: 'product',
@ -30,7 +32,31 @@ export const Product = {
row: true, row: true,
icon: EditIcon, icon: EditIcon,
url: (_id) => url: (_id) =>
`/dashboard/management/products/info?productId=${_id}&action=edit` `/dashboard/management/products/info?productId=${_id}&action=edit`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
}
},
{
name: 'finishEdit',
label: 'Save Edits',
icon: CheckIcon,
url: (_id) =>
`/dashboard/management/products/info?productId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'cancelEdit',
label: 'Cancel Edits',
icon: XMarkIcon,
url: (_id) =>
`/dashboard/management/products/info?productId=${_id}&action=cancelEdit`,
visible: (objectData) => {
console.log(objectData?._isEditing)
return objectData?._isEditing && objectData?._isEditing == true
}
} }
], ],
columns: [ columns: [
@ -129,6 +155,51 @@ export const Product = {
prefix: '£', prefix: '£',
min: 0, min: 0,
step: 0.1 step: 0.1
},
{
name: 'parts',
label: 'Parts',
type: 'objectChildren',
objectType: 'part',
properties: [
{
name: 'part',
label: 'Part',
type: 'object',
objectType: 'part',
required: true
},
{
name: 'part._id',
label: 'Part ID',
type: 'id',
objectType: 'part',
showHyperlink: true,
value: (objectData) => {
return objectData?.part?._id
}
},
{
name: 'quantity',
label: 'Quantity',
type: 'number',
required: true
}
],
rollups: [
{
name: 'totalQuantity',
label: 'Total',
type: 'number',
property: 'quantity',
value: (objectData) => {
return objectData?.parts?.reduce(
(acc, part) => acc + part.quantity,
0
)
}
}
]
} }
] ]
} }

View File

@ -0,0 +1,180 @@
import PurchaseOrderIcon from '../../components/Icons/PurchaseOrderIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const PurchaseOrder = {
name: 'purchaseOrder',
label: 'Purchase Order',
prefix: 'POR',
icon: PurchaseOrderIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) =>
`/dashboard/inventory/purchaseorders/info?purchaseOrderId=${_id}`
}
],
group: ['vendor'],
filters: ['vendor', 'vendor._id'],
sorters: ['createdAt', 'state', 'updatedAt'],
columns: ['_id', 'createdAt', 'state', 'updatedAt', 'vendor', 'vendor._id'],
properties: [
{
name: '_id',
label: 'ID',
type: 'id',
columnFixed: 'left',
objectType: 'purchaseOrder',
columnWidth: 140,
showCopy: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
{ name: 'state', label: 'State', type: 'state', readOnly: true },
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true
},
{
name: 'vendor',
label: 'Vendor',
required: true,
type: 'object',
objectType: 'vendor'
},
{
name: 'vendor._id',
label: 'Vendor ID',
readOnly: true,
type: 'id',
showHyperlink: true,
objectType: 'vendor'
},
{
name: 'courierService',
label: 'Courier Service',
required: true,
type: 'object',
objectType: 'courierService'
},
{
name: 'courierService._id',
label: 'Courier Service ID',
readOnly: true,
type: 'id',
showHyperlink: true,
objectType: 'courierService'
},
{
name: 'items',
label: 'Order 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
},
{
name: 'item._id',
label: 'Item ID',
type: 'id',
objectType: (objectData) => {
return objectData?.itemType
},
showHyperlink: true,
value: (objectData) => {
return objectData?.item?._id
}
},
{
name: 'quantity',
label: 'Quantity',
type: 'number',
required: true
},
{
name: 'price',
label: 'Price',
type: 'number',
required: true,
prefix: '£',
min: 0,
step: 0.01,
value: (objectData) => {
if (objectData?.price == undefined) {
console.log('PurchaseOrder.js', objectData)
return (
(objectData?.item?.cost || 0) * (objectData?.quantity || 0) ||
undefined
)
} else {
return objectData?.part?.price || undefined
}
}
}
],
rollups: [
{
name: 'totalQuantity',
label: 'Total',
type: 'number',
property: 'quantity',
value: (objectData) => {
return objectData?.items?.reduce(
(acc, item) => acc + item.quantity,
0
)
}
},
{
name: 'totalPrice',
label: 'Total',
type: 'number',
prefix: '£',
property: 'price',
value: (objectData) => {
return objectData?.items?.reduce((acc, item) => acc + item.price, 0)
}
}
]
},
{
name: 'cost',
label: 'Cost',
type: 'netGross',
required: true,
prefix: '£',
min: 0,
step: 0.01,
value: (objectData) => {
const net = objectData?.items?.reduce(
(acc, item) => acc + item.price,
0
)
return { net: net, gross: net }
}
}
]
}

View File

@ -2,7 +2,7 @@ import StockAuditIcon from '../../components/Icons/StockAuditIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const StockAudit = { export const StockAudit = {
name: 'stockaudit', name: 'stockAudit',
label: 'Stock Audit', label: 'Stock Audit',
prefix: 'SAU', prefix: 'SAU',
icon: StockAuditIcon, icon: StockAuditIcon,
@ -16,5 +16,38 @@ export const StockAudit = {
url: (_id) => `/dashboard/inventory/stockaudits/info?stockAuditId=${_id}` url: (_id) => `/dashboard/inventory/stockaudits/info?stockAuditId=${_id}`
} }
], ],
url: (id) => `/dashboard/inventory/stockaudits/info?stockAuditId=${id}` url: (id) => `/dashboard/inventory/stockaudits/info?stockAuditId=${id}`,
columns: ['_id', 'state', 'createdAt', 'updatedAt'],
filters: ['_id'],
sorters: ['createdAt', 'updatedAt'],
group: ['state'],
properties: [
{
name: '_id',
label: 'ID',
type: 'id',
objectType: 'stockAudit',
showCopy: true,
readOnly: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
{
name: 'state',
label: 'State',
type: 'state',
readOnly: true,
columnWidth: 120
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true
}
]
} }

View File

@ -1,14 +1,37 @@
import SubJobIcon from '../../components/Icons/SubJobIcon' import SubJobIcon from '../../components/Icons/SubJobIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
export const SubJob = { export const SubJob = {
name: 'subJob', name: 'subJob',
label: 'Sub Job', label: 'Sub Job',
prefix: 'SJB', prefix: 'SJB',
icon: SubJobIcon, icon: SubJobIcon,
actions: [], actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/production/subjobs/info?subJobId=${_id}`
},
{
name: 'cancel',
label: 'Cancel Sub Job',
row: true,
icon: XMarkIcon,
url: (_id) =>
`/dashboard/production/subjobs/info?subJobId=${_id}&action=cancel`,
disabled: (objectData) => {
return objectData?.state?.type !== 'queued'
}
}
],
columns: ['_id', 'printer', 'printer._id', 'job._id', 'state', 'createdAt'], columns: ['_id', 'printer', 'printer._id', 'job._id', 'state', 'createdAt'],
filters: ['state', '_id', 'job._id', 'printer._id'], filters: ['state', '_id', 'job._id', 'printer._id'],
sorters: ['createdAt', 'state'], sorters: ['createdAt', 'state'],
group: ['job'],
properties: [ properties: [
{ {
name: '_id', name: '_id',
@ -19,6 +42,68 @@ export const SubJob = {
columnWidth: 140, columnWidth: 140,
showCopy: true showCopy: true
}, },
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true,
columnWidth: 175
},
{
name: 'state',
label: 'State',
type: 'state',
objectType: 'subJob',
showStatus: true,
showProgress: true,
showId: false,
showQuantity: false,
columnWidth: 250,
readOnly: true
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true,
columnWidth: 175
},
{
name: 'job',
label: 'Job',
type: 'object',
objectType: 'job'
},
{
name: 'job._id',
label: 'Job ID',
type: 'id',
columnWidth: 140,
showHyperlink: true,
objectType: 'job'
},
{
name: 'startedAt',
label: 'Started At',
type: 'dateTime',
readOnly: true
},
{
name: 'moonrakerJobId',
label: 'Moonraker Job ID',
type: 'miscId',
columnWidth: 140,
showCopy: true
},
{
name: 'finishedAt',
label: 'Finished At',
type: 'dateTime',
readOnly: true
},
{ {
name: 'printer', name: 'printer',
label: 'Printer', label: 'Printer',
@ -34,33 +119,6 @@ export const SubJob = {
columnFixed: 'left', columnFixed: 'left',
showHyperlink: true, showHyperlink: true,
objectType: 'printer' objectType: 'printer'
},
{
name: 'job._id',
label: 'Job ID',
type: 'id',
columnWidth: 140,
showHyperlink: true,
objectType: 'job'
},
{
name: 'state',
label: 'State',
type: 'state',
objectType: 'subJob',
showStatus: true,
showProgress: true,
showId: false,
showQuantity: false,
columnWidth: 125,
readOnly: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true,
columnWidth: 175
} }
] ]
} }

View File

@ -1,6 +1,8 @@
import VendorIcon from '../../components/Icons/VendorIcon' import VendorIcon from '../../components/Icons/VendorIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import EditIcon from '../../components/Icons/EditIcon' import EditIcon from '../../components/Icons/EditIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon' import ReloadIcon from '../../components/Icons/ReloadIcon'
import BinIcon from '../../components/Icons/BinIcon' import BinIcon from '../../components/Icons/BinIcon'
@ -31,7 +33,31 @@ export const Vendor = {
row: true, row: true,
icon: EditIcon, icon: EditIcon,
url: (_id) => url: (_id) =>
`/dashboard/management/vendors/info?vendorId=${_id}&action=edit` `/dashboard/management/vendors/info?vendorId=${_id}&action=edit`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
}
},
{
name: 'finishEdit',
label: 'Save Edits',
icon: CheckIcon,
url: (_id) =>
`/dashboard/management/vendors/info?vendorId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'cancelEdit',
label: 'Cancel Edits',
icon: XMarkIcon,
url: (_id) =>
`/dashboard/management/vendors/info?vendorId=${_id}&action=cancelEdit`,
visible: (objectData) => {
console.log(objectData?._isEditing)
return objectData?._isEditing && objectData?._isEditing == true
}
}, },
{ type: 'divider' }, { type: 'divider' },
{ {
@ -43,7 +69,6 @@ export const Vendor = {
`/dashboard/management/vendors/info?vendorId=${_id}&action=delete` `/dashboard/management/vendors/info?vendorId=${_id}&action=delete`
} }
], ],
url: (id) => `/dashboard/management/vendors/info?vendorId=${id}`,
columns: ['name', '_id', 'country', 'email', 'website', 'createdAt'], columns: ['name', '_id', 'country', 'email', 'website', 'createdAt'],
filters: ['name', '_id', 'country', 'email'], filters: ['name', '_id', 'country', 'email'],
sorters: ['name', 'country', 'email', 'createdAt', '_id'], sorters: ['name', 'country', 'email', 'createdAt', '_id'],

View File

@ -3,9 +3,12 @@ import { Route } from 'react-router-dom'
import FilamentStocks from '../components/Dashboard/Inventory/FilamentStocks.jsx' import FilamentStocks from '../components/Dashboard/Inventory/FilamentStocks.jsx'
import FilamentStockInfo from '../components/Dashboard/Inventory/FilamentStocks/FilamentStockInfo.jsx' import FilamentStockInfo from '../components/Dashboard/Inventory/FilamentStocks/FilamentStockInfo.jsx'
import PartStocks from '../components/Dashboard/Inventory/PartStocks.jsx' import PartStocks from '../components/Dashboard/Inventory/PartStocks.jsx'
import PartStockInfo from '../components/Dashboard/Inventory/PartStocks/PartStockInfo.jsx'
import StockEvents from '../components/Dashboard/Inventory/StockEvents.jsx' import StockEvents from '../components/Dashboard/Inventory/StockEvents.jsx'
import StockAudits from '../components/Dashboard/Inventory/StockAudits.jsx' import StockAudits from '../components/Dashboard/Inventory/StockAudits.jsx'
import StockAuditInfo from '../components/Dashboard/Inventory/StockAudits/StockAuditInfo.jsx' import StockAuditInfo from '../components/Dashboard/Inventory/StockAudits/StockAuditInfo.jsx'
import PurchaseOrders from '../components/Dashboard/Inventory/PurchaseOrders.jsx'
import PurchaseOrderInfo from '../components/Dashboard/Inventory/PurchaseOrders/PurchaseOrderInfo.jsx'
const InventoryRoutes = [ const InventoryRoutes = [
<Route <Route
@ -23,6 +26,11 @@ const InventoryRoutes = [
path='inventory/partstocks' path='inventory/partstocks'
element={<PartStocks />} element={<PartStocks />}
/>, />,
<Route
key='partstocks-info'
path='inventory/partstocks/info'
element={<PartStockInfo />}
/>,
<Route <Route
key='stockevents' key='stockevents'
path='inventory/stockevents' path='inventory/stockevents'
@ -37,6 +45,16 @@ const InventoryRoutes = [
key='stockaudits-info' key='stockaudits-info'
path='inventory/stockaudits/info' path='inventory/stockaudits/info'
element={<StockAuditInfo />} element={<StockAuditInfo />}
/>,
<Route
key='purchaseorders'
path='inventory/purchaseorders'
element={<PurchaseOrders />}
/>,
<Route
key='purchaseorders-info'
path='inventory/purchaseorders/info'
element={<PurchaseOrderInfo />}
/> />
] ]

View File

@ -9,6 +9,10 @@ import ProductInfo from '../components/Dashboard/Management/Products/ProductInfo
import Vendors from '../components/Dashboard/Management/Vendors' import Vendors from '../components/Dashboard/Management/Vendors'
import VendorInfo from '../components/Dashboard/Management/Vendors/VendorInfo' import VendorInfo from '../components/Dashboard/Management/Vendors/VendorInfo'
import Materials from '../components/Dashboard/Management/Materials' import Materials from '../components/Dashboard/Management/Materials'
import Couriers from '../components/Dashboard/Management/Couriers'
import CourierInfo from '../components/Dashboard/Management/Couriers/CourierInfo.jsx'
import CourierServices from '../components/Dashboard/Management/CourierServices'
import CourierServiceInfo from '../components/Dashboard/Management/CourierServices/CourierServiceInfo.jsx'
import Settings from '../components/Dashboard/Management/Settings' import Settings from '../components/Dashboard/Management/Settings'
import AuditLogs from '../components/Dashboard/Management/AuditLogs.jsx' import AuditLogs from '../components/Dashboard/Management/AuditLogs.jsx'
import NoteTypes from '../components/Dashboard/Management/NoteTypes.jsx' import NoteTypes from '../components/Dashboard/Management/NoteTypes.jsx'
@ -73,6 +77,22 @@ const ManagementRoutes = [
element={<FileInfo />} element={<FileInfo />}
/>, />,
<Route key='materials' path='management/materials' element={<Materials />} />, <Route key='materials' path='management/materials' element={<Materials />} />,
<Route key='couriers' path='management/couriers' element={<Couriers />} />,
<Route
key='couriers-info'
path='management/couriers/info'
element={<CourierInfo />}
/>,
<Route
key='courierServices'
path='management/courierServices'
element={<CourierServices />}
/>,
<Route
key='courierServices-info'
path='management/courierServices/info'
element={<CourierServiceInfo />}
/>,
<Route key='notetypes' path='management/notetypes' element={<NoteTypes />} />, <Route key='notetypes' path='management/notetypes' element={<NoteTypes />} />,
<Route <Route
key='notetypes-info' key='notetypes-info'

View File

@ -7,6 +7,7 @@ import PrinterInfo from '../components/Dashboard/Production/Printers/PrinterInfo
import Jobs from '../components/Dashboard/Production/Jobs.jsx' import Jobs from '../components/Dashboard/Production/Jobs.jsx'
import JobInfo from '../components/Dashboard/Production/Jobs/JobInfo.jsx' import JobInfo from '../components/Dashboard/Production/Jobs/JobInfo.jsx'
import SubJobs from '../components/Dashboard/Production/SubJobs.jsx' import SubJobs from '../components/Dashboard/Production/SubJobs.jsx'
import SubJobInfo from '../components/Dashboard/Production/SubJobs/SubJobInfo.jsx'
import GCodeFiles from '../components/Dashboard/Production/GCodeFiles' import GCodeFiles from '../components/Dashboard/Production/GCodeFiles'
import GCodeFileInfo from '../components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx' import GCodeFileInfo from '../components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx'
@ -29,6 +30,11 @@ const ProductionRoutes = [
/>, />,
<Route key='jobs' path='production/jobs' element={<Jobs />} />, <Route key='jobs' path='production/jobs' element={<Jobs />} />,
<Route key='subjobs' path='production/subjobs' element={<SubJobs />} />, <Route key='subjobs' path='production/subjobs' element={<SubJobs />} />,
<Route
key='subjobs-info'
path='production/subjobs/info'
element={<SubJobInfo />}
/>,
<Route key='jobs-info' path='production/jobs/info' element={<JobInfo />} />, <Route key='jobs-info' path='production/jobs/info' element={<JobInfo />} />,
<Route <Route
key='gcodefiles' key='gcodefiles'

1333
yarn.lock

File diff suppressed because it is too large Load Diff