Merge branch 'main' of https://git.tombutcher.work/tom/farmcontrol-ui
This commit is contained in:
commit
8ad9a777a4
@ -318,3 +318,35 @@ body {
|
|||||||
.ant-badge.ant-badge-status {
|
.ant-badge.ant-badge-status {
|
||||||
line-height: 18.5px;
|
line-height: 18.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.simplebar-track.simplebar-vertical {
|
||||||
|
right: -16px;
|
||||||
|
width: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simplebar-scrollbar:before {
|
||||||
|
background: #78787854 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.printer-alerts-display-popover .ant-popover-inner {
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 24px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.child-table-rollups *::-webkit-scrollbar:horizontal {
|
||||||
|
height: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rollup-table .ant-table-container {
|
||||||
|
border-start-start-radius: 0px !important;
|
||||||
|
border-start-end-radius: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rollup-table .ant-table {
|
||||||
|
border-radius: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-selection-item .ant-tag,
|
||||||
|
.ant-select-tree-title .ant-tag {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|||||||
@ -59,6 +59,7 @@
|
|||||||
"react-responsive": "^10.0.1",
|
"react-responsive": "^10.0.1",
|
||||||
"react-router-dom": "^7.8.2",
|
"react-router-dom": "^7.8.2",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
|
"simplebar-react": "^3.3.2",
|
||||||
"socket.io-client": "*",
|
"socket.io-client": "*",
|
||||||
"standard": "^17.1.2",
|
"standard": "^17.1.2",
|
||||||
"styled-components": "^6.1.19",
|
"styled-components": "^6.1.19",
|
||||||
|
|||||||
11
src/App.jsx
11
src/App.jsx
@ -13,6 +13,7 @@ import '../assets/stylesheets/App.css'
|
|||||||
import { PrintServerProvider } from './components/Dashboard/context/PrintServerContext.jsx'
|
import { PrintServerProvider } from './components/Dashboard/context/PrintServerContext.jsx'
|
||||||
import { AuthProvider } from './components/Dashboard/context/AuthContext.jsx'
|
import { AuthProvider } from './components/Dashboard/context/AuthContext.jsx'
|
||||||
import { SpotlightProvider } from './components/Dashboard/context/SpotlightContext.jsx'
|
import { SpotlightProvider } from './components/Dashboard/context/SpotlightContext.jsx'
|
||||||
|
import { ActionsModalProvider } from './components/Dashboard/context/ActionsModalContext.jsx'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ThemeProvider,
|
ThemeProvider,
|
||||||
@ -21,6 +22,7 @@ import {
|
|||||||
import AppError from './components/App/AppError'
|
import AppError from './components/App/AppError'
|
||||||
import { ApiServerProvider } from './components/Dashboard/context/ApiServerContext.jsx'
|
import { ApiServerProvider } from './components/Dashboard/context/ApiServerContext.jsx'
|
||||||
import { ElectronProvider } from './components/Dashboard/context/ElectronContext.jsx'
|
import { ElectronProvider } from './components/Dashboard/context/ElectronContext.jsx'
|
||||||
|
import { MessageProvider } from './components/Dashboard/context/MessageContext.jsx'
|
||||||
import AuthCallback from './components/App/AuthCallback.jsx'
|
import AuthCallback from './components/App/AuthCallback.jsx'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -53,6 +55,8 @@ const AppContent = () => {
|
|||||||
<PrintServerProvider>
|
<PrintServerProvider>
|
||||||
<ApiServerProvider>
|
<ApiServerProvider>
|
||||||
<SpotlightProvider>
|
<SpotlightProvider>
|
||||||
|
<ActionsModalProvider>
|
||||||
|
<MessageProvider>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
path='/'
|
path='/'
|
||||||
@ -67,7 +71,10 @@ const AppContent = () => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route path='/auth/callback' element={<AuthCallback />} />
|
<Route
|
||||||
|
path='/auth/callback'
|
||||||
|
element={<AuthCallback />}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='/dashboard'
|
path='/dashboard'
|
||||||
element={
|
element={
|
||||||
@ -89,6 +96,8 @@ const AppContent = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
</MessageProvider>
|
||||||
|
</ActionsModalProvider>
|
||||||
</SpotlightProvider>
|
</SpotlightProvider>
|
||||||
</ApiServerProvider>
|
</ApiServerProvider>
|
||||||
</PrintServerProvider>
|
</PrintServerProvider>
|
||||||
|
|||||||
@ -34,9 +34,15 @@ const routeKeyMap = {
|
|||||||
const DeveloperSidebar = (props) => {
|
const DeveloperSidebar = (props) => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const selectedKey = (() => {
|
const selectedKey = (() => {
|
||||||
const match = Object.keys(routeKeyMap).find((path) =>
|
const match = Object.keys(routeKeyMap).find((path) => {
|
||||||
location.pathname.startsWith(path)
|
const pathSplit = path.split('/')
|
||||||
)
|
const locationPathSplit = location.pathname.split('/')
|
||||||
|
if (pathSplit.length > locationPathSplit.length) return false
|
||||||
|
for (let i = 0; i < pathSplit.length; i++) {
|
||||||
|
if (pathSplit[i] !== locationPathSplit[i]) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
return match ? routeKeyMap[match] : 'sessionstorage'
|
return match ? routeKeyMap[match] : 'sessionstorage'
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import NoteIcon from '../../../Icons/NoteIcon'
|
|||||||
import AuditLogIcon from '../../../Icons/AuditLogIcon'
|
import AuditLogIcon from '../../../Icons/AuditLogIcon'
|
||||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder'
|
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder'
|
||||||
import DocumentPrintButton from '../../common/DocumentPrintButton'
|
import DocumentPrintButton from '../../common/DocumentPrintButton'
|
||||||
|
import ScrollBox from '../../common/ScrollBox'
|
||||||
|
|
||||||
const FilamentStockInfo = () => {
|
const FilamentStockInfo = () => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
@ -112,12 +113,12 @@ const FilamentStockInfo = () => {
|
|||||||
}}
|
}}
|
||||||
editLoading={objectFormState.editLoading}
|
editLoading={objectFormState.editLoading}
|
||||||
formValid={objectFormState.formValid}
|
formValid={objectFormState.formValid}
|
||||||
disabled={objectFormState.lock?.locked || objectFormState.loading}
|
disabled={true}
|
||||||
loading={objectFormState.editLoading}
|
loading={objectFormState.editLoading}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
</Flex>
|
</Flex>
|
||||||
<div style={{ height: '100%', overflow: 'auto' }}>
|
<ScrollBox>
|
||||||
<Flex vertical gap={'large'}>
|
<Flex vertical gap={'large'}>
|
||||||
<ActionHandler
|
<ActionHandler
|
||||||
actions={actions}
|
actions={actions}
|
||||||
@ -199,7 +200,7 @@ const FilamentStockInfo = () => {
|
|||||||
)}
|
)}
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</ScrollBox>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -202,7 +202,7 @@ const LoadFilamentStock = ({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{targetTemperature > 0 &&
|
{targetTemperature > 0 &&
|
||||||
currentTemperature >= targetTemperature &&
|
currentTemperature >= targetTemperature - 2 &&
|
||||||
filamentSensorDetected == false ? (
|
filamentSensorDetected == false ? (
|
||||||
<Alert
|
<Alert
|
||||||
message={'Insert filament to continue'}
|
message={'Insert filament to continue'}
|
||||||
|
|||||||
@ -1,18 +1,9 @@
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { useState } from 'react'
|
|
||||||
import { useMediaQuery } from 'react-responsive'
|
|
||||||
import { Typography, Flex, Steps, Divider } from 'antd'
|
|
||||||
|
|
||||||
import ObjectInfo from '../../common/ObjectInfo'
|
import ObjectInfo from '../../common/ObjectInfo'
|
||||||
import NewObjectForm from '../../common/NewObjectForm'
|
import NewObjectForm from '../../common/NewObjectForm'
|
||||||
import NewObjectButtons from '../../common/NewObjectButtons'
|
import WizardView from '../../common/WizardView'
|
||||||
|
|
||||||
const { Title } = Typography
|
|
||||||
|
|
||||||
const NewFilamentStock = ({ onOk, reset }) => {
|
const NewFilamentStock = ({ onOk, reset }) => {
|
||||||
const [currentStep, setCurrentStep] = useState(0)
|
|
||||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NewObjectForm
|
<NewObjectForm
|
||||||
type={'filamentStock'}
|
type={'filamentStock'}
|
||||||
@ -30,7 +21,6 @@ const NewFilamentStock = ({ onOk, reset }) => {
|
|||||||
column={1}
|
column={1}
|
||||||
bordered={false}
|
bordered={false}
|
||||||
isEditing={true}
|
isEditing={true}
|
||||||
initial={true}
|
|
||||||
required={true}
|
required={true}
|
||||||
objectData={objectData}
|
objectData={objectData}
|
||||||
/>
|
/>
|
||||||
@ -56,43 +46,16 @@ const NewFilamentStock = ({ onOk, reset }) => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
return (
|
return (
|
||||||
<Flex gap='middle'>
|
<WizardView
|
||||||
{!isMobile && (
|
steps={steps}
|
||||||
<div style={{ minWidth: '160px' }}>
|
loading={submitLoading}
|
||||||
<Steps
|
formValid={formValid}
|
||||||
current={currentStep}
|
title='New Filament Stock'
|
||||||
items={steps}
|
|
||||||
direction='vertical'
|
|
||||||
style={{ width: 'fit-content' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isMobile && (
|
|
||||||
<Divider type='vertical' style={{ height: 'unset' }} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Flex vertical gap='middle' style={{ flexGrow: 1 }}>
|
|
||||||
<Title level={2} style={{ margin: 0 }}>
|
|
||||||
New Filament Stock
|
|
||||||
</Title>
|
|
||||||
<div style={{ minHeight: '260px', marginBottom: 8 }}>
|
|
||||||
{steps[currentStep].content}
|
|
||||||
</div>
|
|
||||||
<NewObjectButtons
|
|
||||||
currentStep={currentStep}
|
|
||||||
totalSteps={steps.length}
|
|
||||||
onPrevious={() => setCurrentStep((prev) => prev - 1)}
|
|
||||||
onNext={() => setCurrentStep((prev) => prev + 1)}
|
|
||||||
onSubmit={() => {
|
onSubmit={() => {
|
||||||
handleSubmit()
|
handleSubmit()
|
||||||
onOk()
|
onOk()
|
||||||
}}
|
}}
|
||||||
formValid={formValid}
|
|
||||||
submitLoading={submitLoading}
|
|
||||||
/>
|
/>
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</NewObjectForm>
|
</NewObjectForm>
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import PartStockIcon from '../../Icons/PartStockIcon'
|
|||||||
import ProductStockIcon from '../../Icons/ProductStockIcon'
|
import ProductStockIcon from '../../Icons/ProductStockIcon'
|
||||||
import StockEventIcon from '../../Icons/StockEventIcon'
|
import StockEventIcon from '../../Icons/StockEventIcon'
|
||||||
import StockAuditIcon from '../../Icons/StockAuditIcon'
|
import StockAuditIcon from '../../Icons/StockAuditIcon'
|
||||||
|
import PurchaseOrderIcon from '../../Icons/PurchaseOrderIcon'
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
@ -34,6 +35,13 @@ const items = [
|
|||||||
path: '/dashboard/inventory/productstocks'
|
path: '/dashboard/inventory/productstocks'
|
||||||
},
|
},
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
|
{
|
||||||
|
key: 'purchaseorders',
|
||||||
|
label: 'Purchase Orders',
|
||||||
|
icon: <PurchaseOrderIcon />,
|
||||||
|
path: '/dashboard/inventory/purchaseorders'
|
||||||
|
},
|
||||||
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
key: 'stockevents',
|
key: 'stockevents',
|
||||||
label: 'Stock Events',
|
label: 'Stock Events',
|
||||||
@ -54,16 +62,23 @@ const routeKeyMap = {
|
|||||||
'/dashboard/inventory/partstocks': 'partstocks',
|
'/dashboard/inventory/partstocks': 'partstocks',
|
||||||
'/dashboard/inventory/productstocks': 'productstocks',
|
'/dashboard/inventory/productstocks': 'productstocks',
|
||||||
'/dashboard/inventory/stockevents': 'stockevents',
|
'/dashboard/inventory/stockevents': 'stockevents',
|
||||||
'/dashboard/inventory/stockaudits': 'stockaudits'
|
'/dashboard/inventory/stockaudits': 'stockaudits',
|
||||||
|
'/dashboard/inventory/purchaseorders': 'purchaseorders'
|
||||||
}
|
}
|
||||||
|
|
||||||
const InventorySidebar = (props) => {
|
const InventorySidebar = (props) => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const selectedKey = (() => {
|
const selectedKey = (() => {
|
||||||
const match = Object.keys(routeKeyMap).find((path) =>
|
const match = Object.keys(routeKeyMap).find((path) => {
|
||||||
location.pathname.startsWith(path)
|
const pathSplit = path.split('/')
|
||||||
)
|
const locationPathSplit = location.pathname.split('/')
|
||||||
return match ? routeKeyMap[match] : 'filaments'
|
if (pathSplit.length > locationPathSplit.length) return false
|
||||||
|
for (let i = 0; i < pathSplit.length; i++) {
|
||||||
|
if (pathSplit[i] !== locationPathSplit[i]) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return match ? routeKeyMap[match] : 'overview'
|
||||||
})()
|
})()
|
||||||
|
|
||||||
return <DashboardSidebar items={items} selectedKey={selectedKey} {...props} />
|
return <DashboardSidebar items={items} selectedKey={selectedKey} {...props} />
|
||||||
|
|||||||
@ -1,119 +1,64 @@
|
|||||||
import { useState, useEffect } from 'react'
|
|
||||||
import { Form, Input, Button, Space, Select, InputNumber } from 'antd'
|
|
||||||
import axios from 'axios'
|
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import config from '../../../../config'
|
import ObjectInfo from '../../common/ObjectInfo'
|
||||||
|
import NewObjectForm from '../../common/NewObjectForm'
|
||||||
|
import WizardView from '../../common/WizardView'
|
||||||
|
|
||||||
const NewPartStock = ({ onOk, reset }) => {
|
const NewPartStock = ({ onOk, reset }) => {
|
||||||
const [form] = Form.useForm()
|
|
||||||
const [parts, setParts] = useState([])
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Reset form when reset prop changes
|
|
||||||
if (reset) {
|
|
||||||
form.resetFields()
|
|
||||||
}
|
|
||||||
}, [reset, form])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Fetch parts for the select dropdown
|
|
||||||
const fetchParts = async () => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(`${config.backendUrl}/parts`, {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json'
|
|
||||||
},
|
|
||||||
withCredentials: true
|
|
||||||
})
|
|
||||||
setParts(response.data)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching parts:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchParts()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const onFinish = async (values) => {
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
|
||||||
await axios.post(
|
|
||||||
`${config.backendUrl}/partstocks`,
|
|
||||||
{
|
|
||||||
part: values.part,
|
|
||||||
startingLots: values.startingLots,
|
|
||||||
currentLots: values.startingLots, // Initially current lots equals starting lots
|
|
||||||
notes: values.notes
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
withCredentials: true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
onOk()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error creating part stock:', error)
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<NewObjectForm
|
||||||
form={form}
|
type={'partStock'}
|
||||||
layout='vertical'
|
reset={reset}
|
||||||
onFinish={onFinish}
|
defaultValues={{ state: { type: 'new' } }}
|
||||||
style={{ maxWidth: '100%' }}
|
|
||||||
>
|
>
|
||||||
<Form.Item
|
{({ handleSubmit, submitLoading, objectData, formValid }) => {
|
||||||
name='part'
|
const steps = [
|
||||||
label='Part'
|
{
|
||||||
rules={[{ required: true, message: 'Please select a part' }]}
|
title: 'Required',
|
||||||
>
|
key: 'required',
|
||||||
<Select
|
content: (
|
||||||
placeholder='Select a part'
|
<ObjectInfo
|
||||||
options={parts.map((part) => ({
|
type='partStock'
|
||||||
value: part._id,
|
column={1}
|
||||||
label: part.name
|
bordered={false}
|
||||||
}))}
|
isEditing={true}
|
||||||
|
required={true}
|
||||||
|
objectData={objectData}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
)
|
||||||
|
},
|
||||||
<Form.Item
|
{
|
||||||
name='startingLots'
|
title: 'Summary',
|
||||||
label='Starting Lots'
|
key: 'summary',
|
||||||
rules={[
|
content: (
|
||||||
{ required: true, message: 'Please enter the starting lots' },
|
<ObjectInfo
|
||||||
{ type: 'number', min: 1, message: 'Lots must be at least 1' }
|
type='partStock'
|
||||||
]}
|
column={1}
|
||||||
>
|
bordered={false}
|
||||||
<InputNumber
|
visibleProperties={{
|
||||||
style={{ width: '100%' }}
|
_id: false,
|
||||||
placeholder='Enter starting lots'
|
createdAt: false,
|
||||||
min={1}
|
updatedAt: false
|
||||||
|
}}
|
||||||
|
isEditing={false}
|
||||||
|
objectData={objectData}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
)
|
||||||
|
}
|
||||||
<Form.Item name='notes' label='Notes'>
|
]
|
||||||
<Input.TextArea
|
return (
|
||||||
placeholder='Enter any additional notes'
|
<WizardView
|
||||||
autoSize={{ minRows: 3, maxRows: 6 }}
|
steps={steps}
|
||||||
|
loading={submitLoading}
|
||||||
|
formValid={formValid}
|
||||||
|
title='New Part Stock'
|
||||||
|
onSubmit={() => {
|
||||||
|
handleSubmit()
|
||||||
|
onOk()
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
)
|
||||||
|
}}
|
||||||
<Form.Item>
|
</NewObjectForm>
|
||||||
<Space>
|
|
||||||
<Button type='primary' htmlType='submit' loading={loading}>
|
|
||||||
Create Part Stock
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => form.resetFields()}>Reset</Button>
|
|
||||||
</Space>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,8 +67,4 @@ NewPartStock.propTypes = {
|
|||||||
reset: PropTypes.bool
|
reset: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
NewPartStock.defaultProps = {
|
|
||||||
reset: false
|
|
||||||
}
|
|
||||||
|
|
||||||
export default NewPartStock
|
export default NewPartStock
|
||||||
|
|||||||
209
src/components/Dashboard/Inventory/PartStocks/PartStockInfo.jsx
Normal file
209
src/components/Dashboard/Inventory/PartStocks/PartStockInfo.jsx
Normal 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
|
||||||
101
src/components/Dashboard/Inventory/PurchaseOrders.jsx
Normal file
101
src/components/Dashboard/Inventory/PurchaseOrders.jsx
Normal 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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -1,143 +1,34 @@
|
|||||||
import { useState, useContext, useRef, useEffect } from 'react'
|
// src/stockAudits.js
|
||||||
import { useNavigate } from 'react-router-dom'
|
|
||||||
import { Button, Flex, Space, message, Dropdown, Typography } from 'antd'
|
|
||||||
|
|
||||||
import { AuthContext } from '../context/AuthContext'
|
import { useState, useRef } from 'react'
|
||||||
import { PrintServerContext } from '../context/PrintServerContext'
|
|
||||||
|
|
||||||
import IdDisplay from '../common/IdDisplay'
|
import { Button, Flex, Space, Modal, Dropdown, message } from 'antd'
|
||||||
import StockAuditIcon from '../../Icons/StockAuditIcon'
|
|
||||||
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
|
import NewStockAudit from './StockAudits/NewStockAudit'
|
||||||
import PlusIcon from '../../Icons/PlusIcon'
|
import PlusIcon from '../../Icons/PlusIcon'
|
||||||
import ReloadIcon from '../../Icons/ReloadIcon'
|
import ReloadIcon from '../../Icons/ReloadIcon'
|
||||||
import TimeDisplay from '../common/TimeDisplay'
|
import useColumnVisibility from '../hooks/useColumnVisibility'
|
||||||
import ObjectTable from '../common/ObjectTable'
|
import ObjectTable from '../common/ObjectTable'
|
||||||
|
import ListIcon from '../../Icons/ListIcon'
|
||||||
import config from '../../../config'
|
import GridIcon from '../../Icons/GridIcon'
|
||||||
|
import useViewMode from '../hooks/useViewMode'
|
||||||
const { Text } = Typography
|
import ColumnViewButton from '../common/ColumnViewButton'
|
||||||
|
|
||||||
const StockAudits = () => {
|
const StockAudits = () => {
|
||||||
const [messageApi, contextHolder] = message.useMessage()
|
const [messageApi, contextHolder] = message.useMessage()
|
||||||
const navigate = useNavigate()
|
|
||||||
const { printServer } = useContext(PrintServerContext)
|
|
||||||
const [initialized, setInitialized] = useState(false)
|
|
||||||
const tableRef = useRef()
|
const tableRef = useRef()
|
||||||
|
|
||||||
const { authenticated } = useContext(AuthContext)
|
const [newStockAuditOpen, setNewStockAuditOpen] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
const [viewMode, setViewMode] = useViewMode('stockAudits')
|
||||||
if (printServer && !initialized) {
|
|
||||||
setInitialized(true)
|
|
||||||
printServer.on('notify_stockaudit_update', (updateData) => {
|
|
||||||
if (tableRef.current) {
|
|
||||||
tableRef.current.updateData(updateData._id, updateData)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
const [columnVisibility, setColumnVisibility] =
|
||||||
if (printServer && initialized) {
|
useColumnVisibility('stockAudits')
|
||||||
printServer.off('notify_stockaudit_update')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [printServer, initialized])
|
|
||||||
|
|
||||||
const getStockAuditActionItems = (id) => {
|
|
||||||
return {
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
label: 'Info',
|
|
||||||
key: 'info',
|
|
||||||
icon: <InfoCircleIcon />
|
|
||||||
}
|
|
||||||
],
|
|
||||||
onClick: ({ key }) => {
|
|
||||||
if (key === 'info') {
|
|
||||||
navigate(`/dashboard/inventory/stockaudits/info?stockAuditId=${id}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: '',
|
|
||||||
dataIndex: '',
|
|
||||||
key: 'icon',
|
|
||||||
width: 40,
|
|
||||||
fixed: 'left',
|
|
||||||
render: () => <StockAuditIcon />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'ID',
|
|
||||||
dataIndex: '_id',
|
|
||||||
key: 'id',
|
|
||||||
width: 180,
|
|
||||||
render: (text) => (
|
|
||||||
<IdDisplay id={text} type={'stockaudit'} longId={false} />
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Status',
|
|
||||||
dataIndex: 'status',
|
|
||||||
key: 'status',
|
|
||||||
width: 120,
|
|
||||||
render: (status) => <Text>{status}</Text>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Created At',
|
|
||||||
dataIndex: 'createdAt',
|
|
||||||
key: 'createdAt',
|
|
||||||
width: 180,
|
|
||||||
render: (createdAt) => {
|
|
||||||
if (createdAt) {
|
|
||||||
return <TimeDisplay dateTime={createdAt} />
|
|
||||||
}
|
|
||||||
return 'n/a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Updated At',
|
|
||||||
dataIndex: 'updatedAt',
|
|
||||||
key: 'updatedAt',
|
|
||||||
width: 180,
|
|
||||||
render: (updatedAt) => {
|
|
||||||
if (updatedAt) {
|
|
||||||
return <TimeDisplay dateTime={updatedAt} />
|
|
||||||
}
|
|
||||||
return 'n/a'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Actions',
|
|
||||||
key: 'actions',
|
|
||||||
fixed: 'right',
|
|
||||||
width: 150,
|
|
||||||
render: (text, record) => {
|
|
||||||
return (
|
|
||||||
<Space gap='small'>
|
|
||||||
<Button
|
|
||||||
icon={<InfoCircleIcon />}
|
|
||||||
onClick={() =>
|
|
||||||
navigate(
|
|
||||||
`/dashboard/inventory/stockaudits/info?stockAuditId=${record._id}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Dropdown menu={getStockAuditActionItems(record._id)}>
|
|
||||||
<Button>Actions</Button>
|
|
||||||
</Dropdown>
|
|
||||||
</Space>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const actionItems = {
|
const actionItems = {
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
label: 'New Stock Audit',
|
label: 'New Stock audit',
|
||||||
key: 'newStockAudit',
|
key: 'newStockAudit',
|
||||||
icon: <PlusIcon />
|
icon: <PlusIcon />
|
||||||
},
|
},
|
||||||
@ -152,8 +43,7 @@ const StockAudits = () => {
|
|||||||
if (key === 'reloadList') {
|
if (key === 'reloadList') {
|
||||||
tableRef.current?.reload()
|
tableRef.current?.reload()
|
||||||
} else if (key === 'newStockAudit') {
|
} else if (key === 'newStockAudit') {
|
||||||
// TODO: Implement new stock audit creation
|
setNewStockAuditOpen(true)
|
||||||
messageApi.info('New stock audit creation not implemented yet')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -162,18 +52,54 @@ const StockAudits = () => {
|
|||||||
<>
|
<>
|
||||||
<Flex vertical={'true'} gap='large'>
|
<Flex vertical={'true'} gap='large'>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
<Space>
|
<Flex justify={'space-between'}>
|
||||||
|
<Space size='small'>
|
||||||
<Dropdown menu={actionItems}>
|
<Dropdown menu={actionItems}>
|
||||||
<Button>Actions</Button>
|
<Button>Actions</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
<ColumnViewButton
|
||||||
|
type='stockAudit'
|
||||||
|
loading={false}
|
||||||
|
visibleState={columnVisibility}
|
||||||
|
updateVisibleState={setColumnVisibility}
|
||||||
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
|
||||||
|
onClick={() =>
|
||||||
|
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
<ObjectTable
|
<ObjectTable
|
||||||
ref={tableRef}
|
ref={tableRef}
|
||||||
columns={columns}
|
visibleColumns={columnVisibility}
|
||||||
url={`${config.backendUrl}/stockaudits`}
|
type='stockAudit'
|
||||||
authenticated={authenticated}
|
cards={viewMode === 'cards'}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
<Modal
|
||||||
|
open={newStockAuditOpen}
|
||||||
|
styles={{ content: { paddingBottom: '24px' } }}
|
||||||
|
footer={null}
|
||||||
|
width={800}
|
||||||
|
onCancel={() => {
|
||||||
|
setNewStockAuditOpen(false)
|
||||||
|
}}
|
||||||
|
destroyOnHidden={true}
|
||||||
|
>
|
||||||
|
<NewStockAudit
|
||||||
|
onOk={() => {
|
||||||
|
setNewStockAuditOpen(false)
|
||||||
|
messageApi.success('New stock audit created successfully.')
|
||||||
|
tableRef.current?.reload()
|
||||||
|
}}
|
||||||
|
reset={newStockAuditOpen}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
@ -1,214 +1,207 @@
|
|||||||
import { useEffect, useState, useContext } from 'react'
|
import { useRef, useState } from 'react'
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import axios from 'axios'
|
import { Space, Flex, Card } from 'antd'
|
||||||
import {
|
import { LoadingOutlined } from '@ant-design/icons'
|
||||||
Card,
|
import loglevel from 'loglevel'
|
||||||
Descriptions,
|
import config from '../../../../config.js'
|
||||||
Button,
|
import useCollapseState from '../../hooks/useCollapseState.js'
|
||||||
Space,
|
import NotesPanel from '../../common/NotesPanel.jsx'
|
||||||
message,
|
import InfoCollapse from '../../common/InfoCollapse.jsx'
|
||||||
Typography,
|
import ObjectInfo from '../../common/ObjectInfo.jsx'
|
||||||
Table,
|
import ViewButton from '../../common/ViewButton.jsx'
|
||||||
Tag
|
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
||||||
} from 'antd'
|
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
||||||
import {
|
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||||
ArrowLeftOutlined,
|
import ObjectForm from '../../common/ObjectForm.jsx'
|
||||||
LoadingOutlined,
|
import EditButtons from '../../common/EditButtons.jsx'
|
||||||
ClockCircleOutlined
|
import LockIndicator from '../../common/LockIndicator.jsx'
|
||||||
} from '@ant-design/icons'
|
import ActionHandler from '../../common/ActionHandler.jsx'
|
||||||
|
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||||
|
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||||
|
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||||
|
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||||
|
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||||
|
|
||||||
import { AuthContext } from '../../context/AuthContext'
|
const log = loglevel.getLogger('StockAuditInfo')
|
||||||
import IdDisplay from '../../common/IdDisplay'
|
log.setLevel(config.logLevel)
|
||||||
import TimeDisplay from '../../common/TimeDisplay'
|
|
||||||
|
|
||||||
import config from '../../../../config'
|
|
||||||
import XMarkCircleIcon from '../../../Icons/XMarkCircleIcon'
|
|
||||||
import CheckCircleIcon from '../../../Icons/CheckCircleIcon'
|
|
||||||
|
|
||||||
const { Text, Title } = Typography
|
|
||||||
|
|
||||||
const StockAuditInfo = () => {
|
const StockAuditInfo = () => {
|
||||||
const [messageApi, contextHolder] = message.useMessage()
|
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const objectFormRef = useRef(null)
|
||||||
const { authenticated } = useContext(AuthContext)
|
const actionHandlerRef = useRef(null)
|
||||||
|
|
||||||
const [stockAudit, setStockAudit] = useState(null)
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
|
|
||||||
const stockAuditId = new URLSearchParams(location.search).get('stockAuditId')
|
const stockAuditId = new URLSearchParams(location.search).get('stockAuditId')
|
||||||
|
const [collapseState, updateCollapseState] = useCollapseState(
|
||||||
useEffect(() => {
|
'StockAuditInfo',
|
||||||
const fetchStockAudit = async () => {
|
|
||||||
if (!stockAuditId) {
|
|
||||||
messageApi.error('No stock audit ID provided')
|
|
||||||
navigate('/dashboard/inventory/stockaudits')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.backendUrl}/stockaudits/${stockAuditId}`,
|
|
||||||
{
|
{
|
||||||
headers: {
|
info: true,
|
||||||
Accept: 'application/json'
|
stocks: true,
|
||||||
|
notes: true,
|
||||||
|
auditLogs: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const [objectFormState, setEditFormState] = useState({
|
||||||
|
isEditing: false,
|
||||||
|
editLoading: false,
|
||||||
|
formValid: false,
|
||||||
|
locked: false,
|
||||||
|
loading: false,
|
||||||
|
objectData: {}
|
||||||
|
})
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
reload: () => {
|
||||||
|
objectFormRef?.current.handleFetchObject()
|
||||||
|
return true
|
||||||
},
|
},
|
||||||
withCredentials: true
|
edit: () => {
|
||||||
}
|
objectFormRef?.current.startEditing()
|
||||||
)
|
return false
|
||||||
setStockAudit(response.data)
|
|
||||||
setLoading(false)
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
messageApi.error('Failed to fetch stock audit details')
|
|
||||||
navigate('/dashboard/inventory/stockaudits')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authenticated) {
|
|
||||||
fetchStockAudit()
|
|
||||||
}
|
|
||||||
}, [authenticated, stockAuditId, messageApi, navigate])
|
|
||||||
|
|
||||||
const getStatusTag = (status) => {
|
|
||||||
switch (status?.toLowerCase()) {
|
|
||||||
case 'completed':
|
|
||||||
return (
|
|
||||||
<Tag icon={<CheckCircleIcon />} color='success'>
|
|
||||||
Completed
|
|
||||||
</Tag>
|
|
||||||
)
|
|
||||||
case 'in_progress':
|
|
||||||
return (
|
|
||||||
<Tag icon={<ClockCircleOutlined />} color='processing'>
|
|
||||||
In Progress
|
|
||||||
</Tag>
|
|
||||||
)
|
|
||||||
case 'failed':
|
|
||||||
return (
|
|
||||||
<Tag icon={<XMarkCircleIcon />} color='error'>
|
|
||||||
Failed
|
|
||||||
</Tag>
|
|
||||||
)
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<Tag icon={<ClockCircleOutlined />} color='default'>
|
|
||||||
Unknown
|
|
||||||
</Tag>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const auditItemsColumns = [
|
|
||||||
{
|
|
||||||
title: 'Item ID',
|
|
||||||
dataIndex: '_id',
|
|
||||||
key: 'id',
|
|
||||||
width: 180,
|
|
||||||
render: (text) => (
|
|
||||||
<IdDisplay id={text} type={'stockaudititem'} longId={false} />
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
cancelEdit: () => {
|
||||||
title: 'Item Type',
|
objectFormRef?.current.cancelEditing()
|
||||||
dataIndex: 'itemType',
|
return true
|
||||||
key: 'itemType',
|
|
||||||
width: 120
|
|
||||||
},
|
},
|
||||||
{
|
finishEdit: () => {
|
||||||
title: 'Expected Weight',
|
objectFormRef?.current.handleUpdate()
|
||||||
dataIndex: 'expectedWeight',
|
return true
|
||||||
key: 'expectedWeight',
|
|
||||||
width: 120,
|
|
||||||
render: (weight) => `${weight.toFixed(2)}g`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Actual Weight',
|
|
||||||
dataIndex: 'actualWeight',
|
|
||||||
key: 'actualWeight',
|
|
||||||
width: 120,
|
|
||||||
render: (weight) => `${weight.toFixed(2)}g`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Difference',
|
|
||||||
key: 'difference',
|
|
||||||
width: 120,
|
|
||||||
render: (_, record) => {
|
|
||||||
const diff = record.actualWeight - record.expectedWeight
|
|
||||||
return (
|
|
||||||
<Text type={diff === 0 ? 'success' : 'danger'}>
|
|
||||||
{diff.toFixed(2)}g
|
|
||||||
</Text>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Status',
|
|
||||||
dataIndex: 'status',
|
|
||||||
key: 'status',
|
|
||||||
width: 120,
|
|
||||||
render: (status) => getStatusTag(status)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div style={{ textAlign: 'center', padding: '50px' }}>
|
|
||||||
<LoadingOutlined style={{ fontSize: 24 }} spin />
|
|
||||||
<Text style={{ marginLeft: 16 }}>Loading stock audit details...</Text>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!stockAudit) {
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{contextHolder}
|
<Flex
|
||||||
<Space direction='vertical' size='large' style={{ width: '100%' }}>
|
gap='large'
|
||||||
<Space>
|
vertical='true'
|
||||||
<Button
|
style={{
|
||||||
icon={<ArrowLeftOutlined />}
|
maxHeight: '100%',
|
||||||
onClick={() => navigate('/dashboard/inventory/stockaudits')}
|
minHeight: 0
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Back to Stock Audits
|
<Flex justify={'space-between'}>
|
||||||
</Button>
|
<Space size='middle'>
|
||||||
|
<Space size='small'>
|
||||||
|
<ObjectActions
|
||||||
|
type='stockAudit'
|
||||||
|
id={stockAuditId}
|
||||||
|
disabled={objectFormState.loading}
|
||||||
|
objectData={objectFormState.objectData}
|
||||||
|
/>
|
||||||
|
<ViewButton
|
||||||
|
disabled={objectFormState.loading}
|
||||||
|
items={[
|
||||||
|
{ key: 'info', label: 'Stock Audit Information' },
|
||||||
|
{ key: 'notes', label: 'Notes' },
|
||||||
|
{ key: 'auditLogs', label: 'Audit Logs' }
|
||||||
|
]}
|
||||||
|
visibleState={collapseState}
|
||||||
|
updateVisibleState={updateCollapseState}
|
||||||
|
/>
|
||||||
|
<DocumentPrintButton
|
||||||
|
type='stockAudit'
|
||||||
|
objectData={objectFormState.objectData}
|
||||||
|
disabled={objectFormState.loading}
|
||||||
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
|
<LockIndicator lock={objectFormState.lock} />
|
||||||
|
</Space>
|
||||||
|
<Space>
|
||||||
|
<EditButtons
|
||||||
|
isEditing={objectFormState.isEditing}
|
||||||
|
handleUpdate={() => {
|
||||||
|
actionHandlerRef.current.callAction('finishEdit')
|
||||||
|
}}
|
||||||
|
cancelEditing={() => {
|
||||||
|
actionHandlerRef.current.callAction('cancelEdit')
|
||||||
|
}}
|
||||||
|
startEditing={() => {
|
||||||
|
actionHandlerRef.current.callAction('edit')
|
||||||
|
}}
|
||||||
|
editLoading={objectFormState.editLoading}
|
||||||
|
formValid={objectFormState.formValid}
|
||||||
|
disabled={objectFormState.lock?.locked || objectFormState.loading}
|
||||||
|
loading={objectFormState.editLoading}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<ScrollBox>
|
||||||
|
<Flex vertical gap={'large'}>
|
||||||
|
<ActionHandler
|
||||||
|
actions={actions}
|
||||||
|
loading={objectFormState.loading}
|
||||||
|
ref={actionHandlerRef}
|
||||||
|
>
|
||||||
|
<InfoCollapse
|
||||||
|
title='Stock Audit Information'
|
||||||
|
icon={<InfoCircleIcon />}
|
||||||
|
active={collapseState.info}
|
||||||
|
onToggle={(expanded) => updateCollapseState('info', expanded)}
|
||||||
|
collapseKey='info'
|
||||||
|
>
|
||||||
|
<ObjectForm
|
||||||
|
id={stockAuditId}
|
||||||
|
type='stockAudit'
|
||||||
|
style={{ height: '100%' }}
|
||||||
|
ref={objectFormRef}
|
||||||
|
onStateChange={(state) => {
|
||||||
|
console.log('Got edit form state change', state)
|
||||||
|
setEditFormState((prev) => ({ ...prev, ...state }))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ loading, isEditing, objectData }) => {
|
||||||
|
return (
|
||||||
|
<ObjectInfo
|
||||||
|
loading={loading}
|
||||||
|
indicator={<LoadingOutlined />}
|
||||||
|
isEditing={isEditing}
|
||||||
|
type='stockAudit'
|
||||||
|
objectData={objectData}
|
||||||
|
visibleProperties={{
|
||||||
|
content: false,
|
||||||
|
testObject: false
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</ObjectForm>
|
||||||
|
</InfoCollapse>
|
||||||
|
</ActionHandler>
|
||||||
|
|
||||||
|
<InfoCollapse
|
||||||
|
title='Notes'
|
||||||
|
icon={<NoteIcon />}
|
||||||
|
active={collapseState.notes}
|
||||||
|
onToggle={(expanded) => updateCollapseState('notes', expanded)}
|
||||||
|
collapseKey='notes'
|
||||||
|
>
|
||||||
<Card>
|
<Card>
|
||||||
<Title level={4}>Stock Audit Details</Title>
|
<NotesPanel _id={stockAuditId} type='stockAudit' />
|
||||||
<Descriptions bordered>
|
|
||||||
<Descriptions.Item label='ID'>
|
|
||||||
<IdDisplay
|
|
||||||
id={stockAudit._id}
|
|
||||||
type={'stockaudit'}
|
|
||||||
longId={true}
|
|
||||||
/>
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label='Status'>
|
|
||||||
{getStatusTag(stockAudit.status)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label='Created At'>
|
|
||||||
<TimeDisplay dateTime={stockAudit.createdAt} showSince={true} />
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label='Updated At'>
|
|
||||||
<TimeDisplay dateTime={stockAudit.updatedAt} showSince={true} />
|
|
||||||
</Descriptions.Item>
|
|
||||||
</Descriptions>
|
|
||||||
</Card>
|
</Card>
|
||||||
|
</InfoCollapse>
|
||||||
|
|
||||||
<Card title='Audit Items'>
|
<InfoCollapse
|
||||||
<Table
|
title='Audit Logs'
|
||||||
dataSource={stockAudit.items || []}
|
icon={<AuditLogIcon />}
|
||||||
columns={auditItemsColumns}
|
active={collapseState.auditLogs}
|
||||||
rowKey='_id'
|
onToggle={(expanded) =>
|
||||||
pagination={false}
|
updateCollapseState('auditLogs', expanded)
|
||||||
scroll={{ y: 'calc(100vh - 500px)' }}
|
}
|
||||||
|
collapseKey='auditLogs'
|
||||||
|
>
|
||||||
|
{objectFormState.loading ? (
|
||||||
|
<InfoCollapsePlaceholder />
|
||||||
|
) : (
|
||||||
|
<ObjectTable
|
||||||
|
type='auditLog'
|
||||||
|
masterFilter={{ 'parent._id': stockAuditId }}
|
||||||
|
visibleColumns={{ _id: false, 'parent._id': false }}
|
||||||
/>
|
/>
|
||||||
</Card>
|
)}
|
||||||
</Space>
|
</InfoCollapse>
|
||||||
|
</Flex>
|
||||||
|
</ScrollBox>
|
||||||
|
</Flex>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
98
src/components/Dashboard/Management/CourierServices.jsx
Normal file
98
src/components/Dashboard/Management/CourierServices.jsx
Normal 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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
97
src/components/Dashboard/Management/Couriers.jsx
Normal file
97
src/components/Dashboard/Management/Couriers.jsx
Normal 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
|
||||||
216
src/components/Dashboard/Management/Couriers/CourierInfo.jsx
Normal file
216
src/components/Dashboard/Management/Couriers/CourierInfo.jsx
Normal 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
|
||||||
80
src/components/Dashboard/Management/Couriers/NewCourier.jsx
Normal file
80
src/components/Dashboard/Management/Couriers/NewCourier.jsx
Normal 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
|
||||||
@ -20,6 +20,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
|||||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||||
|
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||||
|
|
||||||
const log = loglevel.getLogger('DocumentJobInfo')
|
const log = loglevel.getLogger('DocumentJobInfo')
|
||||||
log.setLevel(config.logLevel)
|
log.setLevel(config.logLevel)
|
||||||
@ -120,7 +121,7 @@ const DocumentJobInfo = () => {
|
|||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
</Flex>
|
</Flex>
|
||||||
<div style={{ height: '100%', overflow: 'auto' }}>
|
<ScrollBox>
|
||||||
<Flex vertical gap={'large'}>
|
<Flex vertical gap={'large'}>
|
||||||
<ActionHandler
|
<ActionHandler
|
||||||
actions={actions}
|
actions={actions}
|
||||||
@ -186,7 +187,7 @@ const DocumentJobInfo = () => {
|
|||||||
)}
|
)}
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</ScrollBox>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
|||||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||||
|
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||||
|
|
||||||
const log = loglevel.getLogger('DocumentPrinterInfo')
|
const log = loglevel.getLogger('DocumentPrinterInfo')
|
||||||
log.setLevel(config.logLevel)
|
log.setLevel(config.logLevel)
|
||||||
@ -129,7 +130,7 @@ const DocumentPrinterInfo = () => {
|
|||||||
</Space>
|
</Space>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<div style={{ height: '100%', overflowY: 'scroll' }}>
|
<ScrollBox>
|
||||||
<Flex vertical gap={'large'}>
|
<Flex vertical gap={'large'}>
|
||||||
<ActionHandler
|
<ActionHandler
|
||||||
actions={actions}
|
actions={actions}
|
||||||
@ -199,7 +200,7 @@ const DocumentPrinterInfo = () => {
|
|||||||
)}
|
)}
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</ScrollBox>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
|||||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||||
|
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||||
|
|
||||||
const log = loglevel.getLogger('DocumentSizeInfo')
|
const log = loglevel.getLogger('DocumentSizeInfo')
|
||||||
log.setLevel(config.logLevel)
|
log.setLevel(config.logLevel)
|
||||||
@ -120,7 +121,7 @@ const DocumentSizeInfo = () => {
|
|||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
</Flex>
|
</Flex>
|
||||||
<div style={{ height: '100%', overflow: 'auto' }}>
|
<ScrollBox>
|
||||||
<Flex vertical gap={'large'}>
|
<Flex vertical gap={'large'}>
|
||||||
<ActionHandler
|
<ActionHandler
|
||||||
actions={actions}
|
actions={actions}
|
||||||
@ -186,7 +187,7 @@ const DocumentSizeInfo = () => {
|
|||||||
)}
|
)}
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</ScrollBox>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,19 +1,9 @@
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { useState } from 'react'
|
|
||||||
import { useMediaQuery } from 'react-responsive'
|
|
||||||
import { Typography, Flex, Steps, Divider } from 'antd'
|
|
||||||
|
|
||||||
import ObjectInfo from '../../common/ObjectInfo'
|
import ObjectInfo from '../../common/ObjectInfo'
|
||||||
import NewObjectForm from '../../common/NewObjectForm'
|
import NewObjectForm from '../../common/NewObjectForm'
|
||||||
import NewObjectButtons from '../../common/NewObjectButtons'
|
import WizardView from '../../common/WizardView'
|
||||||
|
|
||||||
const { Title } = Typography
|
|
||||||
|
|
||||||
const NewDocumentSize = ({ onOk }) => {
|
const NewDocumentSize = ({ onOk }) => {
|
||||||
const [currentStep, setCurrentStep] = useState(0)
|
|
||||||
|
|
||||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NewObjectForm type={'documentSize'}>
|
<NewObjectForm type={'documentSize'}>
|
||||||
{({ handleSubmit, submitLoading, objectData, formValid }) => {
|
{({ handleSubmit, submitLoading, objectData, formValid }) => {
|
||||||
@ -52,43 +42,16 @@ const NewDocumentSize = ({ onOk }) => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
return (
|
return (
|
||||||
<Flex gap='middle'>
|
<WizardView
|
||||||
{!isMobile && (
|
steps={steps}
|
||||||
<div style={{ minWidth: '160px' }}>
|
loading={submitLoading}
|
||||||
<Steps
|
formValid={formValid}
|
||||||
current={currentStep}
|
title='New Document Size'
|
||||||
items={steps}
|
|
||||||
direction='vertical'
|
|
||||||
style={{ width: 'fit-content' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isMobile && (
|
|
||||||
<Divider type='vertical' style={{ height: 'unset' }} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Flex vertical gap='middle' style={{ flexGrow: 1 }}>
|
|
||||||
<Title level={2} style={{ margin: 0 }}>
|
|
||||||
New Document Size
|
|
||||||
</Title>
|
|
||||||
<div style={{ minHeight: '260px', marginBottom: 8 }}>
|
|
||||||
{steps[currentStep].content}
|
|
||||||
</div>
|
|
||||||
<NewObjectButtons
|
|
||||||
currentStep={currentStep}
|
|
||||||
totalSteps={steps.length}
|
|
||||||
onPrevious={() => setCurrentStep((prev) => prev - 1)}
|
|
||||||
onNext={() => setCurrentStep((prev) => prev + 1)}
|
|
||||||
onSubmit={() => {
|
onSubmit={() => {
|
||||||
handleSubmit()
|
handleSubmit()
|
||||||
onOk()
|
onOk()
|
||||||
}}
|
}}
|
||||||
formValid={formValid}
|
|
||||||
submitLoading={submitLoading}
|
|
||||||
/>
|
/>
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</NewObjectForm>
|
</NewObjectForm>
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import EditButtons from '../../common/EditButtons.jsx'
|
|||||||
import LockIndicator from '../../common/LockIndicator.jsx'
|
import LockIndicator from '../../common/LockIndicator.jsx'
|
||||||
import ActionHandler from '../../common/ActionHandler.jsx'
|
import ActionHandler from '../../common/ActionHandler.jsx'
|
||||||
import ObjectActions from '../../common/ObjectActions.jsx'
|
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||||
|
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||||
|
|
||||||
import TemplateEditor from '../../common/TemplateEditor.jsx'
|
import TemplateEditor from '../../common/TemplateEditor.jsx'
|
||||||
|
|
||||||
@ -122,7 +123,7 @@ const DocumentTemplateDesign = () => {
|
|||||||
</Space>
|
</Space>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<div style={{ height: '100%', overflowY: 'scroll' }}>
|
<ScrollBox>
|
||||||
<Flex vertical gap={'large'}>
|
<Flex vertical gap={'large'}>
|
||||||
<ActionHandler
|
<ActionHandler
|
||||||
actions={actions}
|
actions={actions}
|
||||||
@ -164,7 +165,7 @@ const DocumentTemplateDesign = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</ScrollBox>
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
|||||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||||
|
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||||
|
|
||||||
const log = loglevel.getLogger('DocumentTemplateInfo')
|
const log = loglevel.getLogger('DocumentTemplateInfo')
|
||||||
log.setLevel(config.logLevel)
|
log.setLevel(config.logLevel)
|
||||||
@ -126,7 +127,7 @@ const DocumentTemplateInfo = () => {
|
|||||||
</Space>
|
</Space>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<div style={{ height: '100%', overflowY: 'scroll' }}>
|
<ScrollBox>
|
||||||
<Flex vertical gap={'large'}>
|
<Flex vertical gap={'large'}>
|
||||||
<ActionHandler
|
<ActionHandler
|
||||||
actions={actions}
|
actions={actions}
|
||||||
@ -201,7 +202,7 @@ const DocumentTemplateInfo = () => {
|
|||||||
)}
|
)}
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</ScrollBox>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import ObjectTable from '../../common/ObjectTable.jsx'
|
|||||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||||
import FilamentIcon from '../../../Icons/FilamentIcon.jsx'
|
import FilamentIcon from '../../../Icons/FilamentIcon.jsx'
|
||||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||||
|
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||||
|
|
||||||
const log = loglevel.getLogger('FilamentInfo')
|
const log = loglevel.getLogger('FilamentInfo')
|
||||||
log.setLevel(config.logLevel)
|
log.setLevel(config.logLevel)
|
||||||
@ -126,7 +127,7 @@ const FilamentInfo = () => {
|
|||||||
</Space>
|
</Space>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<div style={{ height: '100%', overflowY: 'scroll' }}>
|
<ScrollBox>
|
||||||
<Flex vertical gap={'large'}>
|
<Flex vertical gap={'large'}>
|
||||||
<ActionHandler
|
<ActionHandler
|
||||||
actions={actions}
|
actions={actions}
|
||||||
@ -218,7 +219,7 @@ const FilamentInfo = () => {
|
|||||||
)}
|
)}
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</ScrollBox>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import FileIcon from '../../../Icons/FileIcon.jsx'
|
|||||||
import FilePreview from '../../common/FilePreview.jsx'
|
import FilePreview from '../../common/FilePreview.jsx'
|
||||||
import MissingPlaceholder from '../../common/MissingPlaceholder.jsx'
|
import MissingPlaceholder from '../../common/MissingPlaceholder.jsx'
|
||||||
import { ApiServerContext } from '../../context/ApiServerContext.jsx'
|
import { ApiServerContext } from '../../context/ApiServerContext.jsx'
|
||||||
|
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||||
|
|
||||||
const log = loglevel.getLogger('FileInfo')
|
const log = loglevel.getLogger('FileInfo')
|
||||||
log.setLevel(config.logLevel)
|
log.setLevel(config.logLevel)
|
||||||
@ -130,7 +131,7 @@ const FileInfo = () => {
|
|||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
</Flex>
|
</Flex>
|
||||||
<div style={{ height: '100%', overflow: 'auto' }}>
|
<ScrollBox>
|
||||||
<Flex vertical gap={'large'}>
|
<Flex vertical gap={'large'}>
|
||||||
<ActionHandler
|
<ActionHandler
|
||||||
actions={actions}
|
actions={actions}
|
||||||
@ -214,7 +215,7 @@ const FileInfo = () => {
|
|||||||
)}
|
)}
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</ScrollBox>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import HostOTP from './HostOtp.jsx'
|
|||||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||||
import PrinterIcon from '../../../Icons/PrinterIcon.jsx'
|
import PrinterIcon from '../../../Icons/PrinterIcon.jsx'
|
||||||
import DocumentPrinterIcon from '../../../Icons/DocumentPrinterIcon.jsx'
|
import DocumentPrinterIcon from '../../../Icons/DocumentPrinterIcon.jsx'
|
||||||
|
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||||
|
|
||||||
const log = loglevel.getLogger('HostInfo')
|
const log = loglevel.getLogger('HostInfo')
|
||||||
log.setLevel(config.logLevel)
|
log.setLevel(config.logLevel)
|
||||||
@ -132,7 +133,7 @@ const HostInfo = () => {
|
|||||||
</Space>
|
</Space>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<div style={{ height: '100%', overflowY: 'scroll' }}>
|
<ScrollBox>
|
||||||
<Flex vertical gap={'large'}>
|
<Flex vertical gap={'large'}>
|
||||||
<ActionHandler
|
<ActionHandler
|
||||||
actions={actions}
|
actions={actions}
|
||||||
@ -245,7 +246,7 @@ const HostInfo = () => {
|
|||||||
)}
|
)}
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</ScrollBox>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@ -17,6 +17,8 @@ import DocumentIcon from '../../Icons/DocumentIcon'
|
|||||||
import DocumentSizeIcon from '../../Icons/DocumentSizeIcon'
|
import DocumentSizeIcon from '../../Icons/DocumentSizeIcon'
|
||||||
import DocumentJobIcon from '../../Icons/DocumentJobIcon'
|
import DocumentJobIcon from '../../Icons/DocumentJobIcon'
|
||||||
import FileIcon from '../../Icons/FileIcon'
|
import FileIcon from '../../Icons/FileIcon'
|
||||||
|
import CourierIcon from '../../Icons/CourierIcon'
|
||||||
|
import CourierServiceIcon from '../../Icons/CourierServiceIcon'
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
@ -50,6 +52,19 @@ const items = [
|
|||||||
path: '/dashboard/management/materials'
|
path: '/dashboard/management/materials'
|
||||||
},
|
},
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
|
{
|
||||||
|
key: 'couriers',
|
||||||
|
icon: <CourierIcon />,
|
||||||
|
label: 'Couriers',
|
||||||
|
path: '/dashboard/management/couriers'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'courierServices',
|
||||||
|
icon: <CourierServiceIcon />,
|
||||||
|
label: 'Courier Services',
|
||||||
|
path: '/dashboard/management/courierservices'
|
||||||
|
},
|
||||||
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
key: 'noteTypes',
|
key: 'noteTypes',
|
||||||
icon: <NoteTypeIcon />,
|
icon: <NoteTypeIcon />,
|
||||||
@ -139,6 +154,8 @@ const routeKeyMap = {
|
|||||||
'/dashboard/management/users': 'users',
|
'/dashboard/management/users': 'users',
|
||||||
'/dashboard/management/products': 'products',
|
'/dashboard/management/products': 'products',
|
||||||
'/dashboard/management/vendors': 'vendors',
|
'/dashboard/management/vendors': 'vendors',
|
||||||
|
'/dashboard/management/couriers': 'couriers',
|
||||||
|
'/dashboard/management/courierservices': 'courierServices',
|
||||||
'/dashboard/management/materials': 'materials',
|
'/dashboard/management/materials': 'materials',
|
||||||
'/dashboard/management/notetypes': 'noteTypes',
|
'/dashboard/management/notetypes': 'noteTypes',
|
||||||
'/dashboard/management/settings': 'settings',
|
'/dashboard/management/settings': 'settings',
|
||||||
@ -154,9 +171,15 @@ const routeKeyMap = {
|
|||||||
const ManagementSidebar = (props) => {
|
const ManagementSidebar = (props) => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const selectedKey = (() => {
|
const selectedKey = (() => {
|
||||||
const match = Object.keys(routeKeyMap).find((path) =>
|
const match = Object.keys(routeKeyMap).find((path) => {
|
||||||
location.pathname.startsWith(path)
|
const pathSplit = path.split('/')
|
||||||
)
|
const locationPathSplit = location.pathname.split('/')
|
||||||
|
if (pathSplit.length > locationPathSplit.length) return false
|
||||||
|
for (let i = 0; i < pathSplit.length; i++) {
|
||||||
|
if (pathSplit[i] !== locationPathSplit[i]) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
return match ? routeKeyMap[match] : 'filaments'
|
return match ? routeKeyMap[match] : 'filaments'
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
|||||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||||
|
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||||
|
|
||||||
const NoteTypeInfo = () => {
|
const NoteTypeInfo = () => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
@ -109,7 +110,7 @@ const NoteTypeInfo = () => {
|
|||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
</Flex>
|
</Flex>
|
||||||
<div style={{ height: '100%', overflow: 'auto' }}>
|
<ScrollBox>
|
||||||
<Flex vertical gap={'large'}>
|
<Flex vertical gap={'large'}>
|
||||||
<ActionHandler
|
<ActionHandler
|
||||||
actions={actions}
|
actions={actions}
|
||||||
@ -164,7 +165,7 @@ const NoteTypeInfo = () => {
|
|||||||
)}
|
)}
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</ScrollBox>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
|||||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||||
|
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||||
|
|
||||||
const log = loglevel.getLogger('NoteInfo')
|
const log = loglevel.getLogger('NoteInfo')
|
||||||
log.setLevel(config.logLevel)
|
log.setLevel(config.logLevel)
|
||||||
@ -118,7 +119,7 @@ const NoteInfo = () => {
|
|||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
</Flex>
|
</Flex>
|
||||||
<div style={{ height: '100%', overflow: 'auto' }}>
|
<ScrollBox>
|
||||||
<Flex vertical gap={'large'}>
|
<Flex vertical gap={'large'}>
|
||||||
<ActionHandler
|
<ActionHandler
|
||||||
actions={actions}
|
actions={actions}
|
||||||
@ -183,7 +184,7 @@ const NoteInfo = () => {
|
|||||||
)}
|
)}
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</ScrollBox>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
|||||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||||
|
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||||
|
|
||||||
const PartInfo = () => {
|
const PartInfo = () => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
@ -109,7 +110,7 @@ const PartInfo = () => {
|
|||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
</Flex>
|
</Flex>
|
||||||
<div style={{ height: '100%', overflow: 'auto' }}>
|
<ScrollBox>
|
||||||
<Flex vertical gap={'large'}>
|
<Flex vertical gap={'large'}>
|
||||||
<ActionHandler
|
<ActionHandler
|
||||||
actions={actions}
|
actions={actions}
|
||||||
@ -174,7 +175,7 @@ const PartInfo = () => {
|
|||||||
)}
|
)}
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</ScrollBox>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -12,12 +12,15 @@ import LockIndicator from '../../common/LockIndicator.jsx'
|
|||||||
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
||||||
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
||||||
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||||
import ProductIcon from '../../../Icons/ProductIcon.jsx'
|
|
||||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||||
import ActionHandler from '../../common/ActionHandler.jsx'
|
import ActionHandler from '../../common/ActionHandler.jsx'
|
||||||
import ObjectActions from '../../common/ObjectActions.jsx'
|
import ObjectActions from '../../common/ObjectActions.jsx'
|
||||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||||
|
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||||
|
import ObjectProperty from '../../common/ObjectProperty.jsx'
|
||||||
|
import { getModelProperty } from '../../../../database/ObjectModels.js'
|
||||||
|
import PartIcon from '../../../Icons/PartIcon.jsx'
|
||||||
|
|
||||||
const ProductInfo = () => {
|
const ProductInfo = () => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
@ -111,19 +114,12 @@ const ProductInfo = () => {
|
|||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
</Flex>
|
</Flex>
|
||||||
<div style={{ height: '100%', overflow: 'auto' }}>
|
<ScrollBox>
|
||||||
<Flex vertical gap={'large'}>
|
<Flex vertical gap={'large'}>
|
||||||
<ActionHandler
|
<ActionHandler
|
||||||
actions={actions}
|
actions={actions}
|
||||||
loading={objectFormState.loading}
|
loading={objectFormState.loading}
|
||||||
ref={actionHandlerRef}
|
ref={actionHandlerRef}
|
||||||
>
|
|
||||||
<InfoCollapse
|
|
||||||
title='Product Information'
|
|
||||||
icon={<InfoCircleIcon />}
|
|
||||||
active={collapseState.info}
|
|
||||||
onToggle={(expanded) => updateCollapseState('info', expanded)}
|
|
||||||
collapseKey='info'
|
|
||||||
>
|
>
|
||||||
<ObjectForm
|
<ObjectForm
|
||||||
id={productId}
|
id={productId}
|
||||||
@ -135,32 +131,47 @@ const ProductInfo = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({ loading, isEditing, objectData }) => (
|
{({ loading, isEditing, objectData }) => (
|
||||||
|
<Flex vertical gap={'large'}>
|
||||||
|
<InfoCollapse
|
||||||
|
title='Product Information'
|
||||||
|
icon={<InfoCircleIcon />}
|
||||||
|
active={collapseState.info}
|
||||||
|
onToggle={(expanded) =>
|
||||||
|
updateCollapseState('info', expanded)
|
||||||
|
}
|
||||||
|
collapseKey='info'
|
||||||
|
>
|
||||||
<ObjectInfo
|
<ObjectInfo
|
||||||
loading={loading}
|
loading={loading}
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
type='product'
|
type='product'
|
||||||
objectData={objectData}
|
objectData={objectData}
|
||||||
|
visibleProperties={{
|
||||||
|
parts: false
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</ObjectForm>
|
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
</ActionHandler>
|
|
||||||
<InfoCollapse
|
<InfoCollapse
|
||||||
title='Product Parts'
|
title='Product Parts'
|
||||||
icon={<ProductIcon />}
|
icon={<PartIcon />}
|
||||||
active={collapseState.parts}
|
active={collapseState.parts}
|
||||||
onToggle={(expanded) => updateCollapseState('parts', expanded)}
|
onToggle={(expanded) =>
|
||||||
|
updateCollapseState('parts', expanded)
|
||||||
|
}
|
||||||
collapseKey='parts'
|
collapseKey='parts'
|
||||||
>
|
>
|
||||||
<ObjectTable
|
<ObjectProperty
|
||||||
type='part'
|
{...getModelProperty('product', 'parts')}
|
||||||
visibleColumns={{
|
isEditing={isEditing}
|
||||||
product: false,
|
objectData={objectData}
|
||||||
'product._id': false
|
loading={loading}
|
||||||
}}
|
|
||||||
masterFilter={{ 'product._id': productId }}
|
|
||||||
/>
|
/>
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</ObjectForm>
|
||||||
|
</ActionHandler>
|
||||||
|
|
||||||
<InfoCollapse
|
<InfoCollapse
|
||||||
title='Notes'
|
title='Notes'
|
||||||
icon={<NoteIcon />}
|
icon={<NoteIcon />}
|
||||||
@ -192,7 +203,7 @@ const ProductInfo = () => {
|
|||||||
)}
|
)}
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</ScrollBox>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
|||||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||||
|
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||||
|
|
||||||
const UserInfo = () => {
|
const UserInfo = () => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
@ -110,7 +111,7 @@ const UserInfo = () => {
|
|||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
</Flex>
|
</Flex>
|
||||||
<div style={{ height: '100%', overflow: 'auto' }}>
|
<ScrollBox>
|
||||||
<Flex vertical gap={'large'}>
|
<Flex vertical gap={'large'}>
|
||||||
<ActionHandler
|
<ActionHandler
|
||||||
actions={actions}
|
actions={actions}
|
||||||
@ -176,7 +177,7 @@ const UserInfo = () => {
|
|||||||
)}
|
)}
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</ScrollBox>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
|||||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||||
|
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||||
|
|
||||||
const log = loglevel.getLogger('VendorInfo')
|
const log = loglevel.getLogger('VendorInfo')
|
||||||
log.setLevel(config.logLevel)
|
log.setLevel(config.logLevel)
|
||||||
@ -118,7 +119,7 @@ const VendorInfo = () => {
|
|||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
</Flex>
|
</Flex>
|
||||||
<div style={{ height: '100%', overflow: 'auto' }}>
|
<ScrollBox>
|
||||||
<Flex vertical gap={'large'}>
|
<Flex vertical gap={'large'}>
|
||||||
<ActionHandler
|
<ActionHandler
|
||||||
actions={actions}
|
actions={actions}
|
||||||
@ -183,7 +184,7 @@ const VendorInfo = () => {
|
|||||||
)}
|
)}
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</ScrollBox>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import useCollapseState from '../../hooks/useCollapseState.js'
|
|||||||
import NotesPanel from '../../common/NotesPanel.jsx'
|
import NotesPanel from '../../common/NotesPanel.jsx'
|
||||||
import InfoCollapse from '../../common/InfoCollapse.jsx'
|
import InfoCollapse from '../../common/InfoCollapse.jsx'
|
||||||
import ObjectInfo from '../../common/ObjectInfo.jsx'
|
import ObjectInfo from '../../common/ObjectInfo.jsx'
|
||||||
|
import ObjectProperty from '../../common/ObjectProperty.jsx'
|
||||||
import ViewButton from '../../common/ViewButton.jsx'
|
import ViewButton from '../../common/ViewButton.jsx'
|
||||||
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
||||||
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
||||||
@ -23,6 +24,9 @@ import EyeIcon from '../../../Icons/EyeIcon.jsx'
|
|||||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||||
import MissingPlaceholder from '../../common/MissingPlaceholder.jsx'
|
import MissingPlaceholder from '../../common/MissingPlaceholder.jsx'
|
||||||
import FilePreview from '../../common/FilePreview.jsx'
|
import FilePreview from '../../common/FilePreview.jsx'
|
||||||
|
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||||
|
import { getModelProperty } from '../../../../database/ObjectModels.js'
|
||||||
|
import PartIcon from '../../../Icons/PartIcon.jsx'
|
||||||
|
|
||||||
const log = loglevel.getLogger('GCodeFileInfo')
|
const log = loglevel.getLogger('GCodeFileInfo')
|
||||||
log.setLevel(config.logLevel)
|
log.setLevel(config.logLevel)
|
||||||
@ -36,7 +40,8 @@ const GCodeFileInfo = () => {
|
|||||||
'GCodeFileInfo',
|
'GCodeFileInfo',
|
||||||
{
|
{
|
||||||
info: true,
|
info: true,
|
||||||
stocks: true,
|
parts: true,
|
||||||
|
preview: true,
|
||||||
notes: true,
|
notes: true,
|
||||||
auditLogs: true
|
auditLogs: true
|
||||||
}
|
}
|
||||||
@ -93,6 +98,7 @@ const GCodeFileInfo = () => {
|
|||||||
disabled={objectFormState.loading}
|
disabled={objectFormState.loading}
|
||||||
items={[
|
items={[
|
||||||
{ key: 'info', label: 'GCode File Information' },
|
{ key: 'info', label: 'GCode File Information' },
|
||||||
|
{ key: 'parts', label: 'Parts' },
|
||||||
{ key: 'preview', label: 'GCode File Preview' },
|
{ key: 'preview', label: 'GCode File Preview' },
|
||||||
{ key: 'notes', label: 'Notes' },
|
{ key: 'notes', label: 'Notes' },
|
||||||
{ key: 'auditLogs', label: 'Audit Logs' }
|
{ key: 'auditLogs', label: 'Audit Logs' }
|
||||||
@ -128,7 +134,7 @@ const GCodeFileInfo = () => {
|
|||||||
</Space>
|
</Space>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<div style={{ height: '100%', overflowY: 'scroll' }}>
|
<ScrollBox>
|
||||||
<Flex vertical gap={'large'}>
|
<Flex vertical gap={'large'}>
|
||||||
<ActionHandler
|
<ActionHandler
|
||||||
actions={actions}
|
actions={actions}
|
||||||
@ -163,7 +169,25 @@ const GCodeFileInfo = () => {
|
|||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
type='gcodeFile'
|
type='gcodeFile'
|
||||||
objectData={objectData}
|
objectData={objectData}
|
||||||
visibleProperties={{}}
|
visibleProperties={{
|
||||||
|
parts: false
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</InfoCollapse>
|
||||||
|
<InfoCollapse
|
||||||
|
title='Parts'
|
||||||
|
icon={<PartIcon />}
|
||||||
|
active={collapseState.parts}
|
||||||
|
onToggle={(expanded) =>
|
||||||
|
updateCollapseState('parts', expanded)
|
||||||
|
}
|
||||||
|
collapseKey='parts'
|
||||||
|
>
|
||||||
|
<ObjectProperty
|
||||||
|
{...getModelProperty('gcodeFile', 'parts')}
|
||||||
|
isEditing={isEditing}
|
||||||
|
objectData={objectData}
|
||||||
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
<InfoCollapse
|
<InfoCollapse
|
||||||
@ -224,7 +248,7 @@ const GCodeFileInfo = () => {
|
|||||||
)}
|
)}
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</ScrollBox>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
|||||||
import JobIcon from '../../../Icons/JobIcon.jsx'
|
import JobIcon from '../../../Icons/JobIcon.jsx'
|
||||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||||
import DeployJob from './DeployJob.jsx'
|
import DeployJob from './DeployJob.jsx'
|
||||||
|
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||||
|
|
||||||
const log = loglevel.getLogger('JobInfo')
|
const log = loglevel.getLogger('JobInfo')
|
||||||
log.setLevel(config.logLevel)
|
log.setLevel(config.logLevel)
|
||||||
@ -132,7 +133,7 @@ const JobInfo = () => {
|
|||||||
</Space>
|
</Space>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<div style={{ height: '100%', overflowY: 'scroll' }}>
|
<ScrollBox>
|
||||||
<Flex vertical gap={'large'}>
|
<Flex vertical gap={'large'}>
|
||||||
<ActionHandler
|
<ActionHandler
|
||||||
actions={actions}
|
actions={actions}
|
||||||
@ -214,7 +215,7 @@ const JobInfo = () => {
|
|||||||
)}
|
)}
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</ScrollBox>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Modal
|
<Modal
|
||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import { ApiServerContext } from '../../context/ApiServerContext.jsx'
|
|||||||
|
|
||||||
import LoadFilamentStock from '../../Inventory/FilamentStocks/LoadFilamentStock.jsx'
|
import LoadFilamentStock from '../../Inventory/FilamentStocks/LoadFilamentStock.jsx'
|
||||||
import UnloadFilamentStock from '../../Inventory/FilamentStocks/UnloadFilamentStock.jsx'
|
import UnloadFilamentStock from '../../Inventory/FilamentStocks/UnloadFilamentStock.jsx'
|
||||||
|
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||||
|
|
||||||
const log = loglevel.getLogger('ControlPrinter')
|
const log = loglevel.getLogger('ControlPrinter')
|
||||||
log.setLevel(config.logLevel)
|
log.setLevel(config.logLevel)
|
||||||
@ -240,7 +241,10 @@ const ControlPrinter = () => {
|
|||||||
visibleState={collapseState}
|
visibleState={collapseState}
|
||||||
updateVisibleState={updateCollapseState}
|
updateVisibleState={updateCollapseState}
|
||||||
/>
|
/>
|
||||||
<AlertsDisplay alerts={objectFormState.objectData?.alerts} />
|
<AlertsDisplay
|
||||||
|
alerts={objectFormState.objectData?.alerts}
|
||||||
|
printerId={printerId}
|
||||||
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
</Space>
|
</Space>
|
||||||
<Space>
|
<Space>
|
||||||
@ -263,7 +267,7 @@ const ControlPrinter = () => {
|
|||||||
</Space>
|
</Space>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<div style={{ height: '100%', overflowY: 'scroll' }}>
|
<ScrollBox>
|
||||||
<Flex vertical gap={'large'}>
|
<Flex vertical gap={'large'}>
|
||||||
<ActionHandler
|
<ActionHandler
|
||||||
actions={actions}
|
actions={actions}
|
||||||
@ -320,7 +324,9 @@ const ControlPrinter = () => {
|
|||||||
currentJob: false,
|
currentJob: false,
|
||||||
'currentJob._id': false,
|
'currentJob._id': false,
|
||||||
currentSubJob: false,
|
currentSubJob: false,
|
||||||
'currentSubJob._id': false
|
'currentSubJob._id': false,
|
||||||
|
createdAt: false,
|
||||||
|
updatedAt: false
|
||||||
}}
|
}}
|
||||||
objectData={printerObjectData}
|
objectData={printerObjectData}
|
||||||
type='printer'
|
type='printer'
|
||||||
@ -486,7 +492,7 @@ const ControlPrinter = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</ScrollBox>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Modal
|
<Modal
|
||||||
open={loadFilamentStockOpen}
|
open={loadFilamentStockOpen}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import ObjectActions from '../../common/ObjectActions.jsx'
|
|||||||
import ObjectTable from '../../common/ObjectTable.jsx'
|
import ObjectTable from '../../common/ObjectTable.jsx'
|
||||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||||
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
|
||||||
|
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||||
|
|
||||||
const log = loglevel.getLogger('PrinterInfo')
|
const log = loglevel.getLogger('PrinterInfo')
|
||||||
log.setLevel(config.logLevel)
|
log.setLevel(config.logLevel)
|
||||||
@ -122,7 +123,7 @@ const PrinterInfo = () => {
|
|||||||
</Space>
|
</Space>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<div style={{ height: '100%', overflowY: 'scroll' }}>
|
<ScrollBox>
|
||||||
<Flex vertical gap={'large'}>
|
<Flex vertical gap={'large'}>
|
||||||
<ActionHandler
|
<ActionHandler
|
||||||
actions={actions}
|
actions={actions}
|
||||||
@ -202,7 +203,7 @@ const PrinterInfo = () => {
|
|||||||
)}
|
)}
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</ScrollBox>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -51,9 +51,15 @@ const routeKeyMap = {
|
|||||||
const ProductionSidebar = (props) => {
|
const ProductionSidebar = (props) => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const selectedKey = (() => {
|
const selectedKey = (() => {
|
||||||
const match = Object.keys(routeKeyMap).find((path) =>
|
const match = Object.keys(routeKeyMap).find((path) => {
|
||||||
location.pathname.startsWith(path)
|
const pathSplit = path.split('/')
|
||||||
)
|
const locationPathSplit = location.pathname.split('/')
|
||||||
|
if (pathSplit.length > locationPathSplit.length) return false
|
||||||
|
for (let i = 0; i < pathSplit.length; i++) {
|
||||||
|
if (pathSplit[i] !== locationPathSplit[i]) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
return match ? routeKeyMap[match] : 'overview'
|
return match ? routeKeyMap[match] : 'overview'
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
|||||||
208
src/components/Dashboard/Production/SubJobs/SubJobInfo.jsx
Normal file
208
src/components/Dashboard/Production/SubJobs/SubJobInfo.jsx
Normal 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
|
||||||
@ -1,15 +1,29 @@
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { Flex, Alert } from 'antd'
|
import { createElement } from 'react'
|
||||||
|
import { Flex, Alert, Button, Dropdown, Popover } from 'antd'
|
||||||
import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon'
|
import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon'
|
||||||
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
|
||||||
|
import { CaretDownOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
const AlertsDisplay = ({ alerts = [] }) => {
|
import { useMediaQuery } from 'react-responsive'
|
||||||
|
import { getModelByName } from '../../../database/ObjectModels'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
const AlertsDisplay = ({
|
||||||
|
alerts = [],
|
||||||
|
printerId,
|
||||||
|
showDismiss = true,
|
||||||
|
showActions = true
|
||||||
|
}) => {
|
||||||
|
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||||
const getAlertType = (type, priority) => {
|
const getAlertType = (type, priority) => {
|
||||||
if (type === 'error' || priority === '9') return 'error'
|
if (type === 'error' || priority === '9') return 'error'
|
||||||
if (type === 'warning' || priority === '8') return 'warning'
|
if (type === 'warning' || priority === '8') return 'warning'
|
||||||
return 'info'
|
return 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const printerModel = getModelByName('printer')
|
||||||
|
const navigate = useNavigate()
|
||||||
const getAlertIcon = (type, priority) => {
|
const getAlertIcon = (type, priority) => {
|
||||||
if (type === 'error' || priority === '9') return <ExclamationOctagonIcon />
|
if (type === 'error' || priority === '9') return <ExclamationOctagonIcon />
|
||||||
if (type === 'warning' || priority === '8')
|
if (type === 'warning' || priority === '8')
|
||||||
@ -17,34 +31,185 @@ const AlertsDisplay = ({ alerts = [] }) => {
|
|||||||
return <InfoCircleIcon />
|
return <InfoCircleIcon />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recursively filter the printer model actions by a set of allowed action keys
|
||||||
|
const filterActionsByKeys = (actions, allowedKeys) => {
|
||||||
|
if (!Array.isArray(actions)) return []
|
||||||
|
|
||||||
|
const filtered = actions
|
||||||
|
.map((action) => {
|
||||||
|
if (action.type === 'divider') {
|
||||||
|
return { type: 'divider' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionKey = action.key || action.name
|
||||||
|
let children = []
|
||||||
|
|
||||||
|
if (Array.isArray(action.children)) {
|
||||||
|
children = filterActionsByKeys(action.children, allowedKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAllowed = actionKey && allowedKeys.has(actionKey)
|
||||||
|
|
||||||
|
if (!isAllowed && children.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...action,
|
||||||
|
children
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((action) => action !== null)
|
||||||
|
|
||||||
|
// Clean up dividers: remove leading/trailing and consecutive dividers
|
||||||
|
const cleaned = []
|
||||||
|
for (const action of filtered) {
|
||||||
|
if (action.type === 'divider') {
|
||||||
|
if (cleaned.length === 0) continue
|
||||||
|
if (cleaned[cleaned.length - 1].type === 'divider') continue
|
||||||
|
}
|
||||||
|
cleaned.push(action)
|
||||||
|
}
|
||||||
|
if (cleaned[cleaned.length - 1]?.type === 'divider') {
|
||||||
|
cleaned.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map filtered printer actions to AntD Dropdown menu items (including children)
|
||||||
|
const mapActionsToMenuItems = (actions) => {
|
||||||
|
if (!Array.isArray(actions)) return []
|
||||||
|
|
||||||
|
return actions.map((action) => {
|
||||||
|
if (action.type === 'divider') {
|
||||||
|
return { type: 'divider' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = {
|
||||||
|
key: action.key || action.name,
|
||||||
|
label: action.label,
|
||||||
|
icon: action.icon ? createElement(action.icon) : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(action.children) && action.children.length > 0) {
|
||||||
|
item.children = mapActionsToMenuItems(action.children)
|
||||||
|
}
|
||||||
|
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (alerts.length == 0) {
|
if (alerts.length == 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const alertElements = alerts.map((alert, index) => {
|
||||||
|
const printerActions = printerModel?.actions || []
|
||||||
|
|
||||||
|
const alertActionKeys = Array.isArray(alert?.actions)
|
||||||
|
? alert.actions
|
||||||
|
.map((action) =>
|
||||||
|
typeof action === 'string'
|
||||||
|
? action
|
||||||
|
: action?.key || action?.name || null
|
||||||
|
)
|
||||||
|
.filter((key) => key != null)
|
||||||
|
: []
|
||||||
|
|
||||||
|
const allowedKeys = new Set(alertActionKeys)
|
||||||
|
const filteredActions = filterActionsByKeys(printerActions, allowedKeys)
|
||||||
|
|
||||||
|
const findActionByKey = (actions, key) => {
|
||||||
|
if (!Array.isArray(actions)) return null
|
||||||
|
|
||||||
|
for (const action of actions) {
|
||||||
|
if (action.type === 'divider') continue
|
||||||
|
|
||||||
|
const actionKey = action.key || action.name
|
||||||
|
if (actionKey === key) {
|
||||||
|
return action
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(action.children) && action.children.length > 0) {
|
||||||
|
const found = findActionByKey(action.children, key)
|
||||||
|
if (found) return found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const menu = {
|
||||||
|
items: mapActionsToMenuItems(filteredActions),
|
||||||
|
onClick: ({ key }) => {
|
||||||
|
const action = findActionByKey(filteredActions, key)
|
||||||
|
|
||||||
|
if (action?.url) {
|
||||||
|
navigate(action.url(printerId))
|
||||||
|
} else {
|
||||||
|
console.warn('No action found for key:', key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex gap='small'>
|
|
||||||
{alerts.map((alert, index) => (
|
|
||||||
<Alert
|
<Alert
|
||||||
key={`${alert.createdAt}-${index}`}
|
key={`${alert.createdAt}-${index}-${alert._id}`}
|
||||||
message={alert.message}
|
message={alert.message}
|
||||||
style={{ padding: '4px 10px 4px 8px' }}
|
style={{ padding: '4px 10px 4px 8px' }}
|
||||||
type={getAlertType(alert.type, alert.priority)}
|
type={getAlertType(alert.type, alert.priority)}
|
||||||
icon={getAlertIcon(alert.type, alert.priority)}
|
icon={getAlertIcon(alert.type, alert.priority)}
|
||||||
showIcon
|
showIcon
|
||||||
|
closable={showDismiss && alert.canDismiss}
|
||||||
|
onClose={() => {
|
||||||
|
console.log('Closing alert:', alert._id)
|
||||||
|
}}
|
||||||
|
action={
|
||||||
|
showActions ? (
|
||||||
|
<Dropdown menu={menu} on>
|
||||||
|
<Button size='small' type='text' style={{ marginLeft: '5px' }}>
|
||||||
|
<CaretDownOutlined />
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
|
||||||
</Flex>
|
|
||||||
)
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
content={alertElements}
|
||||||
|
trigger='hover'
|
||||||
|
arrow={false}
|
||||||
|
placement='bottom'
|
||||||
|
classNames={{
|
||||||
|
root: 'printer-alerts-display-popover'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button>Alerts</Button>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Flex gap='small'>{alertElements}</Flex>
|
||||||
}
|
}
|
||||||
|
|
||||||
AlertsDisplay.propTypes = {
|
AlertsDisplay.propTypes = {
|
||||||
|
printerId: PropTypes.string.isRequired,
|
||||||
|
showActions: PropTypes.bool.isRequired,
|
||||||
|
showDismiss: PropTypes.bool.isRequired,
|
||||||
alerts: PropTypes.arrayOf(
|
alerts: PropTypes.arrayOf(
|
||||||
PropTypes.shape({
|
PropTypes.shape({
|
||||||
priority: PropTypes.string.isRequired,
|
canDismiss: PropTypes.bool.isRequired,
|
||||||
|
_id: PropTypes.string.isRequired,
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
createdAt: PropTypes.string.isRequired,
|
createdAt: PropTypes.string.isRequired,
|
||||||
updatedAt: PropTypes.string.isRequired,
|
updatedAt: PropTypes.string.isRequired,
|
||||||
message: PropTypes.string.isRequired
|
message: PropTypes.string,
|
||||||
|
actions: PropTypes.arrayOf(PropTypes.string)
|
||||||
})
|
})
|
||||||
).isRequired
|
).isRequired
|
||||||
}
|
}
|
||||||
|
|||||||
@ -153,7 +153,7 @@ const DashboardNavigation = () => {
|
|||||||
fontSize: '46px',
|
fontSize: '46px',
|
||||||
height: '16px',
|
height: '16px',
|
||||||
marginLeft: '15px',
|
marginLeft: '15px',
|
||||||
marginRight: '5px'
|
marginRight: '8px'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -313,7 +313,7 @@ const DashboardNavigation = () => {
|
|||||||
{isElectron ? (
|
{isElectron ? (
|
||||||
<Flex
|
<Flex
|
||||||
className='ant-menu-horizontal ant-menu-light electron-navigation-wrapper'
|
className='ant-menu-horizontal ant-menu-light electron-navigation-wrapper'
|
||||||
style={{ lineHeight: '40px', padding: '0 4px 0 4px' }}
|
style={{ lineHeight: '40px', padding: '0 2px 0 2px' }}
|
||||||
>
|
>
|
||||||
{navigationContents}
|
{navigationContents}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@ -36,7 +36,7 @@ const DashboardWindowButtons = () => {
|
|||||||
<Flex align='center'>
|
<Flex align='center'>
|
||||||
{platform == 'darwin' ? (
|
{platform == 'darwin' ? (
|
||||||
isFullScreen == false ? (
|
isFullScreen == false ? (
|
||||||
<div style={{ width: '65px' }} />
|
<div style={{ width: '80px' }} />
|
||||||
) : null
|
) : null
|
||||||
) : (
|
) : (
|
||||||
<div style={{ width: '95px' }}>
|
<div style={{ width: '95px' }}>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Upload, Button, Flex, Typography, Space } from 'antd'
|
import { Upload, Button, Flex, Typography, Space, Progress, Card } from 'antd'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { ApiServerContext } from '../context/ApiServerContext'
|
import { ApiServerContext } from '../context/ApiServerContext'
|
||||||
import UploadIcon from '../../Icons/UploadIcon'
|
import UploadIcon from '../../Icons/UploadIcon'
|
||||||
@ -6,6 +6,7 @@ import { useContext, useState, useEffect } from 'react'
|
|||||||
import ObjectSelect from './ObjectSelect'
|
import ObjectSelect from './ObjectSelect'
|
||||||
import FileList from './FileList'
|
import FileList from './FileList'
|
||||||
import PlusIcon from '../../Icons/PlusIcon'
|
import PlusIcon from '../../Icons/PlusIcon'
|
||||||
|
import { LoadingOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
const { Text } = Typography
|
const { Text } = Typography
|
||||||
|
|
||||||
@ -18,6 +19,8 @@ const FileUpload = ({
|
|||||||
showInfo
|
showInfo
|
||||||
}) => {
|
}) => {
|
||||||
const { uploadFile } = useContext(ApiServerContext)
|
const { uploadFile } = useContext(ApiServerContext)
|
||||||
|
const [uploading, setUploading] = useState(false)
|
||||||
|
const [uploadProgress, setUploadProgress] = useState(0)
|
||||||
|
|
||||||
// Track current files using useState
|
// Track current files using useState
|
||||||
const [currentFiles, setCurrentFiles] = useState(() => {
|
const [currentFiles, setCurrentFiles] = useState(() => {
|
||||||
@ -56,7 +59,11 @@ const FileUpload = ({
|
|||||||
|
|
||||||
const handleFileUpload = async (file) => {
|
const handleFileUpload = async (file) => {
|
||||||
try {
|
try {
|
||||||
const uploadedFile = await uploadFile(file)
|
setUploading(true)
|
||||||
|
const uploadedFile = await uploadFile(file, {}, (progress) => {
|
||||||
|
setUploadProgress(progress)
|
||||||
|
})
|
||||||
|
setUploading(false)
|
||||||
if (uploadedFile) {
|
if (uploadedFile) {
|
||||||
if (multiple) {
|
if (multiple) {
|
||||||
// For multiple files, add to existing array
|
// For multiple files, add to existing array
|
||||||
@ -95,7 +102,7 @@ const FileUpload = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex gap={'small'} vertical>
|
<Flex gap={'small'} vertical>
|
||||||
{hasNoItems ? (
|
{hasNoItems && uploading == false ? (
|
||||||
<Flex gap={'small'} align='center'>
|
<Flex gap={'small'} align='center'>
|
||||||
<Space.Compact style={{ flexGrow: 1 }}>
|
<Space.Compact style={{ flexGrow: 1 }}>
|
||||||
<ObjectSelect
|
<ObjectSelect
|
||||||
@ -123,6 +130,29 @@ const FileUpload = ({
|
|||||||
</Upload>
|
</Upload>
|
||||||
</Flex>
|
</Flex>
|
||||||
) : null}
|
) : null}
|
||||||
|
{uploading == true ? (
|
||||||
|
<Card styles={{ body: { padding: '10px 15px' } }}>
|
||||||
|
<Flex gap={'small'} align='center'>
|
||||||
|
<Text>Uploading...</Text>
|
||||||
|
{uploadProgress > 0 ? (
|
||||||
|
<>
|
||||||
|
{uploadProgress >= 0 && uploadProgress < 100 ? (
|
||||||
|
<>
|
||||||
|
<Progress
|
||||||
|
percent={uploadProgress}
|
||||||
|
showInfo={false}
|
||||||
|
style={{ width: '100px', flexGrow: 1 }}
|
||||||
|
status='active'
|
||||||
|
/>
|
||||||
|
<Text>{uploadProgress}%</Text>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{uploadProgress == 100 ? <LoadingOutlined /> : null}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
) : null}
|
||||||
<FileList
|
<FileList
|
||||||
files={currentFiles}
|
files={currentFiles}
|
||||||
multiple={multiple}
|
multiple={multiple}
|
||||||
|
|||||||
43
src/components/Dashboard/common/MiscId.jsx
Normal file
43
src/components/Dashboard/common/MiscId.jsx
Normal 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
|
||||||
@ -3,8 +3,20 @@ import { Form, message } from 'antd'
|
|||||||
import { ApiServerContext } from '../context/ApiServerContext'
|
import { ApiServerContext } from '../context/ApiServerContext'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import merge from 'lodash/merge'
|
import merge from 'lodash/merge'
|
||||||
|
import set from 'lodash/set'
|
||||||
import { getModelByName } from '../../../database/ObjectModels'
|
import { getModelByName } from '../../../database/ObjectModels'
|
||||||
|
|
||||||
|
const buildObjectFromEntries = (entries = []) => {
|
||||||
|
return entries.reduce((acc, entry) => {
|
||||||
|
const { namePath, value } = entry || {}
|
||||||
|
if (!Array.isArray(namePath) || value === undefined) {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
set(acc, namePath, value)
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NewObjectForm is a reusable form component for creating new objects.
|
* NewObjectForm is a reusable form component for creating new objects.
|
||||||
* It handles form validation, submission, and error handling logic.
|
* It handles form validation, submission, and error handling logic.
|
||||||
@ -30,18 +42,50 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
|
|||||||
const model = getModelByName(type)
|
const model = getModelByName(type)
|
||||||
|
|
||||||
// Function to calculate computed values from model properties
|
// Function to calculate computed values from model properties
|
||||||
const calculateComputedValues = useCallback((currentData, model) => {
|
const calculateComputedValues = useCallback(
|
||||||
if (!model || !model.properties) return {}
|
(currentData, modelDefinition) => {
|
||||||
|
if (!modelDefinition || !Array.isArray(modelDefinition.properties)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
const computedValues = {}
|
const normalizedPath = (name, parentPath = []) => {
|
||||||
|
if (Array.isArray(name)) {
|
||||||
|
return [...parentPath, ...name]
|
||||||
|
}
|
||||||
|
if (typeof name === 'number') {
|
||||||
|
return [...parentPath, name]
|
||||||
|
}
|
||||||
|
if (typeof name === 'string' && name.length > 0) {
|
||||||
|
return [...parentPath, ...name.split('.')]
|
||||||
|
}
|
||||||
|
return parentPath
|
||||||
|
}
|
||||||
|
|
||||||
|
const getValueAtPath = (dataSource, path) => {
|
||||||
|
if (!Array.isArray(path) || path.length === 0) {
|
||||||
|
return dataSource
|
||||||
|
}
|
||||||
|
return path.reduce((acc, key) => {
|
||||||
|
if (acc == null) return acc
|
||||||
|
return acc[key]
|
||||||
|
}, dataSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
const computedEntries = []
|
||||||
|
|
||||||
|
const processProperty = (property, scopeData, parentPath = []) => {
|
||||||
|
if (!property?.name) return
|
||||||
|
|
||||||
|
const propertyPath = normalizedPath(property.name, parentPath)
|
||||||
|
|
||||||
model.properties.forEach((property) => {
|
|
||||||
// Check if this property has a computed value function
|
|
||||||
if (property.value && typeof property.value === 'function') {
|
if (property.value && typeof property.value === 'function') {
|
||||||
try {
|
try {
|
||||||
const computedValue = property.value(currentData)
|
const computedValue = property.value(scopeData || {})
|
||||||
if (computedValue !== undefined) {
|
if (computedValue !== undefined) {
|
||||||
computedValues[property.name] = computedValue
|
computedEntries.push({
|
||||||
|
namePath: propertyPath,
|
||||||
|
value: computedValue
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(
|
console.warn(
|
||||||
@ -50,21 +94,50 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
Array.isArray(property.properties) &&
|
||||||
|
property.properties.length > 0
|
||||||
|
) {
|
||||||
|
if (property.type === 'objectChildren') {
|
||||||
|
const childValues = getValueAtPath(currentData, propertyPath)
|
||||||
|
if (Array.isArray(childValues)) {
|
||||||
|
childValues.forEach((childData = {}, index) => {
|
||||||
|
property.properties.forEach((childProperty) => {
|
||||||
|
processProperty(childProperty, childData || {}, [
|
||||||
|
...propertyPath,
|
||||||
|
index
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const nestedScope = getValueAtPath(currentData, propertyPath) || {}
|
||||||
|
property.properties.forEach((childProperty) => {
|
||||||
|
processProperty(childProperty, nestedScope || {}, propertyPath)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modelDefinition.properties.forEach((property) => {
|
||||||
|
processProperty(property, currentData)
|
||||||
})
|
})
|
||||||
|
|
||||||
return computedValues
|
return computedEntries
|
||||||
}, [])
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
// Set initial form values when defaultValues change
|
// Set initial form values when defaultValues change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Object.keys(defaultValues).length > 0) {
|
if (Object.keys(defaultValues).length > 0) {
|
||||||
// Calculate computed values for initial data
|
// Calculate computed values for initial data
|
||||||
const computedValues = calculateComputedValues(defaultValues, model)
|
const computedEntries = calculateComputedValues(defaultValues, model)
|
||||||
const initialFormData = { ...defaultValues, ...computedValues }
|
const computedValuesObject = buildObjectFromEntries(computedEntries)
|
||||||
|
const initialFormData = merge({}, defaultValues, computedValuesObject)
|
||||||
form.setFieldsValue(initialFormData)
|
form.setFieldsValue(initialFormData)
|
||||||
setObjectData((prev) => {
|
setObjectData((prev) => merge({}, prev, initialFormData))
|
||||||
return merge({}, prev, initialFormData)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}, [form, defaultValues, calculateComputedValues, model])
|
}, [form, defaultValues, calculateComputedValues, model])
|
||||||
|
|
||||||
@ -102,18 +175,31 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
|
|||||||
form={form}
|
form={form}
|
||||||
layout='vertical'
|
layout='vertical'
|
||||||
style={style}
|
style={style}
|
||||||
onValuesChange={(values) => {
|
onValuesChange={(_changedValues, allFormValues) => {
|
||||||
// Calculate computed values based on current form data
|
// Calculate computed values based on current form data
|
||||||
const currentFormData = { ...objectData, ...values }
|
const currentFormData = merge({}, objectData || {}, allFormValues)
|
||||||
const computedValues = calculateComputedValues(currentFormData, model)
|
const computedEntries = calculateComputedValues(currentFormData, model)
|
||||||
|
|
||||||
// Update form with computed values if any were calculated
|
if (Array.isArray(computedEntries) && computedEntries.length > 0) {
|
||||||
if (Object.keys(computedValues).length > 0) {
|
computedEntries.forEach(({ namePath, value }) => {
|
||||||
form.setFieldsValue(computedValues)
|
if (!Array.isArray(namePath) || value === undefined) return
|
||||||
|
const currentValue = form.getFieldValue(namePath)
|
||||||
|
if (currentValue !== value) {
|
||||||
|
if (typeof form.setFieldValue === 'function') {
|
||||||
|
form.setFieldValue(namePath, value)
|
||||||
|
} else {
|
||||||
|
const fallbackPayload = buildObjectFromEntries([
|
||||||
|
{ namePath, value }
|
||||||
|
])
|
||||||
|
form.setFieldsValue(fallbackPayload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge all values (user input + computed values)
|
// Merge all values (user input + computed values)
|
||||||
const allValues = { ...values, ...computedValues }
|
const computedValuesObject = buildObjectFromEntries(computedEntries)
|
||||||
|
const allValues = merge({}, allFormValues, computedValuesObject)
|
||||||
setObjectData((prev) => {
|
setObjectData((prev) => {
|
||||||
return merge({}, prev, allValues)
|
return merge({}, prev, allValues)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import { Dropdown, Button } from 'antd'
|
|||||||
import { getModelByName } from '../../../database/ObjectModels'
|
import { getModelByName } from '../../../database/ObjectModels'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { useNavigate, useLocation } from 'react-router-dom'
|
import { useNavigate, useLocation } from 'react-router-dom'
|
||||||
|
import { useActionsModal } from '../context/ActionsModalContext'
|
||||||
|
import KeyboardShortcut from './KeyboardShortcut'
|
||||||
|
|
||||||
// Recursively filter actions based on visibleActions
|
// Recursively filter actions based on visibleActions
|
||||||
function filterActionsByVisibility(actions, visibleActions) {
|
function filterActionsByVisibility(actions, visibleActions) {
|
||||||
@ -43,6 +45,7 @@ function mapActionsToMenuItems(actions, currentUrlWithActions, id, objectData) {
|
|||||||
const actionUrl = action.url ? action.url(id) : undefined
|
const actionUrl = action.url ? action.url(id) : undefined
|
||||||
|
|
||||||
var disabled = actionUrl && actionUrl === currentUrlWithActions
|
var disabled = actionUrl && actionUrl === currentUrlWithActions
|
||||||
|
var visible = true
|
||||||
|
|
||||||
if (action.disabled) {
|
if (action.disabled) {
|
||||||
if (typeof action.disabled === 'function') {
|
if (typeof action.disabled === 'function') {
|
||||||
@ -52,6 +55,14 @@ function mapActionsToMenuItems(actions, currentUrlWithActions, id, objectData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.visible) {
|
||||||
|
if (typeof action.visible === 'function') {
|
||||||
|
visible = action.visible(objectData)
|
||||||
|
} else {
|
||||||
|
visible = action.visible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const item = {
|
const item = {
|
||||||
key: action.key || action.name,
|
key: action.key || action.name,
|
||||||
label: action.label,
|
label: action.label,
|
||||||
@ -67,7 +78,9 @@ function mapActionsToMenuItems(actions, currentUrlWithActions, id, objectData) {
|
|||||||
objectData
|
objectData
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (visible == true) {
|
||||||
return item
|
return item
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,6 +104,7 @@ const ObjectActions = ({
|
|||||||
const actions = model.actions || []
|
const actions = model.actions || []
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
const { showActionsModal } = useActionsModal()
|
||||||
|
|
||||||
// Get current url without 'action' param
|
// Get current url without 'action' param
|
||||||
const currentUrlWithoutActions = stripActionParam(
|
const currentUrlWithoutActions = stripActionParam(
|
||||||
@ -140,11 +154,20 @@ const ObjectActions = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<KeyboardShortcut
|
||||||
|
shortcut='alt+a'
|
||||||
|
onTrigger={() => showActionsModal(id, type, objectData)}
|
||||||
|
>
|
||||||
<Dropdown menu={menu} {...dropdownProps}>
|
<Dropdown menu={menu} {...dropdownProps}>
|
||||||
<Button {...buttonProps} disabled={disabled}>
|
<Button
|
||||||
|
{...buttonProps}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => showActionsModal(id, type, objectData)}
|
||||||
|
>
|
||||||
Actions
|
Actions
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
</KeyboardShortcut>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
474
src/components/Dashboard/common/ObjectChildTable.jsx
Normal file
474
src/components/Dashboard/common/ObjectChildTable.jsx
Normal 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
|
||||||
@ -13,8 +13,20 @@ import { AuthContext } from '../context/AuthContext'
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import DeleteObjectModal from './DeleteObjectModal'
|
import DeleteObjectModal from './DeleteObjectModal'
|
||||||
import merge from 'lodash/merge'
|
import merge from 'lodash/merge'
|
||||||
|
import set from 'lodash/set'
|
||||||
import { getModelByName } from '../../../database/ObjectModels'
|
import { getModelByName } from '../../../database/ObjectModels'
|
||||||
|
|
||||||
|
const buildObjectFromEntries = (entries = []) => {
|
||||||
|
return entries.reduce((acc, entry) => {
|
||||||
|
const { namePath, value } = entry || {}
|
||||||
|
if (!Array.isArray(namePath) || value === undefined) {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
set(acc, namePath, value)
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ObjectForm is a reusable form component for editing any object type.
|
* ObjectForm is a reusable form component for editing any object type.
|
||||||
* It handles fetching, updating, locking, unlocking, and validation logic.
|
* It handles fetching, updating, locking, unlocking, and validation logic.
|
||||||
@ -37,6 +49,7 @@ const ObjectForm = forwardRef(
|
|||||||
const [lock, setLock] = useState({})
|
const [lock, setLock] = useState({})
|
||||||
const [initialized, setInitialized] = useState(false)
|
const [initialized, setInitialized] = useState(false)
|
||||||
const [isEditing, setIsEditing] = useState(false)
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
|
const isEditingRef = useRef(false)
|
||||||
const [formValid, setFormValid] = useState(false)
|
const [formValid, setFormValid] = useState(false)
|
||||||
|
|
||||||
const [form] = Form.useForm()
|
const [form] = Form.useForm()
|
||||||
@ -115,18 +128,50 @@ const ObjectForm = forwardRef(
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Function to calculate computed values from model properties
|
// Function to calculate computed values from model properties
|
||||||
const calculateComputedValues = useCallback((currentData, model) => {
|
const calculateComputedValues = useCallback(
|
||||||
if (!model || !model.properties) return {}
|
(currentData, modelDefinition) => {
|
||||||
|
if (!modelDefinition || !Array.isArray(modelDefinition.properties)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
const computedValues = {}
|
const normalizedPath = (name, parentPath = []) => {
|
||||||
|
if (Array.isArray(name)) {
|
||||||
|
return [...parentPath, ...name]
|
||||||
|
}
|
||||||
|
if (typeof name === 'number') {
|
||||||
|
return [...parentPath, name]
|
||||||
|
}
|
||||||
|
if (typeof name === 'string' && name.length > 0) {
|
||||||
|
return [...parentPath, ...name.split('.')]
|
||||||
|
}
|
||||||
|
return parentPath
|
||||||
|
}
|
||||||
|
|
||||||
|
const getValueAtPath = (dataSource, path) => {
|
||||||
|
if (!Array.isArray(path) || path.length === 0) {
|
||||||
|
return dataSource
|
||||||
|
}
|
||||||
|
return path.reduce((acc, key) => {
|
||||||
|
if (acc == null) return acc
|
||||||
|
return acc[key]
|
||||||
|
}, dataSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
const computedEntries = []
|
||||||
|
|
||||||
|
const processProperty = (property, scopeData, parentPath = []) => {
|
||||||
|
if (!property?.name) return
|
||||||
|
|
||||||
|
const propertyPath = normalizedPath(property.name, parentPath)
|
||||||
|
|
||||||
model.properties.forEach((property) => {
|
|
||||||
// Check if this property has a computed value function
|
|
||||||
if (property.value && typeof property.value === 'function') {
|
if (property.value && typeof property.value === 'function') {
|
||||||
try {
|
try {
|
||||||
const computedValue = property.value(currentData)
|
const computedValue = property.value(scopeData || {})
|
||||||
if (computedValue !== undefined) {
|
if (computedValue !== undefined) {
|
||||||
computedValues[property.name] = computedValue
|
computedEntries.push({
|
||||||
|
namePath: propertyPath,
|
||||||
|
value: computedValue
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(
|
console.warn(
|
||||||
@ -135,10 +180,41 @@ const ObjectForm = forwardRef(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
Array.isArray(property.properties) &&
|
||||||
|
property.properties.length > 0
|
||||||
|
) {
|
||||||
|
if (property.type === 'objectChildren') {
|
||||||
|
const childValues = getValueAtPath(currentData, propertyPath)
|
||||||
|
if (Array.isArray(childValues)) {
|
||||||
|
childValues.forEach((childData = {}, index) => {
|
||||||
|
property.properties.forEach((childProperty) => {
|
||||||
|
processProperty(childProperty, childData || {}, [
|
||||||
|
...propertyPath,
|
||||||
|
index
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const nestedScope =
|
||||||
|
getValueAtPath(currentData, propertyPath) || {}
|
||||||
|
property.properties.forEach((childProperty) => {
|
||||||
|
processProperty(childProperty, nestedScope || {}, propertyPath)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modelDefinition.properties.forEach((property) => {
|
||||||
|
processProperty(property, currentData)
|
||||||
})
|
})
|
||||||
|
|
||||||
return computedValues
|
return computedEntries
|
||||||
}, [])
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
// Validate form on change (debounced to avoid heavy work on every keystroke)
|
// Validate form on change (debounced to avoid heavy work on every keystroke)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -146,7 +222,8 @@ const ObjectForm = forwardRef(
|
|||||||
const currentFormValues = form.getFieldsValue()
|
const currentFormValues = form.getFieldsValue()
|
||||||
const mergedObjectData = {
|
const mergedObjectData = {
|
||||||
...serverObjectData.current,
|
...serverObjectData.current,
|
||||||
...currentFormValues
|
...currentFormValues,
|
||||||
|
_isEditing: isEditingRef.current
|
||||||
}
|
}
|
||||||
|
|
||||||
form
|
form
|
||||||
@ -198,12 +275,13 @@ const ObjectForm = forwardRef(
|
|||||||
const lockEvent = await fetchObjectLock(id, type)
|
const lockEvent = await fetchObjectLock(id, type)
|
||||||
setLock(lockEvent)
|
setLock(lockEvent)
|
||||||
onStateChangeRef.current({ lock: lockEvent })
|
onStateChangeRef.current({ lock: lockEvent })
|
||||||
setObjectData(data)
|
setObjectData({ ...data, _isEditing: isEditingRef.current })
|
||||||
serverObjectData.current = data
|
serverObjectData.current = data
|
||||||
|
|
||||||
// Calculate and set computed values on initial load
|
// Calculate and set computed values on initial load
|
||||||
const computedValues = calculateComputedValues(data, model)
|
const computedEntries = calculateComputedValues(data, model)
|
||||||
const initialFormData = { ...data, ...computedValues }
|
const computedValuesObject = buildObjectFromEntries(computedEntries)
|
||||||
|
const initialFormData = merge({}, data, computedValuesObject)
|
||||||
|
|
||||||
form.setFieldsValue(initialFormData)
|
form.setFieldsValue(initialFormData)
|
||||||
setFetchLoading(false)
|
setFetchLoading(false)
|
||||||
@ -275,24 +353,37 @@ const ObjectForm = forwardRef(
|
|||||||
|
|
||||||
const startEditing = () => {
|
const startEditing = () => {
|
||||||
setIsEditing(true)
|
setIsEditing(true)
|
||||||
onStateChangeRef.current({ isEditing: true })
|
isEditingRef.current = true
|
||||||
|
console.log('IS EDITING TRUE')
|
||||||
|
setObjectData((prev) => ({ ...prev, _isEditing: isEditingRef.current }))
|
||||||
|
onStateChangeRef.current({
|
||||||
|
isEditing: true,
|
||||||
|
objectData: { ...objectData, _isEditing: isEditingRef.current }
|
||||||
|
})
|
||||||
lockObject(id, type)
|
lockObject(id, type)
|
||||||
}
|
}
|
||||||
|
|
||||||
const cancelEditing = () => {
|
const cancelEditing = () => {
|
||||||
if (serverObjectData.current) {
|
if (serverObjectData.current) {
|
||||||
// Recalculate computed values when canceling
|
// Recalculate computed values when canceling
|
||||||
const computedValues = calculateComputedValues(
|
const computedEntries = calculateComputedValues(
|
||||||
serverObjectData.current,
|
serverObjectData.current,
|
||||||
model
|
model
|
||||||
)
|
)
|
||||||
const resetFormData = { ...serverObjectData.current, ...computedValues }
|
const computedValuesObject = buildObjectFromEntries(computedEntries)
|
||||||
|
const resetFormData = merge(
|
||||||
form.setFieldsValue(resetFormData)
|
{},
|
||||||
setObjectData(resetFormData)
|
serverObjectData.current,
|
||||||
}
|
computedValuesObject
|
||||||
|
)
|
||||||
setIsEditing(false)
|
setIsEditing(false)
|
||||||
onStateChangeRef.current({ isEditing: false })
|
isEditingRef.current = false
|
||||||
|
form.setFieldsValue(resetFormData)
|
||||||
|
console.log('IS EDITING FALSE')
|
||||||
|
setObjectData({ ...resetFormData, _isEditing: isEditingRef.current })
|
||||||
|
}
|
||||||
|
|
||||||
|
onStateChangeRef.current({ isEditing: isEditingRef.current })
|
||||||
unlockObject(id, type)
|
unlockObject(id, type)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,9 +393,15 @@ const ObjectForm = forwardRef(
|
|||||||
setEditLoading(true)
|
setEditLoading(true)
|
||||||
onStateChangeRef.current({ editLoading: true })
|
onStateChangeRef.current({ editLoading: true })
|
||||||
await updateObject(id, type, value)
|
await updateObject(id, type, value)
|
||||||
setObjectData({ ...objectData, ...value })
|
|
||||||
setIsEditing(false)
|
setIsEditing(false)
|
||||||
onStateChangeRef.current({ isEditing: false })
|
isEditingRef.current = false
|
||||||
|
onStateChangeRef.current({ isEditing: isEditingRef.current })
|
||||||
|
setObjectData({
|
||||||
|
...objectData,
|
||||||
|
...value,
|
||||||
|
_isEditing: isEditingRef.current
|
||||||
|
})
|
||||||
messageApi.success('Information updated successfully')
|
messageApi.success('Information updated successfully')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
@ -374,37 +471,51 @@ const ObjectForm = forwardRef(
|
|||||||
form={form}
|
form={form}
|
||||||
layout='vertical'
|
layout='vertical'
|
||||||
style={style}
|
style={style}
|
||||||
onValuesChange={(values) => {
|
onValuesChange={(changedValues, allFormValues) => {
|
||||||
|
// Use the full form snapshot (allFormValues) so list fields (Form.List)
|
||||||
|
// come through as complete arrays instead of sparse arrays like
|
||||||
|
// [null, null, { quantity: 5 }].
|
||||||
if (onEdit != undefined) {
|
if (onEdit != undefined) {
|
||||||
onEdit(values)
|
onEdit(allFormValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate computed values based on current form data
|
// Calculate computed values based on current form data
|
||||||
const currentFormData = { ...objectData, ...values }
|
const currentFormData = {
|
||||||
const computedValues = calculateComputedValues(
|
...(serverObjectData.current || {}),
|
||||||
|
...allFormValues
|
||||||
|
}
|
||||||
|
const computedEntries = calculateComputedValues(
|
||||||
currentFormData,
|
currentFormData,
|
||||||
model
|
model
|
||||||
)
|
)
|
||||||
|
|
||||||
// Update form with computed values if any were calculated and they changed
|
if (Array.isArray(computedEntries) && computedEntries.length > 0) {
|
||||||
if (Object.keys(computedValues).length > 0) {
|
computedEntries.forEach(({ namePath, value }) => {
|
||||||
const currentComputedValues = form.getFieldsValue(
|
if (!Array.isArray(namePath) || value === undefined) return
|
||||||
Object.keys(computedValues)
|
const currentValue = form.getFieldValue(namePath)
|
||||||
)
|
if (currentValue !== value) {
|
||||||
const hasDiff = Object.keys(computedValues).some(
|
if (typeof form.setFieldValue === 'function') {
|
||||||
(key) => currentComputedValues[key] !== computedValues[key]
|
form.setFieldValue(namePath, value)
|
||||||
)
|
} else {
|
||||||
|
const fallbackPayload = buildObjectFromEntries([
|
||||||
if (hasDiff) {
|
{ namePath, value }
|
||||||
form.setFieldsValue(computedValues)
|
])
|
||||||
|
form.setFieldsValue(fallbackPayload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Merge all values (user input + computed values)
|
const computedValuesObject = buildObjectFromEntries(computedEntries)
|
||||||
const allValues = { ...values, ...computedValues }
|
const mergedFormValues = merge(
|
||||||
|
{},
|
||||||
|
allFormValues,
|
||||||
|
computedValuesObject
|
||||||
|
)
|
||||||
|
mergedFormValues._isEditing = isEditingRef.current
|
||||||
|
|
||||||
setObjectData((prev) => {
|
setObjectData((prev) => {
|
||||||
return { ...prev, ...allValues }
|
return { ...prev, ...mergedFormValues }
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -43,6 +43,8 @@ import AlertsDisplay from './AlertsDisplay'
|
|||||||
import FileUpload from './FileUpload'
|
import FileUpload from './FileUpload'
|
||||||
import DataTree from './DataTree'
|
import DataTree from './DataTree'
|
||||||
import FileList from './FileList'
|
import FileList from './FileList'
|
||||||
|
import ObjectChildTable from './ObjectChildTable'
|
||||||
|
import MiscId from './MiscId'
|
||||||
|
|
||||||
const { Text } = Typography
|
const { Text } = Typography
|
||||||
|
|
||||||
@ -86,6 +88,11 @@ const ObjectProperty = ({
|
|||||||
roundNumber = false,
|
roundNumber = false,
|
||||||
showHyperlink,
|
showHyperlink,
|
||||||
showSince,
|
showSince,
|
||||||
|
properties = [],
|
||||||
|
onChange = null,
|
||||||
|
maxWidth = '100%',
|
||||||
|
loading = false,
|
||||||
|
rollups = [],
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
if (value && typeof value == 'function' && objectData) {
|
if (value && typeof value == 'function' && objectData) {
|
||||||
@ -379,6 +386,18 @@ const ObjectProperty = ({
|
|||||||
case 'objectList': {
|
case 'objectList': {
|
||||||
return <ObjectList value={value} objectType={objectType} />
|
return <ObjectList value={value} objectType={objectType} />
|
||||||
}
|
}
|
||||||
|
case 'objectChildren': {
|
||||||
|
return (
|
||||||
|
<ObjectChildTable
|
||||||
|
value={value}
|
||||||
|
properties={properties}
|
||||||
|
objectData={objectData}
|
||||||
|
maxWidth={maxWidth}
|
||||||
|
loading={loading}
|
||||||
|
rollups={rollups}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
case 'state': {
|
case 'state': {
|
||||||
if (value && value?.type) {
|
if (value && value?.type) {
|
||||||
return <StateDisplay {...rest} state={value} />
|
return <StateDisplay {...rest} state={value} />
|
||||||
@ -419,6 +438,9 @@ const ObjectProperty = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case 'miscId': {
|
||||||
|
return <MiscId value={value} {...rest} />
|
||||||
|
}
|
||||||
case 'density': {
|
case 'density': {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
return <Text {...textParams}>{`${value} g/cm³`}</Text>
|
return <Text {...textParams}>{`${value} g/cm³`}</Text>
|
||||||
@ -432,7 +454,14 @@ const ObjectProperty = ({
|
|||||||
}
|
}
|
||||||
case 'alerts': {
|
case 'alerts': {
|
||||||
if (value != null && value?.length != 0) {
|
if (value != null && value?.length != 0) {
|
||||||
return <AlertsDisplay alerts={value} />
|
return (
|
||||||
|
<AlertsDisplay
|
||||||
|
alerts={value}
|
||||||
|
printerId={objectData._id}
|
||||||
|
showDismiss={false}
|
||||||
|
showActions={false}
|
||||||
|
/>
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Text type='secondary' {...textParams}>
|
<Text type='secondary' {...textParams}>
|
||||||
@ -546,6 +575,11 @@ const ObjectProperty = ({
|
|||||||
margin: 0,
|
margin: 0,
|
||||||
...(mergedFormItemProps.style || {})
|
...(mergedFormItemProps.style || {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof onChange === 'function') {
|
||||||
|
mergedFormItemProps.onChange = onChange
|
||||||
|
}
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'netGross':
|
case 'netGross':
|
||||||
return (
|
return (
|
||||||
@ -736,7 +770,7 @@ const ObjectProperty = ({
|
|||||||
case 'objectType':
|
case 'objectType':
|
||||||
return (
|
return (
|
||||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||||
<ObjectTypeSelect disabled={disabled} />
|
<ObjectTypeSelect disabled={disabled} masterFilter={masterFilter} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
case 'objectList':
|
case 'objectList':
|
||||||
@ -775,6 +809,18 @@ const ObjectProperty = ({
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
|
case 'objectChildren': {
|
||||||
|
return (
|
||||||
|
<ObjectChildTable
|
||||||
|
value={value}
|
||||||
|
properties={properties}
|
||||||
|
objectData={objectData}
|
||||||
|
isEditing={true}
|
||||||
|
formListName={formItemName}
|
||||||
|
rollups={rollups}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||||
@ -815,7 +861,9 @@ ObjectProperty.propTypes = {
|
|||||||
showPreview: PropTypes.bool,
|
showPreview: PropTypes.bool,
|
||||||
showHyperlink: PropTypes.bool,
|
showHyperlink: PropTypes.bool,
|
||||||
options: PropTypes.array,
|
options: PropTypes.array,
|
||||||
showSince: PropTypes.bool
|
showSince: PropTypes.bool,
|
||||||
|
loading: PropTypes.bool,
|
||||||
|
rollups: PropTypes.arrayOf(PropTypes.object)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ObjectProperty
|
export default ObjectProperty
|
||||||
|
|||||||
@ -14,8 +14,16 @@ import { AuthContext } from '../context/AuthContext'
|
|||||||
import ObjectProperty from './ObjectProperty'
|
import ObjectProperty from './ObjectProperty'
|
||||||
import { getModelByName } from '../../../database/ObjectModels'
|
import { getModelByName } from '../../../database/ObjectModels'
|
||||||
import merge from 'lodash/merge'
|
import merge from 'lodash/merge'
|
||||||
|
import { getModelProperty } from '../../../database/ObjectModels'
|
||||||
const { SHOW_CHILD } = TreeSelect
|
const { SHOW_CHILD } = TreeSelect
|
||||||
|
|
||||||
|
// Helper to check if two values are equal (handling objects/ids)
|
||||||
|
const areValuesEqual = (v1, v2) => {
|
||||||
|
const id1 = v1 && typeof v1 === 'object' && v1._id ? v1._id : v1
|
||||||
|
const id2 = v2 && typeof v2 === 'object' && v2._id ? v2._id : v2
|
||||||
|
return String(id1) === String(id2)
|
||||||
|
}
|
||||||
|
|
||||||
const ObjectSelect = ({
|
const ObjectSelect = ({
|
||||||
type = 'unknown',
|
type = 'unknown',
|
||||||
showSearch = false,
|
showSearch = false,
|
||||||
@ -31,7 +39,7 @@ const ObjectSelect = ({
|
|||||||
const { token } = useContext(AuthContext)
|
const { token } = useContext(AuthContext)
|
||||||
// --- State ---
|
// --- State ---
|
||||||
const [treeData, setTreeData] = useState([])
|
const [treeData, setTreeData] = useState([])
|
||||||
const [objectPropertiesTree, setObjectPropertiesTree] = useState({})
|
const [objectPropertiesTree, setObjectPropertiesTree] = useState([])
|
||||||
const [initialized, setInitialized] = useState(false)
|
const [initialized, setInitialized] = useState(false)
|
||||||
const [error, setError] = useState(false)
|
const [error, setError] = useState(false)
|
||||||
const properties = useMemo(() => getModelByName(type).group || [], [type])
|
const properties = useMemo(() => getModelByName(type).group || [], [type])
|
||||||
@ -69,6 +77,50 @@ const ObjectSelect = ({
|
|||||||
[isMinimalObject, fetchObject, type]
|
[isMinimalObject, fetchObject, type]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const mergeGroups = useCallback((current, incoming) => {
|
||||||
|
if (!current) return incoming
|
||||||
|
if (!incoming) return current
|
||||||
|
if (!Array.isArray(current) || !Array.isArray(incoming)) return incoming
|
||||||
|
|
||||||
|
const merged = [...current]
|
||||||
|
|
||||||
|
// Helper to generate a unique key for a group node
|
||||||
|
const getGroupKey = (item) => {
|
||||||
|
const val = item.value
|
||||||
|
const valPart =
|
||||||
|
val && typeof val === 'object' && val._id
|
||||||
|
? val._id
|
||||||
|
: JSON.stringify(val)
|
||||||
|
return `${item.property}:${valPart}`
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of incoming) {
|
||||||
|
if (item.property && item.value !== undefined) {
|
||||||
|
// It's a group node
|
||||||
|
const itemKey = getGroupKey(item)
|
||||||
|
const existingIdx = merged.findIndex(
|
||||||
|
(x) =>
|
||||||
|
x.property && x.value !== undefined && getGroupKey(x) === itemKey
|
||||||
|
)
|
||||||
|
|
||||||
|
if (existingIdx > -1) {
|
||||||
|
merged[existingIdx] = {
|
||||||
|
...merged[existingIdx],
|
||||||
|
children: mergeGroups(merged[existingIdx].children, item.children)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
merged.push(item)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// It's a leaf object
|
||||||
|
if (!merged.some((x) => String(x._id) === String(item._id))) {
|
||||||
|
merged.push(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return merged
|
||||||
|
}, [])
|
||||||
|
|
||||||
// Fetch the object properties tree from the API
|
// Fetch the object properties tree from the API
|
||||||
const handleFetchObjectsProperties = useCallback(
|
const handleFetchObjectsProperties = useCallback(
|
||||||
async (customFilter = filter) => {
|
async (customFilter = filter) => {
|
||||||
@ -78,11 +130,14 @@ const ObjectSelect = ({
|
|||||||
filter: customFilter,
|
filter: customFilter,
|
||||||
masterFilter
|
masterFilter
|
||||||
})
|
})
|
||||||
|
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
setObjectPropertiesTree((prev) => merge([], prev, data))
|
setObjectPropertiesTree((prev) => mergeGroups(prev, data))
|
||||||
} else {
|
} else {
|
||||||
setObjectPropertiesTree((prev) => merge({}, prev, data))
|
// Fallback if API returns something unexpected
|
||||||
|
setObjectPropertiesTree((prev) => merge([], prev, data))
|
||||||
}
|
}
|
||||||
|
|
||||||
setInitialLoading(false)
|
setInitialLoading(false)
|
||||||
setError(false)
|
setError(false)
|
||||||
return data
|
return data
|
||||||
@ -92,24 +147,31 @@ const ObjectSelect = ({
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[type, fetchObjectsByProperty, properties, filter, masterFilter]
|
[
|
||||||
|
type,
|
||||||
|
fetchObjectsByProperty,
|
||||||
|
properties,
|
||||||
|
filter,
|
||||||
|
masterFilter,
|
||||||
|
mergeGroups
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Convert the API response to AntD TreeSelect treeData
|
// Convert the API response to AntD TreeSelect treeData
|
||||||
const buildTreeData = useCallback(
|
const buildTreeData = useCallback(
|
||||||
(data, pIdx = 0, parentKeys = [], filterPath = []) => {
|
(data, pIdx = 0, parentKeys = [], filterPath = []) => {
|
||||||
if (!data) return []
|
if (!data || !Array.isArray(data)) return []
|
||||||
if (Array.isArray(data)) {
|
console.log(data, pIdx, properties.length)
|
||||||
|
// If we are past the grouping properties, these are leaf objects
|
||||||
|
if (pIdx >= properties.length) {
|
||||||
return data.map((object) => {
|
return data.map((object) => {
|
||||||
setObjectList((prev) => {
|
setObjectList((prev) => {
|
||||||
const filtered = prev.filter(
|
if (prev.some((p) => p._id === object._id)) return prev
|
||||||
(prevObject) => prevObject._id != object._id
|
return [...prev, object]
|
||||||
)
|
|
||||||
return [...filtered, object]
|
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
title: (
|
title: (
|
||||||
<div style={{ paddingTop: '2px' }}>
|
<div style={{ paddingTop: 0 }}>
|
||||||
<ObjectProperty
|
<ObjectProperty
|
||||||
key={object._id}
|
key={object._id}
|
||||||
type='object'
|
type='object'
|
||||||
@ -123,48 +185,63 @@ const ObjectSelect = ({
|
|||||||
value: object._id,
|
value: object._id,
|
||||||
key: object._id,
|
key: object._id,
|
||||||
isLeaf: true,
|
isLeaf: true,
|
||||||
property: properties[pIdx - 1], // previous property
|
|
||||||
parentKeys,
|
parentKeys,
|
||||||
filterPath
|
filterPath
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (typeof data == 'object') {
|
|
||||||
const property = properties[pIdx] || null
|
// Group Nodes
|
||||||
return Object.entries(data)
|
return data
|
||||||
.map(([key, value]) => {
|
.map((group) => {
|
||||||
if (property != null && typeof value === 'object') {
|
// Only process if it looks like a group
|
||||||
const newFilterPath = filterPath.concat({ property, value: key })
|
if (!group.property) return null
|
||||||
return {
|
|
||||||
title: <ObjectProperty type={property} value={key} />,
|
const { property, value, children } = group
|
||||||
value: parentKeys.concat(key).join('-'),
|
var valueString = value
|
||||||
filterValue: key,
|
if (value && typeof value === 'object' && value._id) {
|
||||||
key: parentKeys.concat(key).join('-'),
|
valueString = value._id
|
||||||
|
}
|
||||||
|
if (Array.isArray(valueString)) {
|
||||||
|
valueString = valueString.join(',')
|
||||||
|
}
|
||||||
|
const nodeKey = parentKeys
|
||||||
|
.concat(property + ':' + valueString)
|
||||||
|
.join('-')
|
||||||
|
const newFilterPath = filterPath.concat({
|
||||||
property,
|
property,
|
||||||
parentKeys: parentKeys.concat(key || '-'),
|
value: valueString
|
||||||
|
})
|
||||||
|
|
||||||
|
const modelProperty = getModelProperty(type, property)
|
||||||
|
return {
|
||||||
|
title: <ObjectProperty {...modelProperty} value={value} />,
|
||||||
|
value: nodeKey,
|
||||||
|
key: nodeKey,
|
||||||
|
property,
|
||||||
|
filterValue: valueString,
|
||||||
|
parentKeys: parentKeys.concat(valueString),
|
||||||
filterPath: newFilterPath,
|
filterPath: newFilterPath,
|
||||||
selectable: false,
|
selectable: false,
|
||||||
|
isLeaf: false,
|
||||||
children: buildTreeData(
|
children: buildTreeData(
|
||||||
value,
|
children,
|
||||||
pIdx + 1,
|
pIdx + 1,
|
||||||
parentKeys.concat(key),
|
parentKeys.concat(valueString),
|
||||||
newFilterPath
|
newFilterPath
|
||||||
),
|
)
|
||||||
isLeaf: false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[properties, type]
|
[properties, type]
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- loadData for async loading on expand ---
|
// --- loadData for async loading on expand ---
|
||||||
const loadData = async (node) => {
|
const loadData = async (node) => {
|
||||||
// node.property is the property name, node.value is the value
|
// node.property is the property name, node.value is the value key
|
||||||
if (!node.property) return
|
if (!node.property) return
|
||||||
|
if (type == 'unknown') return
|
||||||
// Build filter for this node by merging all parent property-value pairs
|
// Build filter for this node by merging all parent property-value pairs
|
||||||
const customFilter = { ...filter }
|
const customFilter = { ...filter }
|
||||||
if (Array.isArray(node.filterPath)) {
|
if (Array.isArray(node.filterPath)) {
|
||||||
@ -172,26 +249,40 @@ const ObjectSelect = ({
|
|||||||
customFilter[property] = value
|
customFilter[property] = value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// Ensure current node is in filter (should be covered by filterPath, but redundancy is safe)
|
||||||
customFilter[node.property] = node.filterValue
|
customFilter[node.property] = node.filterValue
|
||||||
// Fetch children for this node
|
// Fetch children for this node
|
||||||
const data = await handleFetchObjectsProperties(customFilter)
|
const data = await handleFetchObjectsProperties(customFilter)
|
||||||
if (!data) return
|
if (!data) return
|
||||||
// Extract only the children for the specific node that was expanded
|
|
||||||
let nodeSpecificData = data
|
// Navigate to the specific node's children in the response
|
||||||
if (typeof data === 'object' && !Array.isArray(data)) {
|
let nodeSpecificChildren = data
|
||||||
// If the API returns an object with multiple keys, get only the data for this node
|
|
||||||
nodeSpecificData = data[node.value] || {}
|
if (node.filterPath && Array.isArray(node.filterPath)) {
|
||||||
|
for (const pathItem of node.filterPath) {
|
||||||
|
if (!Array.isArray(nodeSpecificChildren)) break
|
||||||
|
const match = nodeSpecificChildren.find(
|
||||||
|
(g) =>
|
||||||
|
g.property === pathItem.property &&
|
||||||
|
areValuesEqual(g.value, pathItem.value)
|
||||||
|
)
|
||||||
|
if (match) {
|
||||||
|
nodeSpecificChildren = match.children
|
||||||
|
} else {
|
||||||
|
nodeSpecificChildren = []
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Build new tree children only for this specific node
|
// Build new tree children only for this specific node
|
||||||
const children = buildTreeData(
|
const children = buildTreeData(
|
||||||
nodeSpecificData,
|
nodeSpecificChildren,
|
||||||
properties.indexOf(node.property) + 1,
|
properties.indexOf(node.property) + 1,
|
||||||
node.parentKeys || [],
|
node.parentKeys || [],
|
||||||
(node.filterPath || []).concat({
|
node.filterPath
|
||||||
property: node.property,
|
|
||||||
value: node.filterValue
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Update treeData with new children for this node only
|
// Update treeData with new children for this node only
|
||||||
setTreeData((prevTreeData) => {
|
setTreeData((prevTreeData) => {
|
||||||
// Helper to recursively update the correct node
|
// Helper to recursively update the correct node
|
||||||
@ -250,7 +341,8 @@ const ObjectSelect = ({
|
|||||||
value &&
|
value &&
|
||||||
typeof value === 'object' &&
|
typeof value === 'object' &&
|
||||||
value !== null &&
|
value !== null &&
|
||||||
!initialized
|
!initialized &&
|
||||||
|
type != 'unknown'
|
||||||
) {
|
) {
|
||||||
// Check if value is a minimal object and fetch full object if needed
|
// Check if value is a minimal object and fetch full object if needed
|
||||||
const fullValue = await fetchFullObjectIfNeeded(value)
|
const fullValue = await fetchFullObjectIfNeeded(value)
|
||||||
@ -260,7 +352,13 @@ const ObjectSelect = ({
|
|||||||
properties.forEach((prop) => {
|
properties.forEach((prop) => {
|
||||||
if (Object.prototype.hasOwnProperty.call(fullValue, prop)) {
|
if (Object.prototype.hasOwnProperty.call(fullValue, prop)) {
|
||||||
const filterValue = fullValue[prop]
|
const filterValue = fullValue[prop]
|
||||||
if (filterValue?.name) {
|
if (
|
||||||
|
filterValue &&
|
||||||
|
typeof filterValue === 'object' &&
|
||||||
|
filterValue._id
|
||||||
|
) {
|
||||||
|
valueFilter[prop] = filterValue._id
|
||||||
|
} else if (filterValue?.name) {
|
||||||
valueFilter[prop] = filterValue.name
|
valueFilter[prop] = filterValue.name
|
||||||
} else if (Array.isArray(filterValue)) {
|
} else if (Array.isArray(filterValue)) {
|
||||||
valueFilter[prop] = filterValue.join(',')
|
valueFilter[prop] = filterValue.join(',')
|
||||||
@ -275,12 +373,13 @@ const ObjectSelect = ({
|
|||||||
setInitialized(true)
|
setInitialized(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!initialized && token != null) {
|
if (!initialized && token != null && type != 'unknown') {
|
||||||
handleFetchObjectsProperties()
|
handleFetchObjectsProperties()
|
||||||
setInitialized(true)
|
setInitialized(true)
|
||||||
}
|
}
|
||||||
if (value == null) {
|
if (value == null || type == 'unknown') {
|
||||||
setTreeSelectValue(null)
|
setTreeSelectValue(null)
|
||||||
|
setInitialLoading(false)
|
||||||
setInitialized(true)
|
setInitialized(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -292,7 +391,8 @@ const ObjectSelect = ({
|
|||||||
handleFetchObjectsProperties,
|
handleFetchObjectsProperties,
|
||||||
initialized,
|
initialized,
|
||||||
token,
|
token,
|
||||||
fetchFullObjectIfNeeded
|
fetchFullObjectIfNeeded,
|
||||||
|
type
|
||||||
])
|
])
|
||||||
|
|
||||||
const prevValuesRef = useRef({ type, masterFilter })
|
const prevValuesRef = useRef({ type, masterFilter })
|
||||||
@ -341,6 +441,14 @@ const ObjectSelect = ({
|
|||||||
}
|
}
|
||||||
}, [value])
|
}, [value])
|
||||||
|
|
||||||
|
const placeholder = useMemo(
|
||||||
|
() =>
|
||||||
|
type == 'unknown'
|
||||||
|
? 'n/a'
|
||||||
|
: `Select a ${getModelByName(type).label.toLowerCase()}...`,
|
||||||
|
[type]
|
||||||
|
)
|
||||||
|
|
||||||
// --- Error UI ---
|
// --- Error UI ---
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
@ -373,12 +481,12 @@ const ObjectSelect = ({
|
|||||||
multiple={multiple}
|
multiple={multiple}
|
||||||
loadData={loadData}
|
loadData={loadData}
|
||||||
showCheckedStrategy={SHOW_CHILD}
|
showCheckedStrategy={SHOW_CHILD}
|
||||||
placeholder={`Select a ${getModelByName(type).label.toLowerCase()}...`}
|
placeholder={placeholder}
|
||||||
{...treeSelectProps}
|
{...treeSelectProps}
|
||||||
{...rest}
|
{...rest}
|
||||||
value={treeSelectValue}
|
value={treeSelectValue}
|
||||||
onChange={onTreeSelectChange}
|
onChange={onTreeSelectChange}
|
||||||
disabled={disabled}
|
disabled={disabled || type == 'unknown'}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,11 +10,18 @@ const ObjectTypeSelect = ({
|
|||||||
placeholder = 'Select object type...',
|
placeholder = 'Select object type...',
|
||||||
showSearch = true,
|
showSearch = true,
|
||||||
allowClear = true,
|
allowClear = true,
|
||||||
disabled = false
|
disabled = false,
|
||||||
|
masterFilter = null
|
||||||
}) => {
|
}) => {
|
||||||
// Create options from object models
|
// Create options from object models
|
||||||
const options = objectModels
|
const options = objectModels
|
||||||
.sort((a, b) => a.label.localeCompare(b.label))
|
.sort((a, b) => a.label.localeCompare(b.label))
|
||||||
|
.filter((model) => {
|
||||||
|
if (masterFilter == null || Object.keys(masterFilter).length == 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return masterFilter.includes(model?.name)
|
||||||
|
})
|
||||||
.map((model) => ({
|
.map((model) => ({
|
||||||
value: model.name,
|
value: model.name,
|
||||||
label: <ObjectTypeDisplay objectType={model.name} />,
|
label: <ObjectTypeDisplay objectType={model.name} />,
|
||||||
@ -46,7 +53,8 @@ ObjectTypeSelect.propTypes = {
|
|||||||
placeholder: PropTypes.string,
|
placeholder: PropTypes.string,
|
||||||
showSearch: PropTypes.bool,
|
showSearch: PropTypes.bool,
|
||||||
allowClear: PropTypes.bool,
|
allowClear: PropTypes.bool,
|
||||||
disabled: PropTypes.bool
|
disabled: PropTypes.bool,
|
||||||
|
masterFilter: PropTypes.object
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ObjectTypeSelect
|
export default ObjectTypeSelect
|
||||||
|
|||||||
@ -70,7 +70,7 @@ const PrinterTemperaturePanel = ({
|
|||||||
}, [temperatureData.bed?.target])
|
}, [temperatureData.bed?.target])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id && connected) {
|
if (id && connected == true) {
|
||||||
const temperatureEventUnsubscribe = subscribeToObjectEvent(
|
const temperatureEventUnsubscribe = subscribeToObjectEvent(
|
||||||
id,
|
id,
|
||||||
'printer',
|
'printer',
|
||||||
|
|||||||
@ -58,6 +58,7 @@ const PropertyChanges = ({ type, value }) => {
|
|||||||
longId={false}
|
longId={false}
|
||||||
minimal={true}
|
minimal={true}
|
||||||
objectData={value?.old}
|
objectData={value?.old}
|
||||||
|
maxWidth='200px'
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{value?.old && value?.new ? (
|
{value?.old && value?.new ? (
|
||||||
@ -71,6 +72,7 @@ const PropertyChanges = ({ type, value }) => {
|
|||||||
longId={false}
|
longId={false}
|
||||||
minimal={true}
|
minimal={true}
|
||||||
objectData={value?.new}
|
objectData={value?.new}
|
||||||
|
maxWidth='200px'
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
20
src/components/Dashboard/common/ScrollBox.jsx
Normal file
20
src/components/Dashboard/common/ScrollBox.jsx
Normal 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
|
||||||
@ -18,8 +18,12 @@ const StateDisplay = ({
|
|||||||
'processing',
|
'processing',
|
||||||
'queued',
|
'queued',
|
||||||
'printing',
|
'printing',
|
||||||
'used'
|
'used',
|
||||||
|
'deploying'
|
||||||
]
|
]
|
||||||
|
const orangeProgressTypes = ['used', 'deploying', 'queued']
|
||||||
|
const activeProgressTypes = ['printing', 'deploying']
|
||||||
|
|
||||||
const currentState = state || {
|
const currentState = state || {
|
||||||
type: 'unknown',
|
type: 'unknown',
|
||||||
progress: 0
|
progress: 0
|
||||||
@ -39,8 +43,12 @@ const StateDisplay = ({
|
|||||||
currentState?.progress > 0 ? (
|
currentState?.progress > 0 ? (
|
||||||
<Progress
|
<Progress
|
||||||
percent={Math.round(currentState.progress * 100)}
|
percent={Math.round(currentState.progress * 100)}
|
||||||
status={currentState.type === 'used' ? '' : 'active'}
|
status={
|
||||||
strokeColor={currentState.type === 'used' ? 'orange' : ''}
|
activeProgressTypes.includes(currentState.type) ? 'active' : ''
|
||||||
|
}
|
||||||
|
strokeColor={
|
||||||
|
orangeProgressTypes.includes(currentState.type) ? 'orange' : ''
|
||||||
|
}
|
||||||
style={{ width: '150px', marginBottom: '2px' }}
|
style={{ width: '150px', marginBottom: '2px' }}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@ -92,6 +92,10 @@ const StateTag = ({ state, showBadge = true, style = {} }) => {
|
|||||||
status = 'warning'
|
status = 'warning'
|
||||||
text = 'Used'
|
text = 'Used'
|
||||||
break
|
break
|
||||||
|
case 'unconsumed':
|
||||||
|
status = 'success'
|
||||||
|
text = 'Unconsumed'
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
status = 'default'
|
status = 'default'
|
||||||
text = state || 'Unknown'
|
text = state || 'Unknown'
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import CheckCircleIcon from '../../Icons/CheckCircleIcon.jsx'
|
|||||||
import ObjectProperty from '../common/ObjectProperty.jsx'
|
import ObjectProperty from '../common/ObjectProperty.jsx'
|
||||||
import TemplatePreview from './TemplatePreview.jsx'
|
import TemplatePreview from './TemplatePreview.jsx'
|
||||||
import DataTree from './DataTree.jsx'
|
import DataTree from './DataTree.jsx'
|
||||||
|
//import { useMediaQuery } from 'react-responsive'
|
||||||
|
|
||||||
const TemplateEditor = ({
|
const TemplateEditor = ({
|
||||||
objectData,
|
objectData,
|
||||||
@ -28,6 +29,7 @@ const TemplateEditor = ({
|
|||||||
const [testObjectViewMode, setTestObjectViewMode] = useState('Tree')
|
const [testObjectViewMode, setTestObjectViewMode] = useState('Tree')
|
||||||
const [previewMessage, setPreviewMessage] = useState('No issues found.')
|
const [previewMessage, setPreviewMessage] = useState('No issues found.')
|
||||||
const [previewError, setPreviewError] = useState(false)
|
const [previewError, setPreviewError] = useState(false)
|
||||||
|
//const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||||
|
|
||||||
const handlePreviewMessage = (message, isError) => {
|
const handlePreviewMessage = (message, isError) => {
|
||||||
setPreviewMessage(message)
|
setPreviewMessage(message)
|
||||||
@ -36,7 +38,7 @@ const TemplateEditor = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Splitter className={'farmcontrol-splitter'}>
|
<Splitter className={'farmcontrol-splitter'} vertical={true}>
|
||||||
{collapseState.preview == true && (
|
{collapseState.preview == true && (
|
||||||
<Splitter.Panel style={{ height: '100%' }}>
|
<Splitter.Panel style={{ height: '100%' }}>
|
||||||
<Card
|
<Card
|
||||||
|
|||||||
@ -58,10 +58,10 @@ const WizardView = ({
|
|||||||
gap={'middle'}
|
gap={'middle'}
|
||||||
style={
|
style={
|
||||||
sideBarGrow == false
|
sideBarGrow == false
|
||||||
? { width: '100%' }
|
? { width: '100%', minWidth: 0 }
|
||||||
: isMobile
|
: isMobile
|
||||||
? { width: '100%' }
|
? { width: '100%', minWidth: 0 }
|
||||||
: { width: '400px' }
|
: { width: '400px', minWidth: 0 }
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Flex vertical gap='middle' style={{ flexGrow: 1, width: '100%' }}>
|
<Flex vertical gap='middle' style={{ flexGrow: 1, width: '100%' }}>
|
||||||
|
|||||||
316
src/components/Dashboard/context/ActionsModalContext.jsx
Normal file
316
src/components/Dashboard/context/ActionsModalContext.jsx
Normal 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 }
|
||||||
@ -70,6 +70,11 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
[userProfile?._id]
|
[userProfile?._id]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const clearSubscriptions = useCallback(() => {
|
||||||
|
subscribedCallbacksRef.current.clear()
|
||||||
|
subscribedLockCallbacksRef.current.clear()
|
||||||
|
}, [])
|
||||||
|
|
||||||
const connectToServer = useCallback(() => {
|
const connectToServer = useCallback(() => {
|
||||||
if (token && authenticated == true) {
|
if (token && authenticated == true) {
|
||||||
logger.debug('Token is available, connecting to api server...')
|
logger.debug('Token is available, connecting to api server...')
|
||||||
@ -101,6 +106,7 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
newSocket.on('disconnect', () => {
|
newSocket.on('disconnect', () => {
|
||||||
logger.debug('Api Server disconnected')
|
logger.debug('Api Server disconnected')
|
||||||
setError('Api Server disconnected')
|
setError('Api Server disconnected')
|
||||||
|
clearSubscriptions()
|
||||||
setConnected(false)
|
setConnected(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -108,16 +114,10 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
logger.error('Api Server connection error:', err)
|
logger.error('Api Server connection error:', err)
|
||||||
messageApi.error('Api Server connection error: ' + err.message)
|
messageApi.error('Api Server connection error: ' + err.message)
|
||||||
setError('Api Server connection error')
|
setError('Api Server connection error')
|
||||||
|
clearSubscriptions()
|
||||||
setConnected(false)
|
setConnected(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
newSocket.on('bridge.notification', (data) => {
|
|
||||||
notificationApi[data.type]({
|
|
||||||
title: data.title,
|
|
||||||
message: data.message
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
newSocket.on('error', (err) => {
|
newSocket.on('error', (err) => {
|
||||||
logger.error('Api Server error:', err)
|
logger.error('Api Server error:', err)
|
||||||
setError('Api Server error')
|
setError('Api Server error')
|
||||||
@ -445,6 +445,7 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
(id, objectType, eventType, callback) => {
|
(id, objectType, eventType, callback) => {
|
||||||
if (socketRef.current && socketRef.current.connected == true) {
|
if (socketRef.current && socketRef.current.connected == true) {
|
||||||
const callbacksRefKey = `${objectType}:${id}:events:${eventType}`
|
const callbacksRefKey = `${objectType}:${id}:events:${eventType}`
|
||||||
|
|
||||||
// Remove callback from the subscribed callbacks map
|
// Remove callback from the subscribed callbacks map
|
||||||
if (subscribedCallbacksRef.current.has(callbacksRefKey)) {
|
if (subscribedCallbacksRef.current.has(callbacksRefKey)) {
|
||||||
const callbacks = subscribedCallbacksRef.current
|
const callbacks = subscribedCallbacksRef.current
|
||||||
@ -452,6 +453,7 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
.filter((cb) => cb !== callback)
|
.filter((cb) => cb !== callback)
|
||||||
if (callbacks.length === 0) {
|
if (callbacks.length === 0) {
|
||||||
subscribedCallbacksRef.current.delete(callbacksRefKey)
|
subscribedCallbacksRef.current.delete(callbacksRefKey)
|
||||||
|
console.log('Unsubscribing from object event:', callbacksRefKey)
|
||||||
socketRef.current.emit('unsubscribeObjectEvent', {
|
socketRef.current.emit('unsubscribeObjectEvent', {
|
||||||
_id: id,
|
_id: id,
|
||||||
objectType,
|
objectType,
|
||||||
@ -479,6 +481,7 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
subscribedCallbacksRef.current.get(callbacksRefKey).length
|
subscribedCallbacksRef.current.get(callbacksRefKey).length
|
||||||
|
|
||||||
if (callbacksLength <= 0) {
|
if (callbacksLength <= 0) {
|
||||||
|
console.log('Subscribing to object event:', callbacksRefKey)
|
||||||
socketRef.current.emit(
|
socketRef.current.emit(
|
||||||
'subscribeToObjectEvent',
|
'subscribeToObjectEvent',
|
||||||
{
|
{
|
||||||
@ -662,7 +665,12 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
`${config.backendUrl}/${type.toLowerCase()}s/properties`,
|
`${config.backendUrl}/${type.toLowerCase()}s/properties`,
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
...filter,
|
...Object.keys(filter).reduce((acc, key) => {
|
||||||
|
acc[key] = Array.isArray(filter[key])
|
||||||
|
? filter[key].join(',')
|
||||||
|
: filter[key]
|
||||||
|
return acc
|
||||||
|
}, {}),
|
||||||
properties: properties.join(','), // Convert array to comma-separated string
|
properties: properties.join(','), // Convert array to comma-separated string
|
||||||
masterFilter: JSON.stringify(masterFilter)
|
masterFilter: JSON.stringify(masterFilter)
|
||||||
},
|
},
|
||||||
@ -932,7 +940,11 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Upload file to the API
|
// Upload file to the API
|
||||||
const uploadFile = async (file, additionalData = {}) => {
|
const uploadFile = async (
|
||||||
|
file,
|
||||||
|
additionalData = {},
|
||||||
|
progressCallback = null
|
||||||
|
) => {
|
||||||
const uploadUrl = `${config.backendUrl}/files`
|
const uploadUrl = `${config.backendUrl}/files`
|
||||||
logger.debug('Uploading file:', file.name, 'to:', uploadUrl)
|
logger.debug('Uploading file:', file.name, 'to:', uploadUrl)
|
||||||
|
|
||||||
@ -955,6 +967,9 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
(progressEvent.loaded * 100) / progressEvent.total
|
(progressEvent.loaded * 100) / progressEvent.total
|
||||||
)
|
)
|
||||||
logger.debug(`Upload progress: ${percentCompleted}%`)
|
logger.debug(`Upload progress: ${percentCompleted}%`)
|
||||||
|
if (progressCallback) {
|
||||||
|
progressCallback(percentCompleted)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -963,7 +978,7 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('File upload error:', err)
|
console.error('File upload error:', err)
|
||||||
showError(err, () => {
|
showError(err, () => {
|
||||||
uploadFile(file, additionalData)
|
uploadFile(file, additionalData, progressCallback)
|
||||||
})
|
})
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
58
src/components/Dashboard/context/MessageContext.jsx
Normal file
58
src/components/Dashboard/context/MessageContext.jsx
Normal 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 }
|
||||||
6
src/components/Icons/CourierIcon.jsx
Normal file
6
src/components/Icons/CourierIcon.jsx
Normal 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
|
||||||
7
src/components/Icons/CourierServiceIcon.jsx
Normal file
7
src/components/Icons/CourierServiceIcon.jsx
Normal 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
|
||||||
|
|
||||||
6
src/components/Icons/OrderItemsIcon.jsx
Normal file
6
src/components/Icons/OrderItemsIcon.jsx
Normal 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
|
||||||
8
src/components/Icons/PurchaseOrderIcon.jsx
Normal file
8
src/components/Icons/PurchaseOrderIcon.jsx
Normal 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
|
||||||
@ -7,6 +7,8 @@ import { Job } from './models/Job'
|
|||||||
import { Product } from './models/Product'
|
import { Product } from './models/Product'
|
||||||
import { Part } from './models/Part.js'
|
import { Part } from './models/Part.js'
|
||||||
import { Vendor } from './models/Vendor'
|
import { Vendor } from './models/Vendor'
|
||||||
|
import { Courier } from './models/Courier'
|
||||||
|
import { CourierService } from './models/CourierService'
|
||||||
import { File } from './models/File'
|
import { File } from './models/File'
|
||||||
import { SubJob } from './models/SubJob'
|
import { SubJob } from './models/SubJob'
|
||||||
import { Initial } from './models/Initial'
|
import { Initial } from './models/Initial'
|
||||||
@ -15,6 +17,7 @@ import { StockEvent } from './models/StockEvent'
|
|||||||
import { StockAudit } from './models/StockAudit'
|
import { StockAudit } from './models/StockAudit'
|
||||||
import { PartStock } from './models/PartStock'
|
import { PartStock } from './models/PartStock'
|
||||||
import { ProductStock } from './models/ProductStock'
|
import { ProductStock } from './models/ProductStock'
|
||||||
|
import { PurchaseOrder } from './models/PurchaseOrder'
|
||||||
import { AuditLog } from './models/AuditLog'
|
import { AuditLog } from './models/AuditLog'
|
||||||
import { User } from './models/User'
|
import { User } from './models/User'
|
||||||
import { NoteType } from './models/NoteType'
|
import { NoteType } from './models/NoteType'
|
||||||
@ -35,6 +38,8 @@ export const objectModels = [
|
|||||||
Product,
|
Product,
|
||||||
Part,
|
Part,
|
||||||
Vendor,
|
Vendor,
|
||||||
|
Courier,
|
||||||
|
CourierService,
|
||||||
File,
|
File,
|
||||||
SubJob,
|
SubJob,
|
||||||
Initial,
|
Initial,
|
||||||
@ -43,6 +48,7 @@ export const objectModels = [
|
|||||||
StockAudit,
|
StockAudit,
|
||||||
PartStock,
|
PartStock,
|
||||||
ProductStock,
|
ProductStock,
|
||||||
|
PurchaseOrder,
|
||||||
AuditLog,
|
AuditLog,
|
||||||
User,
|
User,
|
||||||
NoteType,
|
NoteType,
|
||||||
@ -64,6 +70,8 @@ export {
|
|||||||
Product,
|
Product,
|
||||||
Part,
|
Part,
|
||||||
Vendor,
|
Vendor,
|
||||||
|
Courier,
|
||||||
|
CourierService,
|
||||||
File,
|
File,
|
||||||
SubJob,
|
SubJob,
|
||||||
Initial,
|
Initial,
|
||||||
@ -72,6 +80,7 @@ export {
|
|||||||
StockAudit,
|
StockAudit,
|
||||||
PartStock,
|
PartStock,
|
||||||
ProductStock,
|
ProductStock,
|
||||||
|
PurchaseOrder,
|
||||||
AuditLog,
|
AuditLog,
|
||||||
User,
|
User,
|
||||||
NoteType,
|
NoteType,
|
||||||
|
|||||||
142
src/database/models/Courier.js
Normal file
142
src/database/models/Courier.js
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
170
src/database/models/CourierService.js
Normal file
170
src/database/models/CourierService.js
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -1,6 +1,8 @@
|
|||||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||||
import EditIcon from '../../components/Icons/EditIcon'
|
import EditIcon from '../../components/Icons/EditIcon'
|
||||||
|
import CheckIcon from '../../components/Icons/CheckIcon'
|
||||||
|
import XMarkIcon from '../../components/Icons/XMarkIcon'
|
||||||
import DocumentJobIcon from '../../components/Icons/DocumentJobIcon'
|
import DocumentJobIcon from '../../components/Icons/DocumentJobIcon'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
@ -33,7 +35,31 @@ export const DocumentJob = {
|
|||||||
row: true,
|
row: true,
|
||||||
icon: EditIcon,
|
icon: EditIcon,
|
||||||
url: (_id) =>
|
url: (_id) =>
|
||||||
`/dashboard/management/documentjobs/info?documentJobId=${_id}&action=edit`
|
`/dashboard/management/documentjobs/info?documentJobId=${_id}&action=edit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
return !(objectData?._isEditing && objectData?._isEditing == true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'finishEdit',
|
||||||
|
label: 'Save Edits',
|
||||||
|
icon: CheckIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/documentjobs/info?documentJobId=${_id}&action=finishEdit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
return objectData?._isEditing && objectData?._isEditing == true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cancelEdit',
|
||||||
|
label: 'Cancel Edits',
|
||||||
|
icon: XMarkIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/documentjobs/info?documentJobId=${_id}&action=cancelEdit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
console.log(objectData?._isEditing)
|
||||||
|
return objectData?._isEditing && objectData?._isEditing == true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
columns: ['name', '_id', 'state', 'createdAt', 'updatedAt'],
|
columns: ['name', '_id', 'state', 'createdAt', 'updatedAt'],
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
|||||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||||
import EditIcon from '../../components/Icons/EditIcon'
|
import EditIcon from '../../components/Icons/EditIcon'
|
||||||
import DocumentPrinterIcon from '../../components/Icons/DocumentPrinterIcon'
|
import DocumentPrinterIcon from '../../components/Icons/DocumentPrinterIcon'
|
||||||
|
import CheckIcon from '../../components/Icons/CheckIcon'
|
||||||
|
import XMarkIcon from '../../components/Icons/XMarkIcon'
|
||||||
|
|
||||||
export const DocumentPrinter = {
|
export const DocumentPrinter = {
|
||||||
name: 'documentPrinter',
|
name: 'documentPrinter',
|
||||||
@ -32,7 +34,31 @@ export const DocumentPrinter = {
|
|||||||
row: true,
|
row: true,
|
||||||
icon: EditIcon,
|
icon: EditIcon,
|
||||||
url: (_id) =>
|
url: (_id) =>
|
||||||
`/dashboard/management/documentprinters/info?documentPrinterId=${_id}&action=edit`
|
`/dashboard/management/documentprinters/info?documentPrinterId=${_id}&action=edit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
return !(objectData?._isEditing && objectData?._isEditing == true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'finishEdit',
|
||||||
|
label: 'Save Edits',
|
||||||
|
icon: CheckIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/documentprinters/info?documentPrinterId=${_id}&action=finishEdit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
return objectData?._isEditing && objectData?._isEditing == true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cancelEdit',
|
||||||
|
label: 'Cancel Edits',
|
||||||
|
icon: XMarkIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/documentprinters/info?documentPrinterId=${_id}&action=cancelEdit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
console.log(objectData?._isEditing)
|
||||||
|
return objectData?._isEditing && objectData?._isEditing == true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
columns: [
|
columns: [
|
||||||
@ -101,6 +127,21 @@ export const DocumentPrinter = {
|
|||||||
type: 'bool',
|
type: 'bool',
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'vendor',
|
||||||
|
label: 'Vendor',
|
||||||
|
type: 'object',
|
||||||
|
objectType: 'vendor',
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'vendor._id',
|
||||||
|
label: 'Vendor ID',
|
||||||
|
type: 'id',
|
||||||
|
objectType: 'vendor',
|
||||||
|
showHyperlink: true,
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'host',
|
name: 'host',
|
||||||
label: 'Host',
|
label: 'Host',
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||||
import EditIcon from '../../components/Icons/EditIcon'
|
import EditIcon from '../../components/Icons/EditIcon'
|
||||||
|
import CheckIcon from '../../components/Icons/CheckIcon'
|
||||||
|
import XMarkIcon from '../../components/Icons/XMarkIcon'
|
||||||
import DocumentSizeIcon from '../../components/Icons/DocumentSizeIcon'
|
import DocumentSizeIcon from '../../components/Icons/DocumentSizeIcon'
|
||||||
|
|
||||||
export const DocumentSize = {
|
export const DocumentSize = {
|
||||||
@ -32,7 +34,31 @@ export const DocumentSize = {
|
|||||||
row: true,
|
row: true,
|
||||||
icon: EditIcon,
|
icon: EditIcon,
|
||||||
url: (_id) =>
|
url: (_id) =>
|
||||||
`/dashboard/management/documentsizes/info?documentSizeId=${_id}&action=edit`
|
`/dashboard/management/documentsizes/info?documentSizeId=${_id}&action=edit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
return !(objectData?._isEditing && objectData?._isEditing == true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'finishEdit',
|
||||||
|
label: 'Save Edits',
|
||||||
|
icon: CheckIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/documentsizes/info?documentSizeId=${_id}&action=finishEdit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
return objectData?._isEditing && objectData?._isEditing == true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cancelEdit',
|
||||||
|
label: 'Cancel Edits',
|
||||||
|
icon: XMarkIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/documentsizes/info?documentSizeId=${_id}&action=cancelEdit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
console.log(objectData?._isEditing)
|
||||||
|
return objectData?._isEditing && objectData?._isEditing == true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
columns: [
|
columns: [
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||||
import EditIcon from '../../components/Icons/EditIcon'
|
import EditIcon from '../../components/Icons/EditIcon'
|
||||||
|
import CheckIcon from '../../components/Icons/CheckIcon'
|
||||||
|
import XMarkIcon from '../../components/Icons/XMarkIcon'
|
||||||
import DesignIcon from '../../components/Icons/DesignIcon'
|
import DesignIcon from '../../components/Icons/DesignIcon'
|
||||||
import DocumentTemplateIcon from '../../components/Icons/DocumentTemplateIcon'
|
import DocumentTemplateIcon from '../../components/Icons/DocumentTemplateIcon'
|
||||||
|
|
||||||
@ -41,7 +43,31 @@ export const DocumentTemplate = {
|
|||||||
row: true,
|
row: true,
|
||||||
icon: EditIcon,
|
icon: EditIcon,
|
||||||
url: (_id) =>
|
url: (_id) =>
|
||||||
`/dashboard/management/documenttemplates/info?documentTemplateId=${_id}&action=edit`
|
`/dashboard/management/documenttemplates/info?documentTemplateId=${_id}&action=edit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
return !(objectData?._isEditing && objectData?._isEditing == true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'finishEdit',
|
||||||
|
label: 'Save Edits',
|
||||||
|
icon: CheckIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/documenttemplates/info?documentTemplateId=${_id}&action=finishEdit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
return objectData?._isEditing && objectData?._isEditing == true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cancelEdit',
|
||||||
|
label: 'Cancel Edits',
|
||||||
|
icon: XMarkIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/documenttemplates/info?documentTemplateId=${_id}&action=cancelEdit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
console.log(objectData?._isEditing)
|
||||||
|
return objectData?._isEditing && objectData?._isEditing == true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
columns: [
|
columns: [
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import EditIcon from '../../components/Icons/EditIcon'
|
|||||||
import FilamentIcon from '../../components/Icons/FilamentIcon'
|
import FilamentIcon from '../../components/Icons/FilamentIcon'
|
||||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||||
|
import CheckIcon from '../../components/Icons/CheckIcon'
|
||||||
|
import XMarkIcon from '../../components/Icons/XMarkIcon'
|
||||||
|
|
||||||
export const Filament = {
|
export const Filament = {
|
||||||
name: 'filament',
|
name: 'filament',
|
||||||
@ -30,7 +32,31 @@ export const Filament = {
|
|||||||
row: true,
|
row: true,
|
||||||
icon: EditIcon,
|
icon: EditIcon,
|
||||||
url: (_id) =>
|
url: (_id) =>
|
||||||
`/dashboard/management/filaments/info?filamentId=${_id}&action=edit`
|
`/dashboard/management/filaments/info?filamentId=${_id}&action=edit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
return !(objectData?._isEditing && objectData?._isEditing == true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'finishEdit',
|
||||||
|
label: 'Save Edits',
|
||||||
|
icon: CheckIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/filaments/info?filamentId=${_id}&action=finishEdit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
return objectData?._isEditing && objectData?._isEditing == true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cancelEdit',
|
||||||
|
label: 'Cancel Edits',
|
||||||
|
icon: XMarkIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/filaments/info?filamentId=${_id}&action=cancelEdit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
console.log(objectData?._isEditing)
|
||||||
|
return objectData?._isEditing && objectData?._isEditing == true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
columns: [
|
columns: [
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import DownloadIcon from '../../components/Icons/DownloadIcon'
|
|||||||
import FileIcon from '../../components/Icons/FileIcon'
|
import FileIcon from '../../components/Icons/FileIcon'
|
||||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
import EditIcon from '../../components/Icons/EditIcon'
|
import EditIcon from '../../components/Icons/EditIcon'
|
||||||
|
import CheckIcon from '../../components/Icons/CheckIcon'
|
||||||
|
import XMarkIcon from '../../components/Icons/XMarkIcon'
|
||||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||||
import BinIcon from '../../components/Icons/BinIcon'
|
import BinIcon from '../../components/Icons/BinIcon'
|
||||||
|
|
||||||
@ -31,7 +33,31 @@ export const File = {
|
|||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
row: true,
|
row: true,
|
||||||
icon: EditIcon,
|
icon: EditIcon,
|
||||||
url: (_id) => `/dashboard/management/files/info?fileId=${_id}&action=edit`
|
url: (_id) => `/dashboard/management/files/info?fileId=${_id}&action=edit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
return !(objectData?._isEditing && objectData?._isEditing == true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'finishEdit',
|
||||||
|
label: 'Save Edits',
|
||||||
|
icon: CheckIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/files/info?fileId=${_id}&action=finishEdit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
return objectData?._isEditing && objectData?._isEditing == true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cancelEdit',
|
||||||
|
label: 'Cancel Edits',
|
||||||
|
icon: XMarkIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/files/info?fileId=${_id}&action=cancelEdit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
console.log(objectData?._isEditing)
|
||||||
|
return objectData?._isEditing && objectData?._isEditing == true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'download',
|
name: 'download',
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import DownloadIcon from '../../components/Icons/DownloadIcon'
|
import DownloadIcon from '../../components/Icons/DownloadIcon'
|
||||||
import EditIcon from '../../components/Icons/EditIcon'
|
import EditIcon from '../../components/Icons/EditIcon'
|
||||||
|
import CheckIcon from '../../components/Icons/CheckIcon'
|
||||||
|
import XMarkIcon from '../../components/Icons/XMarkIcon'
|
||||||
import GCodeFileIcon from '../../components/Icons/GCodeFileIcon'
|
import GCodeFileIcon from '../../components/Icons/GCodeFileIcon'
|
||||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||||
@ -39,7 +41,31 @@ export const GCodeFile = {
|
|||||||
row: true,
|
row: true,
|
||||||
icon: EditIcon,
|
icon: EditIcon,
|
||||||
url: (_id) =>
|
url: (_id) =>
|
||||||
`/dashboard/production/gcodefiles/info?gcodeFileId=${_id}&action=edit`
|
`/dashboard/production/gcodefiles/info?gcodeFileId=${_id}&action=edit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
return !(objectData?._isEditing && objectData?._isEditing == true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'finishEdit',
|
||||||
|
label: 'Save Edits',
|
||||||
|
icon: CheckIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/production/gcodefiles/info?gcodeFileId=${_id}&action=finishEdit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
return objectData?._isEditing && objectData?._isEditing == true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cancelEdit',
|
||||||
|
label: 'Cancel Edits',
|
||||||
|
icon: XMarkIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/production/gcodefiles/info?gcodeFileId=${_id}&action=cancelEdit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
console.log(objectData?._isEditing)
|
||||||
|
return objectData?._isEditing && objectData?._isEditing == true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
@ -127,9 +153,11 @@ export const GCodeFile = {
|
|||||||
name: 'cost',
|
name: 'cost',
|
||||||
label: 'Cost',
|
label: 'Cost',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
roundNumber: 2,
|
||||||
value: (objectData) => {
|
value: (objectData) => {
|
||||||
return (
|
return (
|
||||||
objectData?.file?.metaData?.filamentUsedG * objectData?.filament?.cost
|
objectData?.file?.metaData?.filamentUsedG *
|
||||||
|
(objectData?.filament?.cost / 1000)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
@ -196,6 +224,51 @@ export const GCodeFile = {
|
|||||||
label: 'Print Profile',
|
label: 'Print Profile',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'parts',
|
||||||
|
label: 'Parts',
|
||||||
|
type: 'objectChildren',
|
||||||
|
objectType: 'part',
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
name: 'part',
|
||||||
|
label: 'Part',
|
||||||
|
type: 'object',
|
||||||
|
objectType: 'part',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'part._id',
|
||||||
|
label: 'Part ID',
|
||||||
|
type: 'id',
|
||||||
|
objectType: 'part',
|
||||||
|
showHyperlink: true,
|
||||||
|
value: (objectData) => {
|
||||||
|
return objectData?.part?._id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'quantity',
|
||||||
|
label: 'Quantity',
|
||||||
|
type: 'number',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
rollups: [
|
||||||
|
{
|
||||||
|
name: 'totalQuantity',
|
||||||
|
label: 'Total',
|
||||||
|
type: 'number',
|
||||||
|
property: 'quantity',
|
||||||
|
value: (objectData) => {
|
||||||
|
return objectData?.parts?.reduce(
|
||||||
|
(acc, part) => acc + part.quantity,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import HostIcon from '../../components/Icons/HostIcon'
|
|||||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||||
import EditIcon from '../../components/Icons/EditIcon'
|
import EditIcon from '../../components/Icons/EditIcon'
|
||||||
|
import CheckIcon from '../../components/Icons/CheckIcon'
|
||||||
|
import XMarkIcon from '../../components/Icons/XMarkIcon'
|
||||||
import OTPIcon from '../../components/Icons/OTPIcon'
|
import OTPIcon from '../../components/Icons/OTPIcon'
|
||||||
|
|
||||||
export const Host = {
|
export const Host = {
|
||||||
@ -38,7 +40,32 @@ export const Host = {
|
|||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
row: true,
|
row: true,
|
||||||
icon: EditIcon,
|
icon: EditIcon,
|
||||||
url: (_id) => `/dashboard/management/hosts/info?hostId=${_id}&action=edit`
|
url: (_id) =>
|
||||||
|
`/dashboard/management/hosts/info?hostId=${_id}&action=edit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
return !(objectData?._isEditing && objectData?._isEditing == true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'finishEdit',
|
||||||
|
label: 'Save Edits',
|
||||||
|
icon: CheckIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/hosts/info?hostId=${_id}&action=finishEdit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
return objectData?._isEditing && objectData?._isEditing == true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cancelEdit',
|
||||||
|
label: 'Cancel Edits',
|
||||||
|
icon: XMarkIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/hosts/info?hostId=${_id}&action=cancelEdit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
console.log(objectData?._isEditing)
|
||||||
|
return objectData?._isEditing && objectData?._isEditing == true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
columns: ['name', '_id', 'state', 'tags', 'connectedAt'],
|
columns: ['name', '_id', 'state', 'tags', 'connectedAt'],
|
||||||
@ -54,8 +81,8 @@ export const Host = {
|
|||||||
showCopy: true
|
showCopy: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'connectedAt',
|
name: 'createdAt',
|
||||||
label: 'Connected At',
|
label: 'Created At',
|
||||||
type: 'dateTime',
|
type: 'dateTime',
|
||||||
readOnly: true
|
readOnly: true
|
||||||
},
|
},
|
||||||
@ -67,6 +94,12 @@ export const Host = {
|
|||||||
columnWidth: 200,
|
columnWidth: 200,
|
||||||
columnFixed: 'left'
|
columnFixed: 'left'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'updatedAt',
|
||||||
|
label: 'Updated At',
|
||||||
|
type: 'dateTime',
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'state',
|
name: 'state',
|
||||||
label: 'State',
|
label: 'State',
|
||||||
@ -76,10 +109,10 @@ export const Host = {
|
|||||||
readOnly: true
|
readOnly: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'active',
|
name: 'connectedAt',
|
||||||
label: 'Active',
|
label: 'Connected At',
|
||||||
type: 'bool',
|
type: 'dateTime',
|
||||||
required: true
|
readOnly: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'online',
|
name: 'online',
|
||||||
@ -87,6 +120,13 @@ export const Host = {
|
|||||||
type: 'bool',
|
type: 'bool',
|
||||||
readOnly: true
|
readOnly: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'active',
|
||||||
|
label: 'Active',
|
||||||
|
type: 'bool',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'deviceInfo.os',
|
name: 'deviceInfo.os',
|
||||||
label: 'Operating System',
|
label: 'Operating System',
|
||||||
@ -158,6 +198,14 @@ export const Host = {
|
|||||||
label: 'Tags',
|
label: 'Tags',
|
||||||
type: 'tags',
|
type: 'tags',
|
||||||
required: false
|
required: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'files',
|
||||||
|
label: 'Files',
|
||||||
|
type: 'objectList',
|
||||||
|
objectType: 'file',
|
||||||
|
required: false,
|
||||||
|
readOnly: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,6 +56,12 @@ export const Job = {
|
|||||||
objectType: 'job',
|
objectType: 'job',
|
||||||
showCopy: true
|
showCopy: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'createdAt',
|
||||||
|
label: 'Created At',
|
||||||
|
type: 'dateTime',
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'state',
|
name: 'state',
|
||||||
label: 'State',
|
label: 'State',
|
||||||
@ -65,7 +71,39 @@ export const Job = {
|
|||||||
showProgress: true,
|
showProgress: true,
|
||||||
showId: false,
|
showId: false,
|
||||||
showQuantity: false,
|
showQuantity: false,
|
||||||
columnWidth: 150,
|
columnWidth: 250,
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'updatedAt',
|
||||||
|
label: 'Updated At',
|
||||||
|
type: 'dateTime',
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'quantity',
|
||||||
|
label: 'Quantity',
|
||||||
|
type: 'number',
|
||||||
|
columnWidth: 125,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'startedAt',
|
||||||
|
label: 'Started At',
|
||||||
|
type: 'dateTime',
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'printers',
|
||||||
|
label: 'Printers',
|
||||||
|
type: 'objectList',
|
||||||
|
objectType: 'printer',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'finishedAt',
|
||||||
|
label: 'Finished At',
|
||||||
|
type: 'dateTime',
|
||||||
readOnly: true
|
readOnly: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -82,33 +120,6 @@ export const Job = {
|
|||||||
type: 'id',
|
type: 'id',
|
||||||
objectType: 'gcodeFile',
|
objectType: 'gcodeFile',
|
||||||
showHyperlink: true
|
showHyperlink: true
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'quantity',
|
|
||||||
label: 'Quantity',
|
|
||||||
type: 'number',
|
|
||||||
columnWidth: 125,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'createdAt',
|
|
||||||
label: 'Created At',
|
|
||||||
type: 'dateTime',
|
|
||||||
readOnly: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'startedAt',
|
|
||||||
label: 'Started At',
|
|
||||||
type: 'dateTime',
|
|
||||||
readOnly: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'printers',
|
|
||||||
label: 'Printers',
|
|
||||||
type: 'objectList',
|
|
||||||
objectType: 'printer',
|
|
||||||
required: true,
|
|
||||||
span: 2
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import NoteTypeIcon from '../../components/Icons/NoteTypeIcon'
|
|||||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||||
import EditIcon from '../../components/Icons/EditIcon'
|
import EditIcon from '../../components/Icons/EditIcon'
|
||||||
|
import CheckIcon from '../../components/Icons/CheckIcon'
|
||||||
|
import XMarkIcon from '../../components/Icons/XMarkIcon'
|
||||||
|
|
||||||
export const NoteType = {
|
export const NoteType = {
|
||||||
name: 'noteType',
|
name: 'noteType',
|
||||||
@ -30,7 +32,31 @@ export const NoteType = {
|
|||||||
row: true,
|
row: true,
|
||||||
icon: EditIcon,
|
icon: EditIcon,
|
||||||
url: (_id) =>
|
url: (_id) =>
|
||||||
`/dashboard/management/notetypes/info?noteTypeId=${_id}&action=edit`
|
`/dashboard/management/notetypes/info?noteTypeId=${_id}&action=edit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
return !(objectData?._isEditing && objectData?._isEditing == true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'finishEdit',
|
||||||
|
label: 'Save Edits',
|
||||||
|
icon: CheckIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/notetypes/info?noteTypeId=${_id}&action=finishEdit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
return objectData?._isEditing && objectData?._isEditing == true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cancelEdit',
|
||||||
|
label: 'Cancel Edits',
|
||||||
|
icon: XMarkIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/notetypes/info?noteTypeId=${_id}&action=cancelEdit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
console.log(objectData?._isEditing)
|
||||||
|
return objectData?._isEditing && objectData?._isEditing == true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
columns: ['name', '_id', 'color', 'active', 'createdAt', 'updatedAt'],
|
columns: ['name', '_id', 'color', 'active', 'createdAt', 'updatedAt'],
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import EditIcon from '../../components/Icons/EditIcon'
|
|||||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
import PartIcon from '../../components/Icons/PartIcon'
|
import PartIcon from '../../components/Icons/PartIcon'
|
||||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||||
|
import CheckIcon from '../../components/Icons/CheckIcon'
|
||||||
|
import XMarkIcon from '../../components/Icons/XMarkIcon'
|
||||||
|
|
||||||
export const Part = {
|
export const Part = {
|
||||||
name: 'part',
|
name: 'part',
|
||||||
@ -29,7 +31,32 @@ export const Part = {
|
|||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
row: true,
|
row: true,
|
||||||
icon: EditIcon,
|
icon: EditIcon,
|
||||||
url: (_id) => `/dashboard/management/parts/info?partId=${_id}&action=edit`
|
url: (_id) =>
|
||||||
|
`/dashboard/management/parts/info?partId=${_id}&action=edit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
return !(objectData?._isEditing && objectData?._isEditing == true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'finishEdit',
|
||||||
|
label: 'Save Edits',
|
||||||
|
icon: CheckIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/parts/info?partId=${_id}&action=finishEdit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
return objectData?._isEditing && objectData?._isEditing == true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cancelEdit',
|
||||||
|
label: 'Cancel Edits',
|
||||||
|
icon: XMarkIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/parts/info?partId=${_id}&action=cancelEdit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
console.log(objectData?._isEditing)
|
||||||
|
return objectData?._isEditing && objectData?._isEditing == true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
columns: [
|
columns: [
|
||||||
@ -71,22 +98,6 @@ export const Part = {
|
|||||||
type: 'dateTime',
|
type: 'dateTime',
|
||||||
readOnly: true
|
readOnly: true
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
name: 'product',
|
|
||||||
label: 'Product',
|
|
||||||
type: 'object',
|
|
||||||
required: true,
|
|
||||||
objectType: 'product'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'product._id',
|
|
||||||
label: 'Product ID',
|
|
||||||
type: 'id',
|
|
||||||
readOnly: true,
|
|
||||||
showHyperlink: true,
|
|
||||||
objectType: 'product'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'vendor',
|
name: 'vendor',
|
||||||
label: 'Vendor',
|
label: 'Vendor',
|
||||||
@ -111,11 +122,14 @@ export const Part = {
|
|||||||
objectType: 'vendor'
|
objectType: 'vendor'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'globalPricing',
|
name: 'cost',
|
||||||
label: 'Global Price',
|
label: 'Cost',
|
||||||
columnWidth: 150,
|
columnWidth: 150,
|
||||||
required: true,
|
required: true,
|
||||||
type: 'bool'
|
type: 'number',
|
||||||
|
prefix: '£',
|
||||||
|
min: 0,
|
||||||
|
step: 0.01
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'priceMode',
|
name: 'priceMode',
|
||||||
@ -142,18 +156,21 @@ export const Part = {
|
|||||||
step: 0.01
|
step: 0.01
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'amount',
|
name: 'price',
|
||||||
label: 'Amount',
|
label: 'Price',
|
||||||
required: true,
|
required: true,
|
||||||
disabled: (objectData) => {
|
|
||||||
return (
|
|
||||||
objectData.globalPricing == true || objectData.priceMode == 'margin'
|
|
||||||
)
|
|
||||||
},
|
|
||||||
type: 'number',
|
type: 'number',
|
||||||
prefix: '£',
|
prefix: '£',
|
||||||
min: 0,
|
min: 0,
|
||||||
step: 0.1
|
step: 0.1,
|
||||||
|
roundNumber: 2,
|
||||||
|
value: (objectData) => {
|
||||||
|
if (objectData?.priceMode == 'margin') {
|
||||||
|
return objectData?.cost * (1 + objectData?.margin / 100) || undefined
|
||||||
|
} else {
|
||||||
|
return objectData?.price || undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'file',
|
name: 'file',
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import PartStockIcon from '../../components/Icons/PartStockIcon'
|
|||||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
|
||||||
export const PartStock = {
|
export const PartStock = {
|
||||||
name: 'partstock',
|
name: 'partStock',
|
||||||
label: 'Part Stock',
|
label: 'Part Stock',
|
||||||
prefix: 'PTS',
|
prefix: 'PTS',
|
||||||
icon: PartStockIcon,
|
icon: PartStockIcon,
|
||||||
@ -13,8 +13,124 @@ export const PartStock = {
|
|||||||
default: true,
|
default: true,
|
||||||
row: true,
|
row: true,
|
||||||
icon: InfoCircleIcon,
|
icon: InfoCircleIcon,
|
||||||
url: (_id) => `/dashboard/management/partstocks/info?partStockId=${_id}`
|
url: (_id) => `/dashboard/inventory/partstocks/info?partStockId=${_id}`
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
url: (id) => `/dashboard/management/partstocks/info?partStockId=${id}`
|
url: (id) => `/dashboard/inventory/partstocks/info?partStockId=${id}`,
|
||||||
|
filters: ['_id', 'part', 'startingQuantity', 'currentQuantity'],
|
||||||
|
sorters: ['part', 'startingQuantity', 'currentQuantity'],
|
||||||
|
columns: [
|
||||||
|
'_id',
|
||||||
|
'state',
|
||||||
|
'startingQuantity',
|
||||||
|
'currentQuantity',
|
||||||
|
'part',
|
||||||
|
'part._id',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt'
|
||||||
|
],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
name: '_id',
|
||||||
|
label: 'ID',
|
||||||
|
type: 'id',
|
||||||
|
objectType: 'partStock',
|
||||||
|
showCopy: true,
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'createdAt',
|
||||||
|
label: 'Created At',
|
||||||
|
type: 'dateTime',
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'state',
|
||||||
|
label: 'State',
|
||||||
|
type: 'state',
|
||||||
|
readOnly: true,
|
||||||
|
columnWidth: 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'updatedAt',
|
||||||
|
label: 'Updated At',
|
||||||
|
type: 'dateTime',
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sourceType',
|
||||||
|
label: 'Source Type',
|
||||||
|
type: 'objectType',
|
||||||
|
readOnly: false,
|
||||||
|
columnWidth: 200,
|
||||||
|
required: true,
|
||||||
|
masterFilter: ['subJob']
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'consumedAt',
|
||||||
|
label: 'Consumed At',
|
||||||
|
type: 'dateTime',
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'part',
|
||||||
|
label: 'Part',
|
||||||
|
type: 'object',
|
||||||
|
objectType: 'part',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'part._id',
|
||||||
|
label: 'Part ID',
|
||||||
|
type: 'id',
|
||||||
|
objectType: 'part',
|
||||||
|
readOnly: true,
|
||||||
|
showHyperlink: true
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'source',
|
||||||
|
label: 'Source',
|
||||||
|
type: 'object',
|
||||||
|
readOnly: false,
|
||||||
|
required: true,
|
||||||
|
columnWidth: 200,
|
||||||
|
objectType: (objectData) => {
|
||||||
|
return objectData?.sourceType
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'source._id',
|
||||||
|
label: 'Source ID',
|
||||||
|
type: 'id',
|
||||||
|
readOnly: true,
|
||||||
|
columnWidth: 200,
|
||||||
|
objectType: (objectData) => {
|
||||||
|
return objectData?.sourceType
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'currentQuantity',
|
||||||
|
label: 'Current Quantity',
|
||||||
|
type: 'number',
|
||||||
|
readOnly: true,
|
||||||
|
columnWidth: 200,
|
||||||
|
required: true,
|
||||||
|
value: (objectData) => {
|
||||||
|
if (objectData?.state?.type === 'new') {
|
||||||
|
return objectData?.startingQuantity
|
||||||
|
} else {
|
||||||
|
return objectData.currentQuantity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'startingQuantity',
|
||||||
|
label: 'Starting Quantity',
|
||||||
|
type: 'number',
|
||||||
|
columnWidth: 200,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,10 +2,14 @@ import PrinterIcon from '../../components/Icons/PrinterIcon'
|
|||||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||||
import EditIcon from '../../components/Icons/EditIcon'
|
import EditIcon from '../../components/Icons/EditIcon'
|
||||||
|
import CheckIcon from '../../components/Icons/CheckIcon'
|
||||||
|
import XMarkIcon from '../../components/Icons/XMarkIcon'
|
||||||
import PlayCircleIcon from '../../components/Icons/PlayCircleIcon'
|
import PlayCircleIcon from '../../components/Icons/PlayCircleIcon'
|
||||||
import PauseCircleIcon from '../../components/Icons/PauseCircleIcon'
|
import PauseCircleIcon from '../../components/Icons/PauseCircleIcon'
|
||||||
import StopCircleIcon from '../../components/Icons/StopCircleIcon'
|
import StopCircleIcon from '../../components/Icons/StopCircleIcon'
|
||||||
import FilamentStockIcon from '../../components/Icons/FilamentStockIcon'
|
import FilamentStockIcon from '../../components/Icons/FilamentStockIcon'
|
||||||
|
import ControlIcon from '../../components/Icons/ControlIcon'
|
||||||
|
|
||||||
export const Printer = {
|
export const Printer = {
|
||||||
name: 'printer',
|
name: 'printer',
|
||||||
label: 'Printer',
|
label: 'Printer',
|
||||||
@ -32,7 +36,7 @@ export const Printer = {
|
|||||||
name: 'control',
|
name: 'control',
|
||||||
label: 'Control',
|
label: 'Control',
|
||||||
row: true,
|
row: true,
|
||||||
icon: PlayCircleIcon,
|
icon: ControlIcon,
|
||||||
url: (_id) => `/dashboard/production/printers/control?printerId=${_id}`
|
url: (_id) => `/dashboard/production/printers/control?printerId=${_id}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -41,7 +45,31 @@ export const Printer = {
|
|||||||
row: true,
|
row: true,
|
||||||
icon: EditIcon,
|
icon: EditIcon,
|
||||||
url: (_id) =>
|
url: (_id) =>
|
||||||
`/dashboard/production/printers/info?printerId=${_id}&action=edit`
|
`/dashboard/production/printers/info?printerId=${_id}&action=edit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
return !(objectData?._isEditing && objectData?._isEditing == true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'finishEdit',
|
||||||
|
label: 'Save Edits',
|
||||||
|
icon: CheckIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/production/printers/info?printerId=${_id}&action=finishEdit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
return objectData?._isEditing && objectData?._isEditing == true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cancelEdit',
|
||||||
|
label: 'Cancel Edits',
|
||||||
|
icon: XMarkIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/production/printers/info?printerId=${_id}&action=cancelEdit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
console.log(objectData?._isEditing)
|
||||||
|
return objectData?._isEditing && objectData?._isEditing == true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
@ -98,12 +126,13 @@ export const Printer = {
|
|||||||
label: 'Start Queue',
|
label: 'Start Queue',
|
||||||
icon: PlayCircleIcon,
|
icon: PlayCircleIcon,
|
||||||
disabled: (objectData) => {
|
disabled: (objectData) => {
|
||||||
console.log(objectData?.subJobs?.length)
|
console.log(objectData?.queue?.length)
|
||||||
return (
|
return (
|
||||||
objectData?.state?.type == 'error' ||
|
objectData?.state?.type == 'error' ||
|
||||||
objectData?.state?.type == 'printing' ||
|
objectData?.state?.type == 'printing' ||
|
||||||
objectData?.subJobs?.length == 0 ||
|
objectData?.state?.type == 'paused' ||
|
||||||
objectData?.subJobs?.length == undefined
|
objectData?.queue?.length == 0 ||
|
||||||
|
objectData?.queue?.length == undefined
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
url: (_id) =>
|
url: (_id) =>
|
||||||
@ -125,7 +154,7 @@ export const Printer = {
|
|||||||
label: 'Resume Job',
|
label: 'Resume Job',
|
||||||
icon: PlayCircleIcon,
|
icon: PlayCircleIcon,
|
||||||
disabled: (objectData) => {
|
disabled: (objectData) => {
|
||||||
return objectData?.state?.type != 'printing'
|
return objectData?.state?.type != 'paused'
|
||||||
},
|
},
|
||||||
url: (_id) =>
|
url: (_id) =>
|
||||||
`/dashboard/production/printers/control?printerId=${_id}&action=resumeJob`
|
`/dashboard/production/printers/control?printerId=${_id}&action=resumeJob`
|
||||||
@ -137,7 +166,7 @@ export const Printer = {
|
|||||||
disabled: (objectData) => {
|
disabled: (objectData) => {
|
||||||
return (
|
return (
|
||||||
objectData?.state?.type != 'printing' &&
|
objectData?.state?.type != 'printing' &&
|
||||||
objectData?.state?.type != 'error'
|
objectData?.state?.type != 'paused'
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
url: (_id) =>
|
url: (_id) =>
|
||||||
@ -149,20 +178,37 @@ export const Printer = {
|
|||||||
name: 'filamentStock',
|
name: 'filamentStock',
|
||||||
label: 'Filament Stock',
|
label: 'Filament Stock',
|
||||||
icon: FilamentStockIcon,
|
icon: FilamentStockIcon,
|
||||||
|
disabled: (objectData) => {
|
||||||
|
return objectData?.online == false
|
||||||
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name: 'loadFilamentStock',
|
name: 'loadFilamentStock',
|
||||||
label: 'Load Filament Stock',
|
label: 'Load Filament Stock',
|
||||||
icon: FilamentStockIcon,
|
icon: FilamentStockIcon,
|
||||||
url: (_id) =>
|
url: (_id) =>
|
||||||
`/dashboard/production/printers/control?printerId=${_id}&action=loadFilamentStock`
|
`/dashboard/production/printers/control?printerId=${_id}&action=loadFilamentStock`,
|
||||||
|
disabled: (objectData) => {
|
||||||
|
return (
|
||||||
|
objectData?.state?.type == 'printing' ||
|
||||||
|
objectData?.state?.type == 'error' ||
|
||||||
|
objectData?.currentFilamentStock != null
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'unloadFilamentStock',
|
name: 'unloadFilamentStock',
|
||||||
label: 'Unload Filament Stock',
|
label: 'Unload Filament Stock',
|
||||||
icon: FilamentStockIcon,
|
icon: FilamentStockIcon,
|
||||||
url: (_id) =>
|
url: (_id) =>
|
||||||
`/dashboard/production/printers/control?printerId=${_id}&action=unloadFilamentStock`
|
`/dashboard/production/printers/control?printerId=${_id}&action=unloadFilamentStock`,
|
||||||
|
disabled: (objectData) => {
|
||||||
|
return (
|
||||||
|
objectData?.state?.type == 'printing' ||
|
||||||
|
objectData?.state?.type == 'error' ||
|
||||||
|
objectData?.currentFilamentStock == null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -214,7 +260,8 @@ export const Printer = {
|
|||||||
type: 'state',
|
type: 'state',
|
||||||
objectType: 'printer',
|
objectType: 'printer',
|
||||||
showName: false,
|
showName: false,
|
||||||
readOnly: true
|
readOnly: true,
|
||||||
|
columnWidth: 250
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'connectedAt',
|
name: 'connectedAt',
|
||||||
@ -354,7 +401,7 @@ export const Printer = {
|
|||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'subJobs',
|
name: 'queue',
|
||||||
label: 'Queue',
|
label: 'Queue',
|
||||||
type: 'objectList',
|
type: 'objectList',
|
||||||
objectType: 'subJob',
|
objectType: 'subJob',
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import ProductIcon from '../../components/Icons/ProductIcon'
|
|||||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||||
import EditIcon from '../../components/Icons/EditIcon'
|
import EditIcon from '../../components/Icons/EditIcon'
|
||||||
|
import CheckIcon from '../../components/Icons/CheckIcon'
|
||||||
|
import XMarkIcon from '../../components/Icons/XMarkIcon'
|
||||||
|
|
||||||
export const Product = {
|
export const Product = {
|
||||||
name: 'product',
|
name: 'product',
|
||||||
@ -30,7 +32,31 @@ export const Product = {
|
|||||||
row: true,
|
row: true,
|
||||||
icon: EditIcon,
|
icon: EditIcon,
|
||||||
url: (_id) =>
|
url: (_id) =>
|
||||||
`/dashboard/management/products/info?productId=${_id}&action=edit`
|
`/dashboard/management/products/info?productId=${_id}&action=edit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
return !(objectData?._isEditing && objectData?._isEditing == true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'finishEdit',
|
||||||
|
label: 'Save Edits',
|
||||||
|
icon: CheckIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/products/info?productId=${_id}&action=finishEdit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
return objectData?._isEditing && objectData?._isEditing == true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cancelEdit',
|
||||||
|
label: 'Cancel Edits',
|
||||||
|
icon: XMarkIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/products/info?productId=${_id}&action=cancelEdit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
console.log(objectData?._isEditing)
|
||||||
|
return objectData?._isEditing && objectData?._isEditing == true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
columns: [
|
columns: [
|
||||||
@ -129,6 +155,51 @@ export const Product = {
|
|||||||
prefix: '£',
|
prefix: '£',
|
||||||
min: 0,
|
min: 0,
|
||||||
step: 0.1
|
step: 0.1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'parts',
|
||||||
|
label: 'Parts',
|
||||||
|
type: 'objectChildren',
|
||||||
|
objectType: 'part',
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
name: 'part',
|
||||||
|
label: 'Part',
|
||||||
|
type: 'object',
|
||||||
|
objectType: 'part',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'part._id',
|
||||||
|
label: 'Part ID',
|
||||||
|
type: 'id',
|
||||||
|
objectType: 'part',
|
||||||
|
showHyperlink: true,
|
||||||
|
value: (objectData) => {
|
||||||
|
return objectData?.part?._id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'quantity',
|
||||||
|
label: 'Quantity',
|
||||||
|
type: 'number',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
rollups: [
|
||||||
|
{
|
||||||
|
name: 'totalQuantity',
|
||||||
|
label: 'Total',
|
||||||
|
type: 'number',
|
||||||
|
property: 'quantity',
|
||||||
|
value: (objectData) => {
|
||||||
|
return objectData?.parts?.reduce(
|
||||||
|
(acc, part) => acc + part.quantity,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
180
src/database/models/PurchaseOrder.js
Normal file
180
src/database/models/PurchaseOrder.js
Normal 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@ import StockAuditIcon from '../../components/Icons/StockAuditIcon'
|
|||||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
|
||||||
export const StockAudit = {
|
export const StockAudit = {
|
||||||
name: 'stockaudit',
|
name: 'stockAudit',
|
||||||
label: 'Stock Audit',
|
label: 'Stock Audit',
|
||||||
prefix: 'SAU',
|
prefix: 'SAU',
|
||||||
icon: StockAuditIcon,
|
icon: StockAuditIcon,
|
||||||
@ -16,5 +16,38 @@ export const StockAudit = {
|
|||||||
url: (_id) => `/dashboard/inventory/stockaudits/info?stockAuditId=${_id}`
|
url: (_id) => `/dashboard/inventory/stockaudits/info?stockAuditId=${_id}`
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
url: (id) => `/dashboard/inventory/stockaudits/info?stockAuditId=${id}`
|
url: (id) => `/dashboard/inventory/stockaudits/info?stockAuditId=${id}`,
|
||||||
|
columns: ['_id', 'state', 'createdAt', 'updatedAt'],
|
||||||
|
filters: ['_id'],
|
||||||
|
sorters: ['createdAt', 'updatedAt'],
|
||||||
|
group: ['state'],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
name: '_id',
|
||||||
|
label: 'ID',
|
||||||
|
type: 'id',
|
||||||
|
objectType: 'stockAudit',
|
||||||
|
showCopy: true,
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'createdAt',
|
||||||
|
label: 'Created At',
|
||||||
|
type: 'dateTime',
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'state',
|
||||||
|
label: 'State',
|
||||||
|
type: 'state',
|
||||||
|
readOnly: true,
|
||||||
|
columnWidth: 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'updatedAt',
|
||||||
|
label: 'Updated At',
|
||||||
|
type: 'dateTime',
|
||||||
|
readOnly: true
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,37 @@
|
|||||||
import SubJobIcon from '../../components/Icons/SubJobIcon'
|
import SubJobIcon from '../../components/Icons/SubJobIcon'
|
||||||
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
|
import XMarkIcon from '../../components/Icons/XMarkIcon'
|
||||||
|
|
||||||
export const SubJob = {
|
export const SubJob = {
|
||||||
name: 'subJob',
|
name: 'subJob',
|
||||||
label: 'Sub Job',
|
label: 'Sub Job',
|
||||||
prefix: 'SJB',
|
prefix: 'SJB',
|
||||||
icon: SubJobIcon,
|
icon: SubJobIcon,
|
||||||
actions: [],
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'info',
|
||||||
|
label: 'Info',
|
||||||
|
default: true,
|
||||||
|
row: true,
|
||||||
|
icon: InfoCircleIcon,
|
||||||
|
url: (_id) => `/dashboard/production/subjobs/info?subJobId=${_id}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cancel',
|
||||||
|
label: 'Cancel Sub Job',
|
||||||
|
row: true,
|
||||||
|
icon: XMarkIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/production/subjobs/info?subJobId=${_id}&action=cancel`,
|
||||||
|
disabled: (objectData) => {
|
||||||
|
return objectData?.state?.type !== 'queued'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
columns: ['_id', 'printer', 'printer._id', 'job._id', 'state', 'createdAt'],
|
columns: ['_id', 'printer', 'printer._id', 'job._id', 'state', 'createdAt'],
|
||||||
filters: ['state', '_id', 'job._id', 'printer._id'],
|
filters: ['state', '_id', 'job._id', 'printer._id'],
|
||||||
sorters: ['createdAt', 'state'],
|
sorters: ['createdAt', 'state'],
|
||||||
|
group: ['job'],
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
name: '_id',
|
name: '_id',
|
||||||
@ -19,6 +42,68 @@ export const SubJob = {
|
|||||||
columnWidth: 140,
|
columnWidth: 140,
|
||||||
showCopy: true
|
showCopy: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'createdAt',
|
||||||
|
label: 'Created At',
|
||||||
|
type: 'dateTime',
|
||||||
|
readOnly: true,
|
||||||
|
columnWidth: 175
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'state',
|
||||||
|
label: 'State',
|
||||||
|
type: 'state',
|
||||||
|
objectType: 'subJob',
|
||||||
|
showStatus: true,
|
||||||
|
showProgress: true,
|
||||||
|
showId: false,
|
||||||
|
showQuantity: false,
|
||||||
|
columnWidth: 250,
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'updatedAt',
|
||||||
|
label: 'Updated At',
|
||||||
|
type: 'dateTime',
|
||||||
|
readOnly: true,
|
||||||
|
columnWidth: 175
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'job',
|
||||||
|
label: 'Job',
|
||||||
|
type: 'object',
|
||||||
|
objectType: 'job'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'job._id',
|
||||||
|
label: 'Job ID',
|
||||||
|
type: 'id',
|
||||||
|
columnWidth: 140,
|
||||||
|
showHyperlink: true,
|
||||||
|
objectType: 'job'
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'startedAt',
|
||||||
|
label: 'Started At',
|
||||||
|
type: 'dateTime',
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'moonrakerJobId',
|
||||||
|
label: 'Moonraker Job ID',
|
||||||
|
type: 'miscId',
|
||||||
|
columnWidth: 140,
|
||||||
|
showCopy: true
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'finishedAt',
|
||||||
|
label: 'Finished At',
|
||||||
|
type: 'dateTime',
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'printer',
|
name: 'printer',
|
||||||
label: 'Printer',
|
label: 'Printer',
|
||||||
@ -34,33 +119,6 @@ export const SubJob = {
|
|||||||
columnFixed: 'left',
|
columnFixed: 'left',
|
||||||
showHyperlink: true,
|
showHyperlink: true,
|
||||||
objectType: 'printer'
|
objectType: 'printer'
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'job._id',
|
|
||||||
label: 'Job ID',
|
|
||||||
type: 'id',
|
|
||||||
columnWidth: 140,
|
|
||||||
showHyperlink: true,
|
|
||||||
objectType: 'job'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'state',
|
|
||||||
label: 'State',
|
|
||||||
type: 'state',
|
|
||||||
objectType: 'subJob',
|
|
||||||
showStatus: true,
|
|
||||||
showProgress: true,
|
|
||||||
showId: false,
|
|
||||||
showQuantity: false,
|
|
||||||
columnWidth: 125,
|
|
||||||
readOnly: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'createdAt',
|
|
||||||
label: 'Created At',
|
|
||||||
type: 'dateTime',
|
|
||||||
readOnly: true,
|
|
||||||
columnWidth: 175
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import VendorIcon from '../../components/Icons/VendorIcon'
|
import VendorIcon from '../../components/Icons/VendorIcon'
|
||||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
import EditIcon from '../../components/Icons/EditIcon'
|
import EditIcon from '../../components/Icons/EditIcon'
|
||||||
|
import CheckIcon from '../../components/Icons/CheckIcon'
|
||||||
|
import XMarkIcon from '../../components/Icons/XMarkIcon'
|
||||||
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
import ReloadIcon from '../../components/Icons/ReloadIcon'
|
||||||
import BinIcon from '../../components/Icons/BinIcon'
|
import BinIcon from '../../components/Icons/BinIcon'
|
||||||
|
|
||||||
@ -31,7 +33,31 @@ export const Vendor = {
|
|||||||
row: true,
|
row: true,
|
||||||
icon: EditIcon,
|
icon: EditIcon,
|
||||||
url: (_id) =>
|
url: (_id) =>
|
||||||
`/dashboard/management/vendors/info?vendorId=${_id}&action=edit`
|
`/dashboard/management/vendors/info?vendorId=${_id}&action=edit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
return !(objectData?._isEditing && objectData?._isEditing == true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'finishEdit',
|
||||||
|
label: 'Save Edits',
|
||||||
|
icon: CheckIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/vendors/info?vendorId=${_id}&action=finishEdit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
return objectData?._isEditing && objectData?._isEditing == true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cancelEdit',
|
||||||
|
label: 'Cancel Edits',
|
||||||
|
icon: XMarkIcon,
|
||||||
|
url: (_id) =>
|
||||||
|
`/dashboard/management/vendors/info?vendorId=${_id}&action=cancelEdit`,
|
||||||
|
visible: (objectData) => {
|
||||||
|
console.log(objectData?._isEditing)
|
||||||
|
return objectData?._isEditing && objectData?._isEditing == true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
@ -43,7 +69,6 @@ export const Vendor = {
|
|||||||
`/dashboard/management/vendors/info?vendorId=${_id}&action=delete`
|
`/dashboard/management/vendors/info?vendorId=${_id}&action=delete`
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
url: (id) => `/dashboard/management/vendors/info?vendorId=${id}`,
|
|
||||||
columns: ['name', '_id', 'country', 'email', 'website', 'createdAt'],
|
columns: ['name', '_id', 'country', 'email', 'website', 'createdAt'],
|
||||||
filters: ['name', '_id', 'country', 'email'],
|
filters: ['name', '_id', 'country', 'email'],
|
||||||
sorters: ['name', 'country', 'email', 'createdAt', '_id'],
|
sorters: ['name', 'country', 'email', 'createdAt', '_id'],
|
||||||
|
|||||||
@ -3,9 +3,12 @@ import { Route } from 'react-router-dom'
|
|||||||
import FilamentStocks from '../components/Dashboard/Inventory/FilamentStocks.jsx'
|
import FilamentStocks from '../components/Dashboard/Inventory/FilamentStocks.jsx'
|
||||||
import FilamentStockInfo from '../components/Dashboard/Inventory/FilamentStocks/FilamentStockInfo.jsx'
|
import FilamentStockInfo from '../components/Dashboard/Inventory/FilamentStocks/FilamentStockInfo.jsx'
|
||||||
import PartStocks from '../components/Dashboard/Inventory/PartStocks.jsx'
|
import PartStocks from '../components/Dashboard/Inventory/PartStocks.jsx'
|
||||||
|
import PartStockInfo from '../components/Dashboard/Inventory/PartStocks/PartStockInfo.jsx'
|
||||||
import StockEvents from '../components/Dashboard/Inventory/StockEvents.jsx'
|
import StockEvents from '../components/Dashboard/Inventory/StockEvents.jsx'
|
||||||
import StockAudits from '../components/Dashboard/Inventory/StockAudits.jsx'
|
import StockAudits from '../components/Dashboard/Inventory/StockAudits.jsx'
|
||||||
import StockAuditInfo from '../components/Dashboard/Inventory/StockAudits/StockAuditInfo.jsx'
|
import StockAuditInfo from '../components/Dashboard/Inventory/StockAudits/StockAuditInfo.jsx'
|
||||||
|
import PurchaseOrders from '../components/Dashboard/Inventory/PurchaseOrders.jsx'
|
||||||
|
import PurchaseOrderInfo from '../components/Dashboard/Inventory/PurchaseOrders/PurchaseOrderInfo.jsx'
|
||||||
|
|
||||||
const InventoryRoutes = [
|
const InventoryRoutes = [
|
||||||
<Route
|
<Route
|
||||||
@ -23,6 +26,11 @@ const InventoryRoutes = [
|
|||||||
path='inventory/partstocks'
|
path='inventory/partstocks'
|
||||||
element={<PartStocks />}
|
element={<PartStocks />}
|
||||||
/>,
|
/>,
|
||||||
|
<Route
|
||||||
|
key='partstocks-info'
|
||||||
|
path='inventory/partstocks/info'
|
||||||
|
element={<PartStockInfo />}
|
||||||
|
/>,
|
||||||
<Route
|
<Route
|
||||||
key='stockevents'
|
key='stockevents'
|
||||||
path='inventory/stockevents'
|
path='inventory/stockevents'
|
||||||
@ -37,6 +45,16 @@ const InventoryRoutes = [
|
|||||||
key='stockaudits-info'
|
key='stockaudits-info'
|
||||||
path='inventory/stockaudits/info'
|
path='inventory/stockaudits/info'
|
||||||
element={<StockAuditInfo />}
|
element={<StockAuditInfo />}
|
||||||
|
/>,
|
||||||
|
<Route
|
||||||
|
key='purchaseorders'
|
||||||
|
path='inventory/purchaseorders'
|
||||||
|
element={<PurchaseOrders />}
|
||||||
|
/>,
|
||||||
|
<Route
|
||||||
|
key='purchaseorders-info'
|
||||||
|
path='inventory/purchaseorders/info'
|
||||||
|
element={<PurchaseOrderInfo />}
|
||||||
/>
|
/>
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,10 @@ import ProductInfo from '../components/Dashboard/Management/Products/ProductInfo
|
|||||||
import Vendors from '../components/Dashboard/Management/Vendors'
|
import Vendors from '../components/Dashboard/Management/Vendors'
|
||||||
import VendorInfo from '../components/Dashboard/Management/Vendors/VendorInfo'
|
import VendorInfo from '../components/Dashboard/Management/Vendors/VendorInfo'
|
||||||
import Materials from '../components/Dashboard/Management/Materials'
|
import Materials from '../components/Dashboard/Management/Materials'
|
||||||
|
import Couriers from '../components/Dashboard/Management/Couriers'
|
||||||
|
import CourierInfo from '../components/Dashboard/Management/Couriers/CourierInfo.jsx'
|
||||||
|
import CourierServices from '../components/Dashboard/Management/CourierServices'
|
||||||
|
import CourierServiceInfo from '../components/Dashboard/Management/CourierServices/CourierServiceInfo.jsx'
|
||||||
import Settings from '../components/Dashboard/Management/Settings'
|
import Settings from '../components/Dashboard/Management/Settings'
|
||||||
import AuditLogs from '../components/Dashboard/Management/AuditLogs.jsx'
|
import AuditLogs from '../components/Dashboard/Management/AuditLogs.jsx'
|
||||||
import NoteTypes from '../components/Dashboard/Management/NoteTypes.jsx'
|
import NoteTypes from '../components/Dashboard/Management/NoteTypes.jsx'
|
||||||
@ -73,6 +77,22 @@ const ManagementRoutes = [
|
|||||||
element={<FileInfo />}
|
element={<FileInfo />}
|
||||||
/>,
|
/>,
|
||||||
<Route key='materials' path='management/materials' element={<Materials />} />,
|
<Route key='materials' path='management/materials' element={<Materials />} />,
|
||||||
|
<Route key='couriers' path='management/couriers' element={<Couriers />} />,
|
||||||
|
<Route
|
||||||
|
key='couriers-info'
|
||||||
|
path='management/couriers/info'
|
||||||
|
element={<CourierInfo />}
|
||||||
|
/>,
|
||||||
|
<Route
|
||||||
|
key='courierServices'
|
||||||
|
path='management/courierServices'
|
||||||
|
element={<CourierServices />}
|
||||||
|
/>,
|
||||||
|
<Route
|
||||||
|
key='courierServices-info'
|
||||||
|
path='management/courierServices/info'
|
||||||
|
element={<CourierServiceInfo />}
|
||||||
|
/>,
|
||||||
<Route key='notetypes' path='management/notetypes' element={<NoteTypes />} />,
|
<Route key='notetypes' path='management/notetypes' element={<NoteTypes />} />,
|
||||||
<Route
|
<Route
|
||||||
key='notetypes-info'
|
key='notetypes-info'
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import PrinterInfo from '../components/Dashboard/Production/Printers/PrinterInfo
|
|||||||
import Jobs from '../components/Dashboard/Production/Jobs.jsx'
|
import Jobs from '../components/Dashboard/Production/Jobs.jsx'
|
||||||
import JobInfo from '../components/Dashboard/Production/Jobs/JobInfo.jsx'
|
import JobInfo from '../components/Dashboard/Production/Jobs/JobInfo.jsx'
|
||||||
import SubJobs from '../components/Dashboard/Production/SubJobs.jsx'
|
import SubJobs from '../components/Dashboard/Production/SubJobs.jsx'
|
||||||
|
import SubJobInfo from '../components/Dashboard/Production/SubJobs/SubJobInfo.jsx'
|
||||||
import GCodeFiles from '../components/Dashboard/Production/GCodeFiles'
|
import GCodeFiles from '../components/Dashboard/Production/GCodeFiles'
|
||||||
import GCodeFileInfo from '../components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx'
|
import GCodeFileInfo from '../components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx'
|
||||||
|
|
||||||
@ -29,6 +30,11 @@ const ProductionRoutes = [
|
|||||||
/>,
|
/>,
|
||||||
<Route key='jobs' path='production/jobs' element={<Jobs />} />,
|
<Route key='jobs' path='production/jobs' element={<Jobs />} />,
|
||||||
<Route key='subjobs' path='production/subjobs' element={<SubJobs />} />,
|
<Route key='subjobs' path='production/subjobs' element={<SubJobs />} />,
|
||||||
|
<Route
|
||||||
|
key='subjobs-info'
|
||||||
|
path='production/subjobs/info'
|
||||||
|
element={<SubJobInfo />}
|
||||||
|
/>,
|
||||||
<Route key='jobs-info' path='production/jobs/info' element={<JobInfo />} />,
|
<Route key='jobs-info' path='production/jobs/info' element={<JobInfo />} />,
|
||||||
<Route
|
<Route
|
||||||
key='gcodefiles'
|
key='gcodefiles'
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user