Compare commits

...

3 Commits

Author SHA1 Message Date
05d7864da8 A lot of UI fixes updated packages etc. 2025-12-02 18:29:09 +00:00
15a9764762 More small macOS fixes. 2025-11-30 00:17:29 +00:00
5ac11dc59e Small macOS fix. 2025-11-30 00:17:13 +00:00
56 changed files with 3442 additions and 1630 deletions

View File

@ -1,10 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"> <svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.707791,0,0,0.707791,-3.35696e-07,11.571)"> <g transform="matrix(0.965553,0,0,0.965553,2.707286,3.96965)">
<path d="M13.734,57.726L76.694,57.726C86.55,57.726 91.489,51.584 90.229,41.164L86.837,12.946C85.843,4.532 81.097,0 73.389,0L17.039,0C9.33,0 4.579,4.532 3.585,12.946L0.194,41.164C-1.067,51.584 3.872,57.726 13.734,57.726ZM13.734,51.604C8.264,51.604 5.532,47.716 6.33,41.05L9.645,13.674C10.242,8.783 12.818,6.122 17.039,6.122L73.389,6.122C77.61,6.122 80.18,8.783 80.757,13.674L84.066,41.05C84.896,47.741 82.159,51.604 76.694,51.604L13.734,51.604Z" style="fill-rule:nonzero;"/> <path d="M0,38.289C0,42.92 3.792,46.713 8.424,46.713C13.066,46.713 16.88,42.94 16.88,38.289C16.88,33.647 13.066,29.864 8.424,29.864C3.792,29.864 0,33.647 0,38.289ZM4.47,38.289C4.47,36.077 6.234,34.334 8.445,34.334C10.687,34.334 12.41,36.077 12.41,38.289C12.41,40.53 10.687,42.243 8.445,42.243C6.234,42.243 4.47,40.53 4.47,38.289ZM8.445,0C6.681,0 5.129,1.453 5.129,3.267L5.129,32.475L11.741,32.475L11.741,3.267C11.741,1.443 10.259,0 8.445,0ZM8.445,58.061C10.279,58.061 11.741,56.628 11.741,54.889L11.741,44.362L5.129,44.362L5.129,54.889C5.129,56.618 6.652,58.061 8.445,58.061ZM21.887,19.56C21.887,24.182 25.701,27.994 30.333,27.994C34.974,27.994 38.767,24.202 38.767,19.56C38.767,14.928 34.974,11.136 30.333,11.136C25.701,11.136 21.887,14.928 21.887,19.56ZM26.378,19.56C26.378,17.37 28.142,15.606 30.333,15.606C32.574,15.606 34.297,17.37 34.297,19.56C34.297,21.802 32.574,23.525 30.333,23.525C28.142,23.525 26.378,21.802 26.378,19.56ZM30.333,0C28.569,0 27.038,1.453 27.038,3.171L27.038,13.639L33.649,13.639L33.649,3.171C33.649,1.443 32.136,0 30.333,0ZM30.333,58.061C32.166,58.061 33.649,56.628 33.649,54.804L33.649,25.244L27.038,25.244L27.038,54.804C27.038,56.618 28.539,58.061 30.333,58.061ZM43.817,38.289C43.817,42.92 47.61,46.713 52.241,46.713C56.883,46.713 60.675,42.92 60.675,38.289C60.675,33.647 56.883,29.864 52.241,29.864C47.61,29.864 43.817,33.647 43.817,38.289ZM48.287,38.289C48.287,36.077 50.051,34.334 52.241,34.334C54.504,34.334 56.206,36.077 56.206,38.289C56.206,40.53 54.504,42.243 52.241,42.243C50.051,42.243 48.287,40.53 48.287,38.289ZM52.241,0C50.477,0 48.925,1.453 48.925,3.267L48.925,32.592L55.536,32.592L55.536,3.267C55.536,1.443 54.045,0 52.241,0ZM52.241,58.061C54.075,58.061 55.536,56.628 55.536,54.889L55.536,44.054L48.925,44.054L48.925,54.889C48.925,56.618 50.448,58.061 52.241,58.061Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(0.707791,0,0,0.707791,-3.35696e-07,11.571)">
<path d="M24.535,37.579C25.826,37.579 26.851,36.966 26.851,36.089L26.851,27.451C30.02,26.514 32.35,23.524 32.35,20.015C32.35,15.714 28.836,12.194 24.535,12.194C20.208,12.194 16.708,15.739 16.708,20.015C16.708,23.55 19.044,26.519 22.244,27.502L22.244,36.089C22.244,36.966 23.249,37.579 24.535,37.579ZM24.541,44.44C31.002,44.44 35.524,40.961 35.524,36.058C35.524,32.661 33.342,29.971 29.911,28.613L29.911,33.011C30.937,33.758 31.546,34.81 31.546,36.058C31.546,38.902 28.677,40.891 24.541,40.891C20.399,40.891 17.498,38.902 17.498,36.058C17.498,34.784 18.127,33.727 19.185,32.979L19.185,28.619C15.707,29.939 13.525,32.661 13.525,36.058C13.525,40.961 18.079,44.44 24.541,44.44ZM42.796,26.929C45.401,26.929 47.471,24.864 47.471,22.286C47.471,19.687 45.401,17.611 42.796,17.611C40.229,17.611 38.147,19.687 38.147,22.286C38.147,24.864 40.229,26.929 42.796,26.929ZM56.127,26.929C58.701,26.929 60.802,24.864 60.802,22.286C60.802,19.687 58.701,17.611 56.127,17.611C53.529,17.611 51.452,19.687 51.452,22.286C51.452,24.864 53.529,26.929 56.127,26.929ZM69.385,26.929C71.964,26.929 74.06,24.864 74.06,22.286C74.06,19.687 71.964,17.611 69.385,17.611C66.817,17.611 64.71,19.687 64.71,22.286C64.71,24.864 66.817,26.929 69.385,26.929ZM47.112,40.193C49.691,40.193 51.762,38.128 51.762,35.549C51.762,32.95 49.691,30.874 47.112,30.874C44.519,30.874 42.437,32.95 42.437,35.549C42.437,38.128 44.519,40.193 47.112,40.193ZM60.418,40.193C63.017,40.193 65.093,38.128 65.093,35.549C65.093,32.95 63.017,30.874 60.418,30.874C57.845,30.874 55.769,32.95 55.769,35.549C55.769,38.128 57.845,40.193 60.418,40.193ZM73.707,40.193C76.306,40.193 78.382,38.128 78.382,35.549C78.382,32.95 76.306,30.874 73.707,30.874C71.134,30.874 69.057,32.95 69.057,35.549C69.057,38.128 71.134,40.193 73.707,40.193Z" style="fill-rule:nonzero;"/>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M25,54.028C25,54.887 24.545,55.681 23.804,56.116C23.064,56.55 22.148,56.56 21.398,56.141C16.479,53.393 8.262,48.803 8.262,48.803C5.729,47.378 4.441,45.879 4.441,42.127L4.441,20.65C4.441,17.814 5.489,16.02 7.877,14.692L26.541,4.215C29.828,2.367 33.176,2.367 36.463,4.215L55.149,14.692C57.515,16.02 58.563,17.814 58.563,20.65L58.563,22.637C58.563,23.942 57.505,25 56.2,25L56.198,25C55.571,25 54.97,24.751 54.527,24.308C54.084,23.865 53.835,23.264 53.835,22.637L53.835,22.284C53.835,22.284 51.807,23.429 50.385,24.232C49.494,24.736 48.488,25 47.465,25L40.475,25C39.454,25 38.45,24.737 37.56,24.235C33.115,21.731 19.274,13.934 19.274,13.934L11.856,18.15C11.856,18.15 21.408,23.505 25.75,25.94C26.238,26.214 26.59,26.68 26.72,27.224C26.849,27.769 26.746,28.343 26.434,28.808C26.264,28.997 26.133,29.214 26.013,29.437C25.763,29.906 25.335,30.254 24.826,30.402C24.316,30.551 23.768,30.487 23.306,30.226C18.993,27.833 9.169,22.284 9.169,22.284L9.169,41.664C9.169,43.059 9.681,43.919 11.137,44.722C11.137,44.722 20.377,49.94 23.77,51.856C24.53,52.285 25,53.091 25,53.964L25,54.028ZM43.972,22.202L24.257,11.165L28.656,8.675C30.587,7.587 32.407,7.556 34.365,8.675L51.17,18.15L43.972,22.202Z"/>
<g transform="matrix(0.58577,0,0,0.58577,29,29)">
<path d="M11.031,59.75L48.719,59.75C55.859,59.75 59.75,55.891 59.75,48.797L59.75,10.969C59.75,3.891 55.859,-0 48.719,-0L11.031,-0C3.906,-0 -0,3.891 -0,10.969L-0,48.797C-0,55.891 3.906,59.75 11.031,59.75ZM11.906,51.688C9.391,51.688 8.063,50.469 8.063,47.813L8.063,11.969C8.063,9.313 9.391,8.078 11.906,8.078L47.844,8.078C50.344,8.078 51.688,9.313 51.688,11.969L51.688,47.813C51.688,50.469 50.344,51.688 47.844,51.688L11.906,51.688Z" style="fill-rule:nonzero;"/>
<g transform="matrix(0.664312,0,0,0.664312,5.121464,12.135807)">
<path d="M37.594,52.969C39.062,52.969 40.625,52.281 41.875,51L61.844,31.094C62.938,30 63.656,28.188 63.656,26.5C63.656,24.812 62.938,23 61.844,21.906L41.875,1.969C40.625,0.688 39.062,0 37.594,0C33.812,0 31.406,2.562 31.406,5.906C31.406,7.875 32.312,9.25 33.5,10.406L40.5,17.344L50.219,26.5L40.5,35.656L33.5,42.562C32.312,43.688 31.406,45.094 31.406,47.062C31.406,50.406 33.812,52.969 37.594,52.969ZM1.485,32.781L37.75,32.781L51.969,32.094C55.531,31.938 57.906,29.844 57.906,26.5C57.906,23.156 55.531,21.062 51.969,20.906L37.75,20.219L1.485,20.219C-2.515,20.219 -5.14,22.719 -5.14,26.5C-5.14,30.281 -2.515,32.781 1.485,32.781Z" style="fill-rule:nonzero;"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

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

View File

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

View File

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

View File

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

View File

@ -1,119 +1,101 @@
import { useState, useEffect } from 'react'
import { Form, Input, Button, Space, Select, InputNumber } from 'antd'
import axios from 'axios'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import config from '../../../../config' import { 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
const NewPartStock = ({ onOk, reset }) => { const NewPartStock = ({ onOk, reset }) => {
const [form] = Form.useForm() const [currentStep, setCurrentStep] = useState(0)
const [parts, setParts] = useState([]) const isMobile = useMediaQuery({ maxWidth: 768 })
const [loading, setLoading] = useState(false)
useEffect(() => {
// Reset form when reset prop changes
if (reset) {
form.resetFields()
}
}, [reset, form])
useEffect(() => {
// Fetch parts for the select dropdown
const fetchParts = async () => {
try {
const response = await axios.get(`${config.backendUrl}/parts`, {
headers: {
Accept: 'application/json'
},
withCredentials: true
})
setParts(response.data)
} catch (error) {
console.error('Error fetching parts:', error)
}
}
fetchParts()
}, [])
const onFinish = async (values) => {
setLoading(true)
try {
await axios.post(
`${config.backendUrl}/partstocks`,
{
part: values.part,
startingLots: values.startingLots,
currentLots: values.startingLots, // Initially current lots equals starting lots
notes: values.notes
},
{
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
withCredentials: true
}
)
onOk()
} catch (error) {
console.error('Error creating part stock:', error)
} finally {
setLoading(false)
}
}
return ( return (
<Form <NewObjectForm
form={form} type={'partStock'}
layout='vertical' reset={reset}
onFinish={onFinish} defaultValues={{ state: { type: 'new' } }}
style={{ maxWidth: '100%' }}
> >
<Form.Item {({ handleSubmit, submitLoading, objectData, formValid }) => {
name='part' const steps = [
label='Part' {
rules={[{ required: true, message: 'Please select a part' }]} title: 'Required',
> key: 'required',
<Select content: (
placeholder='Select a part' <ObjectInfo
options={parts.map((part) => ({ type='partStock'
value: part._id, column={1}
label: part.name bordered={false}
}))} isEditing={true}
initial={true}
required={true}
objectData={objectData}
/> />
</Form.Item> )
},
<Form.Item {
name='startingLots' title: 'Summary',
label='Starting Lots' key: 'summary',
rules={[ content: (
{ required: true, message: 'Please enter the starting lots' }, <ObjectInfo
{ type: 'number', min: 1, message: 'Lots must be at least 1' } type='partStock'
]} column={1}
> bordered={false}
<InputNumber visibleProperties={{
style={{ width: '100%' }} _id: false,
placeholder='Enter starting lots' createdAt: false,
min={1} updatedAt: false
}}
isEditing={false}
objectData={objectData}
/> />
</Form.Item> )
}
<Form.Item name='notes' label='Notes'> ]
<Input.TextArea return (
placeholder='Enter any additional notes' <Flex gap='middle'>
autoSize={{ minRows: 3, maxRows: 6 }} {!isMobile && (
<div style={{ minWidth: '160px' }}>
<Steps
current={currentStep}
items={steps}
direction='vertical'
style={{ width: 'fit-content' }}
/> />
</Form.Item> </div>
)}
<Form.Item> {!isMobile && (
<Space> <Divider type='vertical' style={{ height: 'unset' }} />
<Button type='primary' htmlType='submit' loading={loading}> )}
Create Part Stock
</Button> <Flex vertical gap='middle' style={{ flexGrow: 1 }}>
<Button onClick={() => form.resetFields()}>Reset</Button> <Title level={2} style={{ margin: 0 }}>
</Space> New Part Stock
</Form.Item> </Title>
</Form> <div style={{ minHeight: '260px', marginBottom: 8 }}>
{steps[currentStep].content}
</div>
<NewObjectButtons
currentStep={currentStep}
totalSteps={steps.length}
onPrevious={() => setCurrentStep((prev) => prev - 1)}
onNext={() => setCurrentStep((prev) => prev + 1)}
onSubmit={() => {
handleSubmit()
onOk()
}}
formValid={formValid}
submitLoading={submitLoading}
/>
</Flex>
</Flex>
)
}}
</NewObjectForm>
) )
} }
@ -122,8 +104,4 @@ NewPartStock.propTypes = {
reset: PropTypes.bool reset: PropTypes.bool
} }
NewPartStock.defaultProps = {
reset: false
}
export default NewPartStock export default NewPartStock

View File

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

View File

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

View File

@ -0,0 +1,107 @@
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
const NewStockAudit = ({ onOk, reset }) => {
const [currentStep, setCurrentStep] = useState(0)
const isMobile = useMediaQuery({ maxWidth: 768 })
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}
initial={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 (
<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 Stock audit
</Title>
<div style={{ minHeight: '260px', marginBottom: 8 }}>
{steps[currentStep].content}
</div>
<NewObjectButtons
currentStep={currentStep}
totalSteps={steps.length}
onPrevious={() => setCurrentStep((prev) => prev - 1)}
onNext={() => setCurrentStep((prev) => prev + 1)}
onSubmit={() => {
handleSubmit()
onOk()
}}
formValid={formValid}
submitLoading={submitLoading}
/>
</Flex>
</Flex>
)
}}
</NewObjectForm>
)
}
NewStockAudit.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool
}
export default NewStockAudit

View File

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

View File

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

View File

@ -8,6 +8,7 @@ import useCollapseState from '../../hooks/useCollapseState.js'
import NotesPanel from '../../common/NotesPanel.jsx' import NotesPanel from '../../common/NotesPanel.jsx'
import InfoCollapse from '../../common/InfoCollapse.jsx' import InfoCollapse from '../../common/InfoCollapse.jsx'
import ObjectInfo from '../../common/ObjectInfo.jsx' import ObjectInfo from '../../common/ObjectInfo.jsx'
import ObjectProperty from '../../common/ObjectProperty.jsx'
import ViewButton from '../../common/ViewButton.jsx' import ViewButton from '../../common/ViewButton.jsx'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx' import NoteIcon from '../../../Icons/NoteIcon.jsx'
@ -24,6 +25,8 @@ import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import MissingPlaceholder from '../../common/MissingPlaceholder.jsx' import MissingPlaceholder from '../../common/MissingPlaceholder.jsx'
import FilePreview from '../../common/FilePreview.jsx' import FilePreview from '../../common/FilePreview.jsx'
import ScrollBox from '../../common/ScrollBox.jsx' import ScrollBox from '../../common/ScrollBox.jsx'
import { getModelProperty } from '../../../../database/ObjectModels.js'
import PartIcon from '../../../Icons/PartIcon.jsx'
const log = loglevel.getLogger('GCodeFileInfo') const log = loglevel.getLogger('GCodeFileInfo')
log.setLevel(config.logLevel) log.setLevel(config.logLevel)
@ -37,7 +40,8 @@ const GCodeFileInfo = () => {
'GCodeFileInfo', 'GCodeFileInfo',
{ {
info: true, info: true,
stocks: true, parts: true,
preview: true,
notes: true, notes: true,
auditLogs: true auditLogs: true
} }
@ -94,6 +98,7 @@ const GCodeFileInfo = () => {
disabled={objectFormState.loading} disabled={objectFormState.loading}
items={[ items={[
{ key: 'info', label: 'GCode File Information' }, { key: 'info', label: 'GCode File Information' },
{ key: 'parts', label: 'Parts' },
{ key: 'preview', label: 'GCode File Preview' }, { key: 'preview', label: 'GCode File Preview' },
{ key: 'notes', label: 'Notes' }, { key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' } { key: 'auditLogs', label: 'Audit Logs' }
@ -164,7 +169,25 @@ const GCodeFileInfo = () => {
isEditing={isEditing} isEditing={isEditing}
type='gcodeFile' type='gcodeFile'
objectData={objectData} objectData={objectData}
visibleProperties={{}} visibleProperties={{
parts: false
}}
/>
</InfoCollapse>
<InfoCollapse
title='Parts'
icon={<PartIcon />}
active={collapseState.parts}
onToggle={(expanded) =>
updateCollapseState('parts', expanded)
}
collapseKey='parts'
>
<ObjectProperty
{...getModelProperty('gcodeFile', 'parts')}
isEditing={isEditing}
objectData={objectData}
loading={loading}
/> />
</InfoCollapse> </InfoCollapse>
<InfoCollapse <InfoCollapse

View File

@ -241,7 +241,10 @@ const ControlPrinter = () => {
visibleState={collapseState} visibleState={collapseState}
updateVisibleState={updateCollapseState} updateVisibleState={updateCollapseState}
/> />
<AlertsDisplay alerts={objectFormState.objectData?.alerts} /> <AlertsDisplay
alerts={objectFormState.objectData?.alerts}
printerId={printerId}
/>
</Space> </Space>
</Space> </Space>
<Space> <Space>
@ -321,7 +324,9 @@ const ControlPrinter = () => {
currentJob: false, currentJob: false,
'currentJob._id': false, 'currentJob._id': false,
currentSubJob: false, currentSubJob: false,
'currentSubJob._id': false 'currentSubJob._id': false,
createdAt: false,
updatedAt: false
}} }}
objectData={printerObjectData} objectData={printerObjectData}
type='printer' type='printer'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -102,9 +102,9 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
form={form} form={form}
layout='vertical' layout='vertical'
style={style} style={style}
onValuesChange={(values) => { onValuesChange={(_changedValues, allFormValues) => {
// Calculate computed values based on current form data // Calculate computed values based on current form data
const currentFormData = { ...objectData, ...values } const currentFormData = { ...objectData, ...allFormValues }
const computedValues = calculateComputedValues(currentFormData, model) const computedValues = calculateComputedValues(currentFormData, model)
// Update form with computed values if any were calculated // Update form with computed values if any were calculated
@ -113,7 +113,7 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
} }
// Merge all values (user input + computed values) // Merge all values (user input + computed values)
const allValues = { ...values, ...computedValues } const allValues = { ...allFormValues, ...computedValues }
setObjectData((prev) => { setObjectData((prev) => {
return merge({}, prev, allValues) return merge({}, prev, allValues)
}) })

View File

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

View File

@ -0,0 +1,469 @@
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>{record[property.name]}</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 }}
className='rollup-table'
/>
</div>
) : null
const tableComponent = (
<Flex vertical>
<div ref={mainTableWrapperRef}>
<Table
style={{ maxWidth }}
dataSource={dataSource}
columns={tableColumns}
loading={{ spinning: loading, indicator: <LoadingOutlined spin /> }}
pagination={false}
size={size}
rowKey={resolvedRowKey}
scroll={scrollConfig}
locale={{ emptyText }}
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'}
{...tableProps}
/>
</div>
{rollupTable}
</Flex>
)
const handleAddListItem = () => {
const newItem = {}
resolvedProperties.forEach((property) => {
if (property?.name) {
newItem[property.name] = null
}
})
add(newItem)
}
return (
<Card>
<Flex vertical gap={'middle'}>
<Flex justify={'space-between'}>
<Button>Actions</Button>
<Button
type='primary'
icon={<PlusIcon />}
onClick={handleAddListItem}
/>
</Flex>
{listTable}
</Flex>
</Card>
)
}}
</Form.List>
)
}
if (isEditing === true) {
return (
<Card>
<Flex vertical gap={'middle'}>
<Flex justify={'space-between'}>
<Button>Actions</Button>
<Button
type='primary'
icon={<PlusIcon />}
onClick={handleAddItem}
/>
</Flex>
{tableComponent}
</Flex>
</Card>
)
}
return tableComponent
}
ObjectChildTable.propTypes = {
properties: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
label: PropTypes.string,
type: PropTypes.string
})
).isRequired,
columns: PropTypes.arrayOf(PropTypes.string),
visibleColumns: PropTypes.object,
scrollHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
size: PropTypes.string,
loading: PropTypes.bool,
rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
skeletonRows: PropTypes.number,
additionalColumns: PropTypes.arrayOf(PropTypes.object),
emptyText: PropTypes.node,
isEditing: PropTypes.bool,
formListName: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
value: PropTypes.arrayOf(PropTypes.object),
onChange: PropTypes.func,
maxWidth: PropTypes.string,
rollups: PropTypes.arrayOf(PropTypes.object),
objectData: PropTypes.object
}
export default ObjectChildTable

View File

@ -37,6 +37,7 @@ const ObjectForm = forwardRef(
const [lock, setLock] = useState({}) const [lock, setLock] = useState({})
const [initialized, setInitialized] = useState(false) const [initialized, setInitialized] = useState(false)
const [isEditing, setIsEditing] = useState(false) const [isEditing, setIsEditing] = useState(false)
const isEditingRef = useRef(false)
const [formValid, setFormValid] = useState(false) const [formValid, setFormValid] = useState(false)
const [form] = Form.useForm() const [form] = Form.useForm()
@ -146,7 +147,8 @@ const ObjectForm = forwardRef(
const currentFormValues = form.getFieldsValue() const currentFormValues = form.getFieldsValue()
const mergedObjectData = { const mergedObjectData = {
...serverObjectData.current, ...serverObjectData.current,
...currentFormValues ...currentFormValues,
_isEditing: isEditingRef.current
} }
form form
@ -198,7 +200,7 @@ const ObjectForm = forwardRef(
const lockEvent = await fetchObjectLock(id, type) const lockEvent = await fetchObjectLock(id, type)
setLock(lockEvent) setLock(lockEvent)
onStateChangeRef.current({ lock: lockEvent }) onStateChangeRef.current({ lock: lockEvent })
setObjectData(data) setObjectData({ ...data, _isEditing: isEditingRef.current })
serverObjectData.current = data serverObjectData.current = data
// Calculate and set computed values on initial load // Calculate and set computed values on initial load
@ -275,7 +277,13 @@ const ObjectForm = forwardRef(
const startEditing = () => { const startEditing = () => {
setIsEditing(true) setIsEditing(true)
onStateChangeRef.current({ isEditing: true }) isEditingRef.current = true
console.log('IS EDITING TRUE')
setObjectData((prev) => ({ ...prev, _isEditing: isEditingRef.current }))
onStateChangeRef.current({
isEditing: true,
objectData: { ...objectData, _isEditing: isEditingRef.current }
})
lockObject(id, type) lockObject(id, type)
} }
@ -287,12 +295,14 @@ const ObjectForm = forwardRef(
model model
) )
const resetFormData = { ...serverObjectData.current, ...computedValues } const resetFormData = { ...serverObjectData.current, ...computedValues }
form.setFieldsValue(resetFormData)
setObjectData(resetFormData)
}
setIsEditing(false) setIsEditing(false)
onStateChangeRef.current({ isEditing: false }) isEditingRef.current = false
form.setFieldsValue(resetFormData)
console.log('IS EDITING FALSE')
setObjectData({ ...resetFormData, _isEditing: isEditingRef.current })
}
onStateChangeRef.current({ isEditing: isEditingRef.current })
unlockObject(id, type) unlockObject(id, type)
} }
@ -302,9 +312,15 @@ const ObjectForm = forwardRef(
setEditLoading(true) setEditLoading(true)
onStateChangeRef.current({ editLoading: true }) onStateChangeRef.current({ editLoading: true })
await updateObject(id, type, value) await updateObject(id, type, value)
setObjectData({ ...objectData, ...value })
setIsEditing(false) setIsEditing(false)
onStateChangeRef.current({ isEditing: false }) isEditingRef.current = false
onStateChangeRef.current({ isEditing: isEditingRef.current })
setObjectData({
...objectData,
...value,
_isEditing: isEditingRef.current
})
messageApi.success('Information updated successfully') messageApi.success('Information updated successfully')
} catch (err) { } catch (err) {
console.error(err) console.error(err)
@ -374,13 +390,19 @@ const ObjectForm = forwardRef(
form={form} form={form}
layout='vertical' layout='vertical'
style={style} style={style}
onValuesChange={(values) => { onValuesChange={(changedValues, allFormValues) => {
// Use the full form snapshot (allFormValues) so list fields (Form.List)
// come through as complete arrays instead of sparse arrays like
// [null, null, { quantity: 5 }].
if (onEdit != undefined) { if (onEdit != undefined) {
onEdit(values) onEdit(allFormValues)
} }
// Calculate computed values based on current form data // Calculate computed values based on current form data
const currentFormData = { ...objectData, ...values } const currentFormData = {
...(serverObjectData.current || {}),
...allFormValues
}
const computedValues = calculateComputedValues( const computedValues = calculateComputedValues(
currentFormData, currentFormData,
model model
@ -400,8 +422,12 @@ const ObjectForm = forwardRef(
} }
} }
// Merge all values (user input + computed values) // Merge all values (user input + computed values) and keep editing flag
const allValues = { ...values, ...computedValues } const allValues = {
...allFormValues,
...computedValues,
_isEditing: isEditingRef.current
}
setObjectData((prev) => { setObjectData((prev) => {
return { ...prev, ...allValues } return { ...prev, ...allValues }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,6 +15,7 @@ import { StockEvent } from './models/StockEvent'
import { StockAudit } from './models/StockAudit' import { StockAudit } from './models/StockAudit'
import { PartStock } from './models/PartStock' import { PartStock } from './models/PartStock'
import { ProductStock } from './models/ProductStock' import { ProductStock } from './models/ProductStock'
import { PurchaseOrder } from './models/PurchaseOrder'
import { AuditLog } from './models/AuditLog' import { AuditLog } from './models/AuditLog'
import { User } from './models/User' import { User } from './models/User'
import { NoteType } from './models/NoteType' import { NoteType } from './models/NoteType'
@ -43,6 +44,7 @@ export const objectModels = [
StockAudit, StockAudit,
PartStock, PartStock,
ProductStock, ProductStock,
PurchaseOrder,
AuditLog, AuditLog,
User, User,
NoteType, NoteType,
@ -72,6 +74,7 @@ export {
StockAudit, StockAudit,
PartStock, PartStock,
ProductStock, ProductStock,
PurchaseOrder,
AuditLog, AuditLog,
User, User,
NoteType, NoteType,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,8 @@ import EditIcon from '../../components/Icons/EditIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon' import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import PartIcon from '../../components/Icons/PartIcon' import PartIcon from '../../components/Icons/PartIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon' import ReloadIcon from '../../components/Icons/ReloadIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
export const Part = { export const Part = {
name: 'part', name: 'part',
@ -29,7 +31,32 @@ export const Part = {
label: 'Edit', label: 'Edit',
row: true, row: true,
icon: EditIcon, icon: EditIcon,
url: (_id) => `/dashboard/management/parts/info?partId=${_id}&action=edit` url: (_id) =>
`/dashboard/management/parts/info?partId=${_id}&action=edit`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
}
},
{
name: 'finishEdit',
label: 'Save Edits',
icon: CheckIcon,
url: (_id) =>
`/dashboard/management/parts/info?partId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'cancelEdit',
label: 'Cancel Edits',
icon: XMarkIcon,
url: (_id) =>
`/dashboard/management/parts/info?partId=${_id}&action=cancelEdit`,
visible: (objectData) => {
console.log(objectData?._isEditing)
return objectData?._isEditing && objectData?._isEditing == true
}
} }
], ],
columns: [ columns: [
@ -71,22 +98,6 @@ export const Part = {
type: 'dateTime', type: 'dateTime',
readOnly: true readOnly: true
}, },
{
name: 'product',
label: 'Product',
type: 'object',
required: true,
objectType: 'product'
},
{
name: 'product._id',
label: 'Product ID',
type: 'id',
readOnly: true,
showHyperlink: true,
objectType: 'product'
},
{ {
name: 'vendor', name: 'vendor',
label: 'Vendor', label: 'Vendor',

View File

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

View File

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

View File

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

View File

@ -0,0 +1,21 @@
import PurchaseOrderIcon from '../../components/Icons/PurchaseOrderIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
export const PurchaseOrder = {
name: 'purchaseorder',
label: 'Product Stock',
prefix: 'PDS',
icon: PurchaseOrderIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) =>
`/dashboard/management/purchaseorders/info?purchaseOrderId=${_id}`
}
],
url: (id) => `/dashboard/management/purchaseorders/info?purchaseOrderId=${id}`
}

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import { Route } from 'react-router-dom'
import FilamentStocks from '../components/Dashboard/Inventory/FilamentStocks.jsx' import FilamentStocks from '../components/Dashboard/Inventory/FilamentStocks.jsx'
import FilamentStockInfo from '../components/Dashboard/Inventory/FilamentStocks/FilamentStockInfo.jsx' import FilamentStockInfo from '../components/Dashboard/Inventory/FilamentStocks/FilamentStockInfo.jsx'
import PartStocks from '../components/Dashboard/Inventory/PartStocks.jsx' import PartStocks from '../components/Dashboard/Inventory/PartStocks.jsx'
import PartStockInfo from '../components/Dashboard/Inventory/PartStocks/PartStockInfo.jsx'
import StockEvents from '../components/Dashboard/Inventory/StockEvents.jsx' import StockEvents from '../components/Dashboard/Inventory/StockEvents.jsx'
import StockAudits from '../components/Dashboard/Inventory/StockAudits.jsx' import StockAudits from '../components/Dashboard/Inventory/StockAudits.jsx'
import StockAuditInfo from '../components/Dashboard/Inventory/StockAudits/StockAuditInfo.jsx' import StockAuditInfo from '../components/Dashboard/Inventory/StockAudits/StockAuditInfo.jsx'
@ -23,6 +24,11 @@ const InventoryRoutes = [
path='inventory/partstocks' path='inventory/partstocks'
element={<PartStocks />} element={<PartStocks />}
/>, />,
<Route
key='partstocks-info'
path='inventory/partstocks/info'
element={<PartStockInfo />}
/>,
<Route <Route
key='stockevents' key='stockevents'
path='inventory/stockevents' path='inventory/stockevents'

View File

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

1228
yarn.lock

File diff suppressed because it is too large Load Diff