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