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"?>
<!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;">
<g transform="matrix(0.707791,0,0,0.707791,-3.35696e-07,11.571)">
<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;"/>
</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 transform="matrix(0.965553,0,0,0.965553,2.707286,3.96965)">
<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>
</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 {
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-router-dom": "^7.8.2",
"remark-gfm": "^4.0.1",
"simplebar-react": "^3.3.2",
"socket.io-client": "*",
"standard": "^17.1.2",
"styled-components": "^6.1.19",

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -102,9 +102,9 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
form={form}
layout='vertical'
style={style}
onValuesChange={(values) => {
onValuesChange={(_changedValues, allFormValues) => {
// Calculate computed values based on current form data
const currentFormData = { ...objectData, ...values }
const currentFormData = { ...objectData, ...allFormValues }
const computedValues = calculateComputedValues(currentFormData, model)
// 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)
const allValues = { ...values, ...computedValues }
const allValues = { ...allFormValues, ...computedValues }
setObjectData((prev) => {
return merge({}, prev, allValues)
})

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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 { PartStock } from './models/PartStock'
import { ProductStock } from './models/ProductStock'
import { PurchaseOrder } from './models/PurchaseOrder'
import { AuditLog } from './models/AuditLog'
import { User } from './models/User'
import { NoteType } from './models/NoteType'
@ -43,6 +44,7 @@ export const objectModels = [
StockAudit,
PartStock,
ProductStock,
PurchaseOrder,
AuditLog,
User,
NoteType,
@ -72,6 +74,7 @@ export {
StockAudit,
PartStock,
ProductStock,
PurchaseOrder,
AuditLog,
User,
NoteType,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import { Route } from 'react-router-dom'
import FilamentStocks from '../components/Dashboard/Inventory/FilamentStocks.jsx'
import FilamentStockInfo from '../components/Dashboard/Inventory/FilamentStocks/FilamentStockInfo.jsx'
import PartStocks from '../components/Dashboard/Inventory/PartStocks.jsx'
import PartStockInfo from '../components/Dashboard/Inventory/PartStocks/PartStockInfo.jsx'
import StockEvents from '../components/Dashboard/Inventory/StockEvents.jsx'
import StockAudits from '../components/Dashboard/Inventory/StockAudits.jsx'
import StockAuditInfo from '../components/Dashboard/Inventory/StockAudits/StockAuditInfo.jsx'
@ -23,6 +24,11 @@ const InventoryRoutes = [
path='inventory/partstocks'
element={<PartStocks />}
/>,
<Route
key='partstocks-info'
path='inventory/partstocks/info'
element={<PartStockInfo />}
/>,
<Route
key='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 JobInfo from '../components/Dashboard/Production/Jobs/JobInfo.jsx'
import SubJobs from '../components/Dashboard/Production/SubJobs.jsx'
import SubJobInfo from '../components/Dashboard/Production/SubJobs/SubJobInfo.jsx'
import GCodeFiles from '../components/Dashboard/Production/GCodeFiles'
import GCodeFileInfo from '../components/Dashboard/Production/GCodeFiles/GCodeFileInfo.jsx'
@ -29,6 +30,11 @@ const ProductionRoutes = [
/>,
<Route key='jobs' path='production/jobs' element={<Jobs />} />,
<Route key='subjobs' path='production/subjobs' element={<SubJobs />} />,
<Route
key='subjobs-info'
path='production/subjobs/info'
element={<SubJobInfo />}
/>,
<Route key='jobs-info' path='production/jobs/info' element={<JobInfo />} />,
<Route
key='gcodefiles'

1228
yarn.lock

File diff suppressed because it is too large Load Diff