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 {
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-router-dom": "^7.8.2",
"remark-gfm": "^4.0.1",
"simplebar-react": "^3.3.2",
"socket.io-client": "*",
"standard": "^17.1.2",
"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 { AuthProvider } from './components/Dashboard/context/AuthContext.jsx'
import { SpotlightProvider } from './components/Dashboard/context/SpotlightContext.jsx'
import { ActionsModalProvider } from './components/Dashboard/context/ActionsModalContext.jsx'
import {
ThemeProvider,
@ -21,6 +22,7 @@ import {
import AppError from './components/App/AppError'
import { ApiServerProvider } from './components/Dashboard/context/ApiServerContext.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 {
@ -53,42 +55,49 @@ const AppContent = () => {
<PrintServerProvider>
<ApiServerProvider>
<SpotlightProvider>
<Routes>
<Route
path='/'
element={
<PrivateRoute
component={() => (
<Navigate
to='/dashboard/production/overview'
replace
<ActionsModalProvider>
<MessageProvider>
<Routes>
<Route
path='/'
element={
<PrivateRoute
component={() => (
<Navigate
to='/dashboard/production/overview'
replace
/>
)}
/>
)}
}
/>
}
/>
<Route path='/auth/callback' element={<AuthCallback />} />
<Route
path='/dashboard'
element={
<PrivateRoute component={() => <Dashboard />} />
}
>
{ProductionRoutes}
{InventoryRoutes}
{ManagementRoutes}
{DeveloperRoutes}
</Route>
<Route
path='*'
element={
<AppError
message='Error 404. Page not found.'
showRefresh={false}
<Route
path='/auth/callback'
element={<AuthCallback />}
/>
}
/>
</Routes>
<Route
path='/dashboard'
element={
<PrivateRoute component={() => <Dashboard />} />
}
>
{ProductionRoutes}
{InventoryRoutes}
{ManagementRoutes}
{DeveloperRoutes}
</Route>
<Route
path='*'
element={
<AppError
message='Error 404. Page not found.'
showRefresh={false}
/>
}
/>
</Routes>
</MessageProvider>
</ActionsModalProvider>
</SpotlightProvider>
</ApiServerProvider>
</PrintServerProvider>

View File

@ -34,9 +34,15 @@ const routeKeyMap = {
const DeveloperSidebar = (props) => {
const location = useLocation()
const selectedKey = (() => {
const match = Object.keys(routeKeyMap).find((path) =>
location.pathname.startsWith(path)
)
const match = Object.keys(routeKeyMap).find((path) => {
const pathSplit = path.split('/')
const locationPathSplit = location.pathname.split('/')
if (pathSplit.length > locationPathSplit.length) return false
for (let i = 0; i < pathSplit.length; i++) {
if (pathSplit[i] !== locationPathSplit[i]) return false
}
return true
})
return match ? routeKeyMap[match] : 'sessionstorage'
})()

View File

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

View File

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

View File

@ -1,18 +1,9 @@
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 NewObjectForm from '../../common/NewObjectForm'
import NewObjectButtons from '../../common/NewObjectButtons'
const { Title } = Typography
import WizardView from '../../common/WizardView'
const NewFilamentStock = ({ onOk, reset }) => {
const [currentStep, setCurrentStep] = useState(0)
const isMobile = useMediaQuery({ maxWidth: 768 })
return (
<NewObjectForm
type={'filamentStock'}
@ -30,7 +21,6 @@ const NewFilamentStock = ({ onOk, reset }) => {
column={1}
bordered={false}
isEditing={true}
initial={true}
required={true}
objectData={objectData}
/>
@ -56,43 +46,16 @@ const NewFilamentStock = ({ onOk, reset }) => {
}
]
return (
<Flex gap='middle'>
{!isMobile && (
<div style={{ minWidth: '160px' }}>
<Steps
current={currentStep}
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={() => {
handleSubmit()
onOk()
}}
formValid={formValid}
submitLoading={submitLoading}
/>
</Flex>
</Flex>
<WizardView
steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Filament Stock'
onSubmit={() => {
handleSubmit()
onOk()
}}
/>
)
}}
</NewObjectForm>

View File

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

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 config from '../../../../config'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
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 (
<Form
form={form}
layout='vertical'
onFinish={onFinish}
style={{ maxWidth: '100%' }}
<NewObjectForm
type={'partStock'}
reset={reset}
defaultValues={{ state: { type: 'new' } }}
>
<Form.Item
name='part'
label='Part'
rules={[{ required: true, message: 'Please select a part' }]}
>
<Select
placeholder='Select a part'
options={parts.map((part) => ({
value: part._id,
label: part.name
}))}
/>
</Form.Item>
<Form.Item
name='startingLots'
label='Starting Lots'
rules={[
{ required: true, message: 'Please enter the starting lots' },
{ type: 'number', min: 1, message: 'Lots must be at least 1' }
]}
>
<InputNumber
style={{ width: '100%' }}
placeholder='Enter starting lots'
min={1}
/>
</Form.Item>
<Form.Item name='notes' label='Notes'>
<Input.TextArea
placeholder='Enter any additional notes'
autoSize={{ minRows: 3, maxRows: 6 }}
/>
</Form.Item>
<Form.Item>
<Space>
<Button type='primary' htmlType='submit' loading={loading}>
Create Part Stock
</Button>
<Button onClick={() => form.resetFields()}>Reset</Button>
</Space>
</Form.Item>
</Form>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='partStock'
column={1}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='partStock'
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 Part Stock'
onSubmit={() => {
handleSubmit()
onOk()
}}
/>
)
}}
</NewObjectForm>
)
}
@ -122,8 +67,4 @@ NewPartStock.propTypes = {
reset: PropTypes.bool
}
NewPartStock.defaultProps = {
reset: false
}
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'
import { useNavigate } from 'react-router-dom'
import { Button, Flex, Space, message, Dropdown, Typography } from 'antd'
// src/stockAudits.js
import { AuthContext } from '../context/AuthContext'
import { PrintServerContext } from '../context/PrintServerContext'
import { useState, useRef } from 'react'
import IdDisplay from '../common/IdDisplay'
import StockAuditIcon from '../../Icons/StockAuditIcon'
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import { Button, Flex, Space, Modal, Dropdown, message } from 'antd'
import NewStockAudit from './StockAudits/NewStockAudit'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import TimeDisplay from '../common/TimeDisplay'
import useColumnVisibility from '../hooks/useColumnVisibility'
import ObjectTable from '../common/ObjectTable'
import config from '../../../config'
const { Text } = Typography
import ListIcon from '../../Icons/ListIcon'
import GridIcon from '../../Icons/GridIcon'
import useViewMode from '../hooks/useViewMode'
import ColumnViewButton from '../common/ColumnViewButton'
const StockAudits = () => {
const [messageApi, contextHolder] = message.useMessage()
const navigate = useNavigate()
const { printServer } = useContext(PrintServerContext)
const [initialized, setInitialized] = useState(false)
const tableRef = useRef()
const { authenticated } = useContext(AuthContext)
const [newStockAuditOpen, setNewStockAuditOpen] = useState(false)
useEffect(() => {
if (printServer && !initialized) {
setInitialized(true)
printServer.on('notify_stockaudit_update', (updateData) => {
if (tableRef.current) {
tableRef.current.updateData(updateData._id, updateData)
}
})
}
const [viewMode, setViewMode] = useViewMode('stockAudits')
return () => {
if (printServer && initialized) {
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 [columnVisibility, setColumnVisibility] =
useColumnVisibility('stockAudits')
const actionItems = {
items: [
{
label: 'New Stock Audit',
label: 'New Stock audit',
key: 'newStockAudit',
icon: <PlusIcon />
},
@ -152,8 +43,7 @@ const StockAudits = () => {
if (key === 'reloadList') {
tableRef.current?.reload()
} else if (key === 'newStockAudit') {
// TODO: Implement new stock audit creation
messageApi.info('New stock audit creation not implemented yet')
setNewStockAuditOpen(true)
}
}
}
@ -162,18 +52,54 @@ const StockAudits = () => {
<>
<Flex vertical={'true'} gap='large'>
{contextHolder}
<Space>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
</Space>
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<ColumnViewButton
type='stockAudit'
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}
columns={columns}
url={`${config.backendUrl}/stockaudits`}
authenticated={authenticated}
visibleColumns={columnVisibility}
type='stockAudit'
cards={viewMode === 'cards'}
/>
</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 { useLocation, useNavigate } from 'react-router-dom'
import axios from 'axios'
import {
Card,
Descriptions,
Button,
Space,
message,
Typography,
Table,
Tag
} from 'antd'
import {
ArrowLeftOutlined,
LoadingOutlined,
ClockCircleOutlined
} from '@ant-design/icons'
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 { AuthContext } from '../../context/AuthContext'
import IdDisplay from '../../common/IdDisplay'
import TimeDisplay from '../../common/TimeDisplay'
import config from '../../../../config'
import XMarkCircleIcon from '../../../Icons/XMarkCircleIcon'
import CheckCircleIcon from '../../../Icons/CheckCircleIcon'
const { Text, Title } = Typography
const log = loglevel.getLogger('StockAuditInfo')
log.setLevel(config.logLevel)
const StockAuditInfo = () => {
const [messageApi, contextHolder] = message.useMessage()
const location = useLocation()
const navigate = useNavigate()
const { authenticated } = useContext(AuthContext)
const [stockAudit, setStockAudit] = useState(null)
const [loading, setLoading] = useState(true)
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const stockAuditId = new URLSearchParams(location.search).get('stockAuditId')
useEffect(() => {
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: {
Accept: 'application/json'
},
withCredentials: true
}
)
setStockAudit(response.data)
setLoading(false)
} catch (err) {
console.error(err)
messageApi.error('Failed to fetch stock audit details')
navigate('/dashboard/inventory/stockaudits')
}
const [collapseState, updateCollapseState] = useCollapseState(
'StockAuditInfo',
{
info: true,
stocks: true,
notes: true,
auditLogs: true
}
)
if (authenticated) {
fetchStockAudit()
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
}
}, [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} />
)
},
{
title: 'Item Type',
dataIndex: 'itemType',
key: 'itemType',
width: 120
},
{
title: 'Expected Weight',
dataIndex: 'expectedWeight',
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 (
<>
{contextHolder}
<Space direction='vertical' size='large' style={{ width: '100%' }}>
<Space>
<Button
icon={<ArrowLeftOutlined />}
onClick={() => navigate('/dashboard/inventory/stockaudits')}
>
Back to Stock Audits
</Button>
</Space>
<Card>
<Title level={4}>Stock Audit Details</Title>
<Descriptions bordered>
<Descriptions.Item label='ID'>
<IdDisplay
id={stockAudit._id}
type={'stockaudit'}
longId={true}
<Flex
gap='large'
vertical='true'
style={{
maxHeight: '100%',
minHeight: 0
}}
>
<Flex justify={'space-between'}>
<Space size='middle'>
<Space size='small'>
<ObjectActions
type='stockAudit'
id={stockAuditId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
</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>
<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>
<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>
<Card title='Audit Items'>
<Table
dataSource={stockAudit.items || []}
columns={auditItemsColumns}
rowKey='_id'
pagination={false}
scroll={{ y: 'calc(100vh - 500px)' }}
/>
</Card>
</Space>
<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>
<NotesPanel _id={stockAuditId} type='stockAudit' />
</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': stockAuditId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</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 InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
const log = loglevel.getLogger('DocumentJobInfo')
log.setLevel(config.logLevel)
@ -120,7 +121,7 @@ const DocumentJobInfo = () => {
/>
</Space>
</Flex>
<div style={{ height: '100%', overflow: 'auto' }}>
<ScrollBox>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
@ -186,7 +187,7 @@ const DocumentJobInfo = () => {
)}
</InfoCollapse>
</Flex>
</div>
</ScrollBox>
</Flex>
</>
)

View File

@ -20,6 +20,7 @@ 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('DocumentPrinterInfo')
log.setLevel(config.logLevel)
@ -129,7 +130,7 @@ const DocumentPrinterInfo = () => {
</Space>
</Flex>
<div style={{ height: '100%', overflowY: 'scroll' }}>
<ScrollBox>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
@ -199,7 +200,7 @@ const DocumentPrinterInfo = () => {
)}
</InfoCollapse>
</Flex>
</div>
</ScrollBox>
</Flex>
</>
)

View File

@ -20,6 +20,7 @@ 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('DocumentSizeInfo')
log.setLevel(config.logLevel)
@ -120,7 +121,7 @@ const DocumentSizeInfo = () => {
/>
</Space>
</Flex>
<div style={{ height: '100%', overflow: 'auto' }}>
<ScrollBox>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
@ -186,7 +187,7 @@ const DocumentSizeInfo = () => {
)}
</InfoCollapse>
</Flex>
</div>
</ScrollBox>
</Flex>
</>
)

View File

@ -1,19 +1,9 @@
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 NewObjectForm from '../../common/NewObjectForm'
import NewObjectButtons from '../../common/NewObjectButtons'
const { Title } = Typography
import WizardView from '../../common/WizardView'
const NewDocumentSize = ({ onOk }) => {
const [currentStep, setCurrentStep] = useState(0)
const isMobile = useMediaQuery({ maxWidth: 768 })
return (
<NewObjectForm type={'documentSize'}>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
@ -52,43 +42,16 @@ const NewDocumentSize = ({ onOk }) => {
}
]
return (
<Flex gap='middle'>
{!isMobile && (
<div style={{ minWidth: '160px' }}>
<Steps
current={currentStep}
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={() => {
handleSubmit()
onOk()
}}
formValid={formValid}
submitLoading={submitLoading}
/>
</Flex>
</Flex>
<WizardView
steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Document Size'
onSubmit={() => {
handleSubmit()
onOk()
}}
/>
)
}}
</NewObjectForm>

View File

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

View File

@ -20,6 +20,7 @@ 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('DocumentTemplateInfo')
log.setLevel(config.logLevel)
@ -126,7 +127,7 @@ const DocumentTemplateInfo = () => {
</Space>
</Flex>
<div style={{ height: '100%', overflowY: 'scroll' }}>
<ScrollBox>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
@ -201,7 +202,7 @@ const DocumentTemplateInfo = () => {
)}
</InfoCollapse>
</Flex>
</div>
</ScrollBox>
</Flex>
</>
)

View File

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

View File

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

View File

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

View File

@ -17,6 +17,8 @@ import DocumentIcon from '../../Icons/DocumentIcon'
import DocumentSizeIcon from '../../Icons/DocumentSizeIcon'
import DocumentJobIcon from '../../Icons/DocumentJobIcon'
import FileIcon from '../../Icons/FileIcon'
import CourierIcon from '../../Icons/CourierIcon'
import CourierServiceIcon from '../../Icons/CourierServiceIcon'
const items = [
{
@ -50,6 +52,19 @@ const items = [
path: '/dashboard/management/materials'
},
{ 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',
icon: <NoteTypeIcon />,
@ -139,6 +154,8 @@ const routeKeyMap = {
'/dashboard/management/users': 'users',
'/dashboard/management/products': 'products',
'/dashboard/management/vendors': 'vendors',
'/dashboard/management/couriers': 'couriers',
'/dashboard/management/courierservices': 'courierServices',
'/dashboard/management/materials': 'materials',
'/dashboard/management/notetypes': 'noteTypes',
'/dashboard/management/settings': 'settings',
@ -154,9 +171,15 @@ const routeKeyMap = {
const ManagementSidebar = (props) => {
const location = useLocation()
const selectedKey = (() => {
const match = Object.keys(routeKeyMap).find((path) =>
location.pathname.startsWith(path)
)
const match = Object.keys(routeKeyMap).find((path) => {
const pathSplit = path.split('/')
const locationPathSplit = location.pathname.split('/')
if (pathSplit.length > locationPathSplit.length) return false
for (let i = 0; i < pathSplit.length; i++) {
if (pathSplit[i] !== locationPathSplit[i]) return false
}
return true
})
return match ? routeKeyMap[match] : 'filaments'
})()

View File

@ -16,6 +16,7 @@ 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 NoteTypeInfo = () => {
const location = useLocation()
@ -109,7 +110,7 @@ const NoteTypeInfo = () => {
/>
</Space>
</Flex>
<div style={{ height: '100%', overflow: 'auto' }}>
<ScrollBox>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
@ -164,7 +165,7 @@ const NoteTypeInfo = () => {
)}
</InfoCollapse>
</Flex>
</div>
</ScrollBox>
</Flex>
</>
)

View File

@ -19,6 +19,7 @@ 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('NoteInfo')
log.setLevel(config.logLevel)
@ -118,7 +119,7 @@ const NoteInfo = () => {
/>
</Space>
</Flex>
<div style={{ height: '100%', overflow: 'auto' }}>
<ScrollBox>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
@ -183,7 +184,7 @@ const NoteInfo = () => {
)}
</InfoCollapse>
</Flex>
</div>
</ScrollBox>
</Flex>
</>
)

View File

@ -17,6 +17,7 @@ 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 PartInfo = () => {
const location = useLocation()
@ -109,7 +110,7 @@ const PartInfo = () => {
/>
</Space>
</Flex>
<div style={{ height: '100%', overflow: 'auto' }}>
<ScrollBox>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
@ -174,7 +175,7 @@ const PartInfo = () => {
)}
</InfoCollapse>
</Flex>
</div>
</ScrollBox>
</Flex>
</>
)

View File

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

View File

@ -18,6 +18,7 @@ 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 UserInfo = () => {
const location = useLocation()
@ -110,7 +111,7 @@ const UserInfo = () => {
/>
</Space>
</Flex>
<div style={{ height: '100%', overflow: 'auto' }}>
<ScrollBox>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
@ -176,7 +177,7 @@ const UserInfo = () => {
)}
</InfoCollapse>
</Flex>
</div>
</ScrollBox>
</Flex>
</>
)

View File

@ -19,6 +19,7 @@ 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('VendorInfo')
log.setLevel(config.logLevel)
@ -118,7 +119,7 @@ const VendorInfo = () => {
/>
</Space>
</Flex>
<div style={{ height: '100%', overflow: 'auto' }}>
<ScrollBox>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
@ -183,7 +184,7 @@ const VendorInfo = () => {
)}
</InfoCollapse>
</Flex>
</div>
</ScrollBox>
</Flex>
</>
)

View File

@ -8,6 +8,7 @@ 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 ObjectProperty from '../../common/ObjectProperty.jsx'
import ViewButton from '../../common/ViewButton.jsx'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
@ -23,6 +24,9 @@ import EyeIcon from '../../../Icons/EyeIcon.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import MissingPlaceholder from '../../common/MissingPlaceholder.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')
log.setLevel(config.logLevel)
@ -36,7 +40,8 @@ const GCodeFileInfo = () => {
'GCodeFileInfo',
{
info: true,
stocks: true,
parts: true,
preview: true,
notes: true,
auditLogs: true
}
@ -93,6 +98,7 @@ const GCodeFileInfo = () => {
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'GCode File Information' },
{ key: 'parts', label: 'Parts' },
{ key: 'preview', label: 'GCode File Preview' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
@ -128,7 +134,7 @@ const GCodeFileInfo = () => {
</Space>
</Flex>
<div style={{ height: '100%', overflowY: 'scroll' }}>
<ScrollBox>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
@ -163,7 +169,25 @@ const GCodeFileInfo = () => {
isEditing={isEditing}
type='gcodeFile'
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
@ -224,7 +248,7 @@ const GCodeFileInfo = () => {
)}
</InfoCollapse>
</Flex>
</div>
</ScrollBox>
</Flex>
</>
)

View File

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

View File

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

View File

@ -20,6 +20,7 @@ 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('PrinterInfo')
log.setLevel(config.logLevel)
@ -122,7 +123,7 @@ const PrinterInfo = () => {
</Space>
</Flex>
<div style={{ height: '100%', overflowY: 'scroll' }}>
<ScrollBox>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
@ -202,7 +203,7 @@ const PrinterInfo = () => {
)}
</InfoCollapse>
</Flex>
</div>
</ScrollBox>
</Flex>
</>
)

View File

@ -51,9 +51,15 @@ const routeKeyMap = {
const ProductionSidebar = (props) => {
const location = useLocation()
const selectedKey = (() => {
const match = Object.keys(routeKeyMap).find((path) =>
location.pathname.startsWith(path)
)
const match = Object.keys(routeKeyMap).find((path) => {
const pathSplit = path.split('/')
const locationPathSplit = location.pathname.split('/')
if (pathSplit.length > locationPathSplit.length) return false
for (let i = 0; i < pathSplit.length; i++) {
if (pathSplit[i] !== locationPathSplit[i]) return false
}
return true
})
return match ? routeKeyMap[match] : 'overview'
})()

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 { Flex, Alert } from 'antd'
import { createElement } from 'react'
import { Flex, Alert, Button, Dropdown, Popover } from 'antd'
import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon'
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) => {
if (type === 'error' || priority === '9') return 'error'
if (type === 'warning' || priority === '8') return 'warning'
return 'info'
}
const printerModel = getModelByName('printer')
const navigate = useNavigate()
const getAlertIcon = (type, priority) => {
if (type === 'error' || priority === '9') return <ExclamationOctagonIcon />
if (type === 'warning' || priority === '8')
@ -17,34 +31,185 @@ const AlertsDisplay = ({ alerts = [] }) => {
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) {
return null
}
return (
<Flex gap='small'>
{alerts.map((alert, index) => (
<Alert
key={`${alert.createdAt}-${index}`}
message={alert.message}
style={{ padding: '4px 10px 4px 8px' }}
type={getAlertType(alert.type, alert.priority)}
icon={getAlertIcon(alert.type, alert.priority)}
showIcon
/>
))}
</Flex>
)
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 (
<Alert
key={`${alert.createdAt}-${index}-${alert._id}`}
message={alert.message}
style={{ padding: '4px 10px 4px 8px' }}
type={getAlertType(alert.type, alert.priority)}
icon={getAlertIcon(alert.type, alert.priority)}
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
}
/>
)
})
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 = {
printerId: PropTypes.string.isRequired,
showActions: PropTypes.bool.isRequired,
showDismiss: PropTypes.bool.isRequired,
alerts: PropTypes.arrayOf(
PropTypes.shape({
priority: PropTypes.string.isRequired,
canDismiss: PropTypes.bool.isRequired,
_id: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
createdAt: PropTypes.string.isRequired,
updatedAt: PropTypes.string.isRequired,
message: PropTypes.string.isRequired
message: PropTypes.string,
actions: PropTypes.arrayOf(PropTypes.string)
})
).isRequired
}

View File

@ -153,7 +153,7 @@ const DashboardNavigation = () => {
fontSize: '46px',
height: '16px',
marginLeft: '15px',
marginRight: '5px'
marginRight: '8px'
}}
/>
)}
@ -313,7 +313,7 @@ const DashboardNavigation = () => {
{isElectron ? (
<Flex
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}
</Flex>

View File

@ -36,7 +36,7 @@ const DashboardWindowButtons = () => {
<Flex align='center'>
{platform == 'darwin' ? (
isFullScreen == false ? (
<div style={{ width: '65px' }} />
<div style={{ width: '80px' }} />
) : null
) : (
<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 { ApiServerContext } from '../context/ApiServerContext'
import UploadIcon from '../../Icons/UploadIcon'
@ -6,6 +6,7 @@ import { useContext, useState, useEffect } from 'react'
import ObjectSelect from './ObjectSelect'
import FileList from './FileList'
import PlusIcon from '../../Icons/PlusIcon'
import { LoadingOutlined } from '@ant-design/icons'
const { Text } = Typography
@ -18,6 +19,8 @@ const FileUpload = ({
showInfo
}) => {
const { uploadFile } = useContext(ApiServerContext)
const [uploading, setUploading] = useState(false)
const [uploadProgress, setUploadProgress] = useState(0)
// Track current files using useState
const [currentFiles, setCurrentFiles] = useState(() => {
@ -56,7 +59,11 @@ const FileUpload = ({
const handleFileUpload = async (file) => {
try {
const uploadedFile = await uploadFile(file)
setUploading(true)
const uploadedFile = await uploadFile(file, {}, (progress) => {
setUploadProgress(progress)
})
setUploading(false)
if (uploadedFile) {
if (multiple) {
// For multiple files, add to existing array
@ -95,7 +102,7 @@ const FileUpload = ({
return (
<Flex gap={'small'} vertical>
{hasNoItems ? (
{hasNoItems && uploading == false ? (
<Flex gap={'small'} align='center'>
<Space.Compact style={{ flexGrow: 1 }}>
<ObjectSelect
@ -123,6 +130,29 @@ const FileUpload = ({
</Upload>
</Flex>
) : 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
files={currentFiles}
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 PropTypes from 'prop-types'
import merge from 'lodash/merge'
import set from 'lodash/set'
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.
* It handles form validation, submission, and error handling logic.
@ -30,41 +42,102 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
const model = getModelByName(type)
// Function to calculate computed values from model properties
const calculateComputedValues = useCallback((currentData, model) => {
if (!model || !model.properties) return {}
const calculateComputedValues = useCallback(
(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
}
model.properties.forEach((property) => {
// Check if this property has a computed value function
if (property.value && typeof property.value === 'function') {
try {
const computedValue = property.value(currentData)
if (computedValue !== undefined) {
computedValues[property.name] = computedValue
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)
if (property.value && typeof property.value === 'function') {
try {
const computedValue = property.value(scopeData || {})
if (computedValue !== undefined) {
computedEntries.push({
namePath: propertyPath,
value: computedValue
})
}
} catch (error) {
console.warn(
`Error calculating value for property ${property.name}:`,
error
)
}
}
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)
})
}
} catch (error) {
console.warn(
`Error calculating value for property ${property.name}:`,
error
)
}
}
})
return computedValues
}, [])
modelDefinition.properties.forEach((property) => {
processProperty(property, currentData)
})
return computedEntries
},
[]
)
// Set initial form values when defaultValues change
useEffect(() => {
if (Object.keys(defaultValues).length > 0) {
// Calculate computed values for initial data
const computedValues = calculateComputedValues(defaultValues, model)
const initialFormData = { ...defaultValues, ...computedValues }
const computedEntries = calculateComputedValues(defaultValues, model)
const computedValuesObject = buildObjectFromEntries(computedEntries)
const initialFormData = merge({}, defaultValues, computedValuesObject)
form.setFieldsValue(initialFormData)
setObjectData((prev) => {
return merge({}, prev, initialFormData)
})
setObjectData((prev) => merge({}, prev, initialFormData))
}
}, [form, defaultValues, calculateComputedValues, model])
@ -102,18 +175,31 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
form={form}
layout='vertical'
style={style}
onValuesChange={(values) => {
onValuesChange={(_changedValues, allFormValues) => {
// Calculate computed values based on current form data
const currentFormData = { ...objectData, ...values }
const computedValues = calculateComputedValues(currentFormData, model)
const currentFormData = merge({}, objectData || {}, allFormValues)
const computedEntries = calculateComputedValues(currentFormData, model)
// Update form with computed values if any were calculated
if (Object.keys(computedValues).length > 0) {
form.setFieldsValue(computedValues)
if (Array.isArray(computedEntries) && computedEntries.length > 0) {
computedEntries.forEach(({ namePath, value }) => {
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)
const allValues = { ...values, ...computedValues }
const computedValuesObject = buildObjectFromEntries(computedEntries)
const allValues = merge({}, allFormValues, computedValuesObject)
setObjectData((prev) => {
return merge({}, prev, allValues)
})

View File

@ -3,6 +3,8 @@ import { Dropdown, Button } from 'antd'
import { getModelByName } from '../../../database/ObjectModels'
import PropTypes from 'prop-types'
import { useNavigate, useLocation } from 'react-router-dom'
import { useActionsModal } from '../context/ActionsModalContext'
import KeyboardShortcut from './KeyboardShortcut'
// Recursively filter actions based on visibleActions
function filterActionsByVisibility(actions, visibleActions) {
@ -43,6 +45,7 @@ function mapActionsToMenuItems(actions, currentUrlWithActions, id, objectData) {
const actionUrl = action.url ? action.url(id) : undefined
var disabled = actionUrl && actionUrl === currentUrlWithActions
var visible = true
if (action.disabled) {
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 = {
key: action.key || action.name,
label: action.label,
@ -67,7 +78,9 @@ function mapActionsToMenuItems(actions, currentUrlWithActions, id, objectData) {
objectData
)
}
return item
if (visible == true) {
return item
}
})
}
@ -91,6 +104,7 @@ const ObjectActions = ({
const actions = model.actions || []
const navigate = useNavigate()
const location = useLocation()
const { showActionsModal } = useActionsModal()
// Get current url without 'action' param
const currentUrlWithoutActions = stripActionParam(
@ -140,11 +154,20 @@ const ObjectActions = ({
}
return (
<Dropdown menu={menu} {...dropdownProps}>
<Button {...buttonProps} disabled={disabled}>
Actions
</Button>
</Dropdown>
<KeyboardShortcut
shortcut='alt+a'
onTrigger={() => showActionsModal(id, type, objectData)}
>
<Dropdown menu={menu} {...dropdownProps}>
<Button
{...buttonProps}
disabled={disabled}
onClick={() => showActionsModal(id, type, objectData)}
>
Actions
</Button>
</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 DeleteObjectModal from './DeleteObjectModal'
import merge from 'lodash/merge'
import set from 'lodash/set'
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.
* It handles fetching, updating, locking, unlocking, and validation logic.
@ -37,6 +49,7 @@ const ObjectForm = forwardRef(
const [lock, setLock] = useState({})
const [initialized, setInitialized] = useState(false)
const [isEditing, setIsEditing] = useState(false)
const isEditingRef = useRef(false)
const [formValid, setFormValid] = useState(false)
const [form] = Form.useForm()
@ -115,30 +128,93 @@ const ObjectForm = forwardRef(
})
// Function to calculate computed values from model properties
const calculateComputedValues = useCallback((currentData, model) => {
if (!model || !model.properties) return {}
const calculateComputedValues = useCallback(
(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
}
model.properties.forEach((property) => {
// Check if this property has a computed value function
if (property.value && typeof property.value === 'function') {
try {
const computedValue = property.value(currentData)
if (computedValue !== undefined) {
computedValues[property.name] = computedValue
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)
if (property.value && typeof property.value === 'function') {
try {
const computedValue = property.value(scopeData || {})
if (computedValue !== undefined) {
computedEntries.push({
namePath: propertyPath,
value: computedValue
})
}
} catch (error) {
console.warn(
`Error calculating value for property ${property.name}:`,
error
)
}
}
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)
})
}
} catch (error) {
console.warn(
`Error calculating value for property ${property.name}:`,
error
)
}
}
})
return computedValues
}, [])
modelDefinition.properties.forEach((property) => {
processProperty(property, currentData)
})
return computedEntries
},
[]
)
// Validate form on change (debounced to avoid heavy work on every keystroke)
useEffect(() => {
@ -146,7 +222,8 @@ const ObjectForm = forwardRef(
const currentFormValues = form.getFieldsValue()
const mergedObjectData = {
...serverObjectData.current,
...currentFormValues
...currentFormValues,
_isEditing: isEditingRef.current
}
form
@ -198,12 +275,13 @@ const ObjectForm = forwardRef(
const lockEvent = await fetchObjectLock(id, type)
setLock(lockEvent)
onStateChangeRef.current({ lock: lockEvent })
setObjectData(data)
setObjectData({ ...data, _isEditing: isEditingRef.current })
serverObjectData.current = data
// Calculate and set computed values on initial load
const computedValues = calculateComputedValues(data, model)
const initialFormData = { ...data, ...computedValues }
const computedEntries = calculateComputedValues(data, model)
const computedValuesObject = buildObjectFromEntries(computedEntries)
const initialFormData = merge({}, data, computedValuesObject)
form.setFieldsValue(initialFormData)
setFetchLoading(false)
@ -275,24 +353,37 @@ const ObjectForm = forwardRef(
const startEditing = () => {
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)
}
const cancelEditing = () => {
if (serverObjectData.current) {
// Recalculate computed values when canceling
const computedValues = calculateComputedValues(
const computedEntries = calculateComputedValues(
serverObjectData.current,
model
)
const resetFormData = { ...serverObjectData.current, ...computedValues }
const computedValuesObject = buildObjectFromEntries(computedEntries)
const resetFormData = merge(
{},
serverObjectData.current,
computedValuesObject
)
setIsEditing(false)
isEditingRef.current = false
form.setFieldsValue(resetFormData)
setObjectData(resetFormData)
console.log('IS EDITING FALSE')
setObjectData({ ...resetFormData, _isEditing: isEditingRef.current })
}
setIsEditing(false)
onStateChangeRef.current({ isEditing: false })
onStateChangeRef.current({ isEditing: isEditingRef.current })
unlockObject(id, type)
}
@ -302,9 +393,15 @@ const ObjectForm = forwardRef(
setEditLoading(true)
onStateChangeRef.current({ editLoading: true })
await updateObject(id, type, value)
setObjectData({ ...objectData, ...value })
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')
} catch (err) {
console.error(err)
@ -374,37 +471,51 @@ const ObjectForm = forwardRef(
form={form}
layout='vertical'
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) {
onEdit(values)
onEdit(allFormValues)
}
// Calculate computed values based on current form data
const currentFormData = { ...objectData, ...values }
const computedValues = calculateComputedValues(
const currentFormData = {
...(serverObjectData.current || {}),
...allFormValues
}
const computedEntries = calculateComputedValues(
currentFormData,
model
)
// Update form with computed values if any were calculated and they changed
if (Object.keys(computedValues).length > 0) {
const currentComputedValues = form.getFieldsValue(
Object.keys(computedValues)
)
const hasDiff = Object.keys(computedValues).some(
(key) => currentComputedValues[key] !== computedValues[key]
)
if (hasDiff) {
form.setFieldsValue(computedValues)
}
if (Array.isArray(computedEntries) && computedEntries.length > 0) {
computedEntries.forEach(({ namePath, value }) => {
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)
const allValues = { ...values, ...computedValues }
const computedValuesObject = buildObjectFromEntries(computedEntries)
const mergedFormValues = merge(
{},
allFormValues,
computedValuesObject
)
mergedFormValues._isEditing = isEditingRef.current
setObjectData((prev) => {
return { ...prev, ...allValues }
return { ...prev, ...mergedFormValues }
})
}}
>

View File

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

View File

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

View File

@ -10,11 +10,18 @@ const ObjectTypeSelect = ({
placeholder = 'Select object type...',
showSearch = true,
allowClear = true,
disabled = false
disabled = false,
masterFilter = null
}) => {
// Create options from object models
const options = objectModels
.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) => ({
value: model.name,
label: <ObjectTypeDisplay objectType={model.name} />,
@ -46,7 +53,8 @@ ObjectTypeSelect.propTypes = {
placeholder: PropTypes.string,
showSearch: PropTypes.bool,
allowClear: PropTypes.bool,
disabled: PropTypes.bool
disabled: PropTypes.bool,
masterFilter: PropTypes.object
}
export default ObjectTypeSelect

View File

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

View File

@ -58,6 +58,7 @@ const PropertyChanges = ({ type, value }) => {
longId={false}
minimal={true}
objectData={value?.old}
maxWidth='200px'
/>
) : null}
{value?.old && value?.new ? (
@ -71,6 +72,7 @@ const PropertyChanges = ({ type, value }) => {
longId={false}
minimal={true}
objectData={value?.new}
maxWidth='200px'
/>
) : null}
</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',
'queued',
'printing',
'used'
'used',
'deploying'
]
const orangeProgressTypes = ['used', 'deploying', 'queued']
const activeProgressTypes = ['printing', 'deploying']
const currentState = state || {
type: 'unknown',
progress: 0
@ -39,8 +43,12 @@ const StateDisplay = ({
currentState?.progress > 0 ? (
<Progress
percent={Math.round(currentState.progress * 100)}
status={currentState.type === 'used' ? '' : 'active'}
strokeColor={currentState.type === 'used' ? 'orange' : ''}
status={
activeProgressTypes.includes(currentState.type) ? 'active' : ''
}
strokeColor={
orangeProgressTypes.includes(currentState.type) ? 'orange' : ''
}
style={{ width: '150px', marginBottom: '2px' }}
/>
) : null}

View File

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

View File

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

View File

@ -58,10 +58,10 @@ const WizardView = ({
gap={'middle'}
style={
sideBarGrow == false
? { width: '100%' }
? { width: '100%', minWidth: 0 }
: isMobile
? { width: '100%' }
: { width: '400px' }
? { width: '100%', minWidth: 0 }
: { width: '400px', minWidth: 0 }
}
>
<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]
)
const clearSubscriptions = useCallback(() => {
subscribedCallbacksRef.current.clear()
subscribedLockCallbacksRef.current.clear()
}, [])
const connectToServer = useCallback(() => {
if (token && authenticated == true) {
logger.debug('Token is available, connecting to api server...')
@ -101,6 +106,7 @@ const ApiServerProvider = ({ children }) => {
newSocket.on('disconnect', () => {
logger.debug('Api Server disconnected')
setError('Api Server disconnected')
clearSubscriptions()
setConnected(false)
})
@ -108,16 +114,10 @@ const ApiServerProvider = ({ children }) => {
logger.error('Api Server connection error:', err)
messageApi.error('Api Server connection error: ' + err.message)
setError('Api Server connection error')
clearSubscriptions()
setConnected(false)
})
newSocket.on('bridge.notification', (data) => {
notificationApi[data.type]({
title: data.title,
message: data.message
})
})
newSocket.on('error', (err) => {
logger.error('Api Server error:', err)
setError('Api Server error')
@ -445,6 +445,7 @@ const ApiServerProvider = ({ children }) => {
(id, objectType, eventType, callback) => {
if (socketRef.current && socketRef.current.connected == true) {
const callbacksRefKey = `${objectType}:${id}:events:${eventType}`
// Remove callback from the subscribed callbacks map
if (subscribedCallbacksRef.current.has(callbacksRefKey)) {
const callbacks = subscribedCallbacksRef.current
@ -452,6 +453,7 @@ const ApiServerProvider = ({ children }) => {
.filter((cb) => cb !== callback)
if (callbacks.length === 0) {
subscribedCallbacksRef.current.delete(callbacksRefKey)
console.log('Unsubscribing from object event:', callbacksRefKey)
socketRef.current.emit('unsubscribeObjectEvent', {
_id: id,
objectType,
@ -479,6 +481,7 @@ const ApiServerProvider = ({ children }) => {
subscribedCallbacksRef.current.get(callbacksRefKey).length
if (callbacksLength <= 0) {
console.log('Subscribing to object event:', callbacksRefKey)
socketRef.current.emit(
'subscribeToObjectEvent',
{
@ -662,7 +665,12 @@ const ApiServerProvider = ({ children }) => {
`${config.backendUrl}/${type.toLowerCase()}s/properties`,
{
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
masterFilter: JSON.stringify(masterFilter)
},
@ -932,7 +940,11 @@ const ApiServerProvider = ({ children }) => {
}
// Upload file to the API
const uploadFile = async (file, additionalData = {}) => {
const uploadFile = async (
file,
additionalData = {},
progressCallback = null
) => {
const uploadUrl = `${config.backendUrl}/files`
logger.debug('Uploading file:', file.name, 'to:', uploadUrl)
@ -955,6 +967,9 @@ const ApiServerProvider = ({ children }) => {
(progressEvent.loaded * 100) / progressEvent.total
)
logger.debug(`Upload progress: ${percentCompleted}%`)
if (progressCallback) {
progressCallback(percentCompleted)
}
}
})
@ -963,7 +978,7 @@ const ApiServerProvider = ({ children }) => {
} catch (err) {
console.error('File upload error:', err)
showError(err, () => {
uploadFile(file, additionalData)
uploadFile(file, additionalData, progressCallback)
})
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 { Part } from './models/Part.js'
import { Vendor } from './models/Vendor'
import { Courier } from './models/Courier'
import { CourierService } from './models/CourierService'
import { File } from './models/File'
import { SubJob } from './models/SubJob'
import { Initial } from './models/Initial'
@ -15,6 +17,7 @@ import { StockEvent } from './models/StockEvent'
import { StockAudit } from './models/StockAudit'
import { PartStock } from './models/PartStock'
import { ProductStock } from './models/ProductStock'
import { PurchaseOrder } from './models/PurchaseOrder'
import { AuditLog } from './models/AuditLog'
import { User } from './models/User'
import { NoteType } from './models/NoteType'
@ -35,6 +38,8 @@ export const objectModels = [
Product,
Part,
Vendor,
Courier,
CourierService,
File,
SubJob,
Initial,
@ -43,6 +48,7 @@ export const objectModels = [
StockAudit,
PartStock,
ProductStock,
PurchaseOrder,
AuditLog,
User,
NoteType,
@ -64,6 +70,8 @@ export {
Product,
Part,
Vendor,
Courier,
CourierService,
File,
SubJob,
Initial,
@ -72,6 +80,7 @@ export {
StockAudit,
PartStock,
ProductStock,
PurchaseOrder,
AuditLog,
User,
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 ReloadIcon from '../../components/Icons/ReloadIcon'
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 dayjs from 'dayjs'
@ -33,7 +35,31 @@ export const DocumentJob = {
row: true,
icon: EditIcon,
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'],

View File

@ -2,6 +2,8 @@ import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon'
import EditIcon from '../../components/Icons/EditIcon'
import DocumentPrinterIcon from '../../components/Icons/DocumentPrinterIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
export const DocumentPrinter = {
name: 'documentPrinter',
@ -32,7 +34,31 @@ export const DocumentPrinter = {
row: true,
icon: EditIcon,
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: [
@ -101,6 +127,21 @@ export const DocumentPrinter = {
type: 'bool',
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',
label: 'Host',

View File

@ -1,6 +1,8 @@
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon'
import EditIcon from '../../components/Icons/EditIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
import DocumentSizeIcon from '../../components/Icons/DocumentSizeIcon'
export const DocumentSize = {
@ -32,7 +34,31 @@ export const DocumentSize = {
row: true,
icon: EditIcon,
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: [

View File

@ -1,6 +1,8 @@
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon'
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 DocumentTemplateIcon from '../../components/Icons/DocumentTemplateIcon'
@ -41,7 +43,31 @@ export const DocumentTemplate = {
row: true,
icon: EditIcon,
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: [

View File

@ -2,6 +2,8 @@ import EditIcon from '../../components/Icons/EditIcon'
import FilamentIcon from '../../components/Icons/FilamentIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
export const Filament = {
name: 'filament',
@ -30,7 +32,31 @@ export const Filament = {
row: true,
icon: EditIcon,
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: [

View File

@ -2,6 +2,8 @@ import DownloadIcon from '../../components/Icons/DownloadIcon'
import FileIcon from '../../components/Icons/FileIcon'
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'
@ -31,7 +33,31 @@ export const File = {
label: 'Edit',
row: true,
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',

View File

@ -1,5 +1,7 @@
import DownloadIcon from '../../components/Icons/DownloadIcon'
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 InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon'
@ -39,7 +41,31 @@ export const GCodeFile = {
row: true,
icon: EditIcon,
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',
label: 'Cost',
type: 'number',
roundNumber: 2,
value: (objectData) => {
return (
objectData?.file?.metaData?.filamentUsedG * objectData?.filament?.cost
objectData?.file?.metaData?.filamentUsedG *
(objectData?.filament?.cost / 1000)
)
},
readOnly: true,
@ -196,6 +224,51 @@ export const GCodeFile = {
label: 'Print Profile',
type: 'text',
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 ReloadIcon from '../../components/Icons/ReloadIcon'
import EditIcon from '../../components/Icons/EditIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
import OTPIcon from '../../components/Icons/OTPIcon'
export const Host = {
@ -38,7 +40,32 @@ export const Host = {
label: 'Edit',
row: true,
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'],
@ -54,8 +81,8 @@ export const Host = {
showCopy: true
},
{
name: 'connectedAt',
label: 'Connected At',
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
@ -67,6 +94,12 @@ export const Host = {
columnWidth: 200,
columnFixed: 'left'
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true
},
{
name: 'state',
label: 'State',
@ -76,10 +109,10 @@ export const Host = {
readOnly: true
},
{
name: 'active',
label: 'Active',
type: 'bool',
required: true
name: 'connectedAt',
label: 'Connected At',
type: 'dateTime',
readOnly: true
},
{
name: 'online',
@ -87,6 +120,13 @@ export const Host = {
type: 'bool',
readOnly: true
},
{
name: 'active',
label: 'Active',
type: 'bool',
required: true
},
{
name: 'deviceInfo.os',
label: 'Operating System',
@ -158,6 +198,14 @@ export const Host = {
label: 'Tags',
type: 'tags',
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',
showCopy: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
{
name: 'state',
label: 'State',
@ -65,7 +71,39 @@ export const Job = {
showProgress: true,
showId: 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
},
{
@ -82,33 +120,6 @@ export const Job = {
type: 'id',
objectType: 'gcodeFile',
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 ReloadIcon from '../../components/Icons/ReloadIcon'
import EditIcon from '../../components/Icons/EditIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
export const NoteType = {
name: 'noteType',
@ -30,7 +32,31 @@ export const NoteType = {
row: true,
icon: EditIcon,
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'],

View File

@ -2,6 +2,8 @@ import EditIcon from '../../components/Icons/EditIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import PartIcon from '../../components/Icons/PartIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
export const Part = {
name: 'part',
@ -29,7 +31,32 @@ export const Part = {
label: 'Edit',
row: true,
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: [
@ -71,22 +98,6 @@ export const Part = {
type: 'dateTime',
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',
label: 'Vendor',
@ -111,11 +122,14 @@ export const Part = {
objectType: 'vendor'
},
{
name: 'globalPricing',
label: 'Global Price',
name: 'cost',
label: 'Cost',
columnWidth: 150,
required: true,
type: 'bool'
type: 'number',
prefix: '£',
min: 0,
step: 0.01
},
{
name: 'priceMode',
@ -142,18 +156,21 @@ export const Part = {
step: 0.01
},
{
name: 'amount',
label: 'Amount',
name: 'price',
label: 'Price',
required: true,
disabled: (objectData) => {
return (
objectData.globalPricing == true || objectData.priceMode == 'margin'
)
},
type: 'number',
prefix: '£',
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',

View File

@ -2,7 +2,7 @@ import PartStockIcon from '../../components/Icons/PartStockIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const PartStock = {
name: 'partstock',
name: 'partStock',
label: 'Part Stock',
prefix: 'PTS',
icon: PartStockIcon,
@ -13,8 +13,124 @@ export const PartStock = {
default: true,
row: true,
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 ReloadIcon from '../../components/Icons/ReloadIcon'
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 PauseCircleIcon from '../../components/Icons/PauseCircleIcon'
import StopCircleIcon from '../../components/Icons/StopCircleIcon'
import FilamentStockIcon from '../../components/Icons/FilamentStockIcon'
import ControlIcon from '../../components/Icons/ControlIcon'
export const Printer = {
name: 'printer',
label: 'Printer',
@ -32,7 +36,7 @@ export const Printer = {
name: 'control',
label: 'Control',
row: true,
icon: PlayCircleIcon,
icon: ControlIcon,
url: (_id) => `/dashboard/production/printers/control?printerId=${_id}`
},
{
@ -41,7 +45,31 @@ export const Printer = {
row: true,
icon: EditIcon,
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' },
{
@ -98,12 +126,13 @@ export const Printer = {
label: 'Start Queue',
icon: PlayCircleIcon,
disabled: (objectData) => {
console.log(objectData?.subJobs?.length)
console.log(objectData?.queue?.length)
return (
objectData?.state?.type == 'error' ||
objectData?.state?.type == 'printing' ||
objectData?.subJobs?.length == 0 ||
objectData?.subJobs?.length == undefined
objectData?.state?.type == 'paused' ||
objectData?.queue?.length == 0 ||
objectData?.queue?.length == undefined
)
},
url: (_id) =>
@ -125,7 +154,7 @@ export const Printer = {
label: 'Resume Job',
icon: PlayCircleIcon,
disabled: (objectData) => {
return objectData?.state?.type != 'printing'
return objectData?.state?.type != 'paused'
},
url: (_id) =>
`/dashboard/production/printers/control?printerId=${_id}&action=resumeJob`
@ -137,7 +166,7 @@ export const Printer = {
disabled: (objectData) => {
return (
objectData?.state?.type != 'printing' &&
objectData?.state?.type != 'error'
objectData?.state?.type != 'paused'
)
},
url: (_id) =>
@ -149,20 +178,37 @@ export const Printer = {
name: 'filamentStock',
label: 'Filament Stock',
icon: FilamentStockIcon,
disabled: (objectData) => {
return objectData?.online == false
},
children: [
{
name: 'loadFilamentStock',
label: 'Load Filament Stock',
icon: FilamentStockIcon,
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',
label: 'Unload Filament Stock',
icon: FilamentStockIcon,
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',
objectType: 'printer',
showName: false,
readOnly: true
readOnly: true,
columnWidth: 250
},
{
name: 'connectedAt',
@ -354,7 +401,7 @@ export const Printer = {
required: false
},
{
name: 'subJobs',
name: 'queue',
label: 'Queue',
type: 'objectList',
objectType: 'subJob',

View File

@ -2,6 +2,8 @@ import ProductIcon from '../../components/Icons/ProductIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon'
import EditIcon from '../../components/Icons/EditIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
export const Product = {
name: 'product',
@ -30,7 +32,31 @@ export const Product = {
row: true,
icon: EditIcon,
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: [
@ -129,6 +155,51 @@ export const Product = {
prefix: '£',
min: 0,
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'
export const StockAudit = {
name: 'stockaudit',
name: 'stockAudit',
label: 'Stock Audit',
prefix: 'SAU',
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}`,
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 InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
export const SubJob = {
name: 'subJob',
label: 'Sub Job',
prefix: 'SJB',
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'],
filters: ['state', '_id', 'job._id', 'printer._id'],
sorters: ['createdAt', 'state'],
group: ['job'],
properties: [
{
name: '_id',
@ -19,6 +42,68 @@ export const SubJob = {
columnWidth: 140,
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',
label: 'Printer',
@ -34,33 +119,6 @@ export const SubJob = {
columnFixed: 'left',
showHyperlink: true,
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 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'
@ -31,7 +33,31 @@ export const Vendor = {
row: true,
icon: EditIcon,
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' },
{
@ -43,7 +69,6 @@ export const Vendor = {
`/dashboard/management/vendors/info?vendorId=${_id}&action=delete`
}
],
url: (id) => `/dashboard/management/vendors/info?vendorId=${id}`,
columns: ['name', '_id', 'country', 'email', 'website', 'createdAt'],
filters: ['name', '_id', 'country', 'email'],
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 FilamentStockInfo from '../components/Dashboard/Inventory/FilamentStocks/FilamentStockInfo.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 StockAudits from '../components/Dashboard/Inventory/StockAudits.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 = [
<Route
@ -23,6 +26,11 @@ const InventoryRoutes = [
path='inventory/partstocks'
element={<PartStocks />}
/>,
<Route
key='partstocks-info'
path='inventory/partstocks/info'
element={<PartStockInfo />}
/>,
<Route
key='stockevents'
path='inventory/stockevents'
@ -37,6 +45,16 @@ const InventoryRoutes = [
key='stockaudits-info'
path='inventory/stockaudits/info'
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 VendorInfo from '../components/Dashboard/Management/Vendors/VendorInfo'
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 AuditLogs from '../components/Dashboard/Management/AuditLogs.jsx'
import NoteTypes from '../components/Dashboard/Management/NoteTypes.jsx'
@ -73,6 +77,22 @@ const ManagementRoutes = [
element={<FileInfo />}
/>,
<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-info'

View File

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

1333
yarn.lock

File diff suppressed because it is too large Load Diff