Compare commits

..

10 Commits

Author SHA1 Message Date
34d919d88e Updated @ant-design/charts dependency from version 2.6.2 to 2.6.5 in package.json and yarn.lock for improved features and bug fixes. 2025-12-09 02:11:52 +00:00
705c517acf Added totalTime field to Job and SubJob models for duration calculation, and updated OrderItem model to include reference field and reorder properties for improved data structure. 2025-12-09 02:11:43 +00:00
c96f223176 Added labelWidth property to PrinterInfo component for improved layout consistency. 2025-12-09 02:10:53 +00:00
bf1b61179f Added labelWidth property to ObjectInfo components in NewJob and NewPrinter for consistent layout adjustments. 2025-12-09 02:10:45 +00:00
d8bfc19917 Added new InfoCollapse sections for Online Printers and Queued Jobs in ProductionOverview, including ObjectTable components for data display and updated collapse states for better user interaction. 2025-12-09 02:10:35 +00:00
5f33ed69fb Refined interval calculation in ApiServerProvider to adjust polling frequency based on time range, introducing new thresholds for intervals over 1, 2, and 3 days. 2025-12-09 02:10:23 +00:00
b7e81e2caa Enhanced ObjectSelect component to handle array inputs by normalizing values to a string of IDs, improving identity detection for in-place updates. 2025-12-09 02:10:09 +00:00
7f11168b25 Updated App.css to include new styles for picker components and adjusted button width for outlined variant. 2025-12-09 02:09:56 +00:00
3d17a08a71 Enhanced HistoryDisplay component with custom date range selection and improved loading states. 2025-12-09 02:09:48 +00:00
2d2aff125c Added destroyOnHidden property to the new job modal for improved resource management. 2025-12-09 02:09:36 +00:00
14 changed files with 264 additions and 62 deletions

View File

@ -28,7 +28,12 @@
.ant-select,
.ant-progress,
.ant-collapse,
.ant-picker-dropdown,
.ant-radio-group,
.g2-tooltip-title,
.g2-tooltip-list-item,
.ant-picker-input,
.ant-picker-header-view button,
[class*=' ant-radio'] {
font-family: 'DM Sans';
}
@ -391,3 +396,7 @@ body {
.object-info-descriptions table {
table-layout: fixed !important;
}
.ant-btn-variant-outlined.ant-btn.ant-btn-icon-only {
min-width: 32px;
}

View File

@ -9,7 +9,7 @@
"private": true,
"homepage": "./",
"dependencies": {
"@ant-design/charts": "^2.6.2",
"@ant-design/charts": "^2.6.5",
"@babel/plugin-transform-private-property-in-object": "^7.27.1",
"@codemirror/lang-cpp": "^6.0.3",
"@codemirror/lang-css": "^6.3.1",

View File

@ -85,6 +85,7 @@ const Jobs = () => {
open={newJobOpen}
footer={null}
width={700}
destroyOnHidden={true}
onCancel={() => {
setNewJobOpen(false)
}}

View File

@ -24,6 +24,7 @@ const NewJob = ({ onOk, defaultValues }) => {
bordered={false}
isEditing={true}
required={true}
labelWidth='100px'
objectData={objectData}
/>
)
@ -36,11 +37,14 @@ const NewJob = ({ onOk, defaultValues }) => {
type='job'
column={1}
bordered={false}
labelWidth='100px'
visibleProperties={{
_id: false,
createdAt: false,
updatedAt: false,
startedAt: false
startedAt: false,
finishedAt: false,
totalTime: false
}}
isEditing={false}
objectData={objectData}

View File

@ -25,6 +25,7 @@ const NewPrinter = ({ onOk, defaultValues }) => {
<ObjectInfo
type='printer'
column={1}
labelWidth='100px'
bordered={false}
isEditing={true}
required={true}
@ -42,6 +43,7 @@ const NewPrinter = ({ onOk, defaultValues }) => {
bordered={false}
isEditing={true}
required={false}
labelWidth='100px'
objectData={objectData}
visibleProperties={{
firmware: false,

View File

@ -155,6 +155,7 @@ const PrinterInfo = () => {
isEditing={isEditing}
type='printer'
objectData={objectData}
labelWidth='175px'
visibleProperties={{
currentFilamentStock: false,
'currentFilamentStock._id': false,

View File

@ -7,6 +7,7 @@ import InfoCollapse from '../common/InfoCollapse'
import ScrollBox from '../common/ScrollBox'
import { ApiServerContext } from '../context/ApiServerContext'
import ObjectTable from '../common/ObjectTable'
const ProductionOverview = () => {
const { connected } = useContext(ApiServerContext)
@ -16,6 +17,7 @@ const ProductionOverview = () => {
{
printerStats: true,
printerHistory: true,
onlinePrinters: true,
jobStats: true,
productionStats: true,
jobStatsDetails: true
@ -40,6 +42,7 @@ const ProductionOverview = () => {
<InfoCollapse
title='Printer Statistics'
icon={null}
canCollapse={false}
active={collapseState.printerStats}
onToggle={(isActive) =>
updateCollapseState('printerStats', isActive)
@ -60,6 +63,7 @@ const ProductionOverview = () => {
<InfoCollapse
title='Job Statistics'
icon={null}
canCollapse={false}
active={collapseState.jobStats}
onToggle={(isActive) => updateCollapseState('jobStats', isActive)}
className='no-t-padding-collapse'
@ -76,7 +80,7 @@ const ProductionOverview = () => {
</InfoCollapse>
<Flex gap='large' wrap='wrap'>
<Flex flex={1} vertical style={{ minWidth: '300px' }}>
<Flex flex={1} vertical style={{ minWidth: '300px' }} gap='large'>
<InfoCollapse
title='Printer History'
icon={null}
@ -89,8 +93,20 @@ const ProductionOverview = () => {
>
<HistoryDisplay objectType='printer' />
</InfoCollapse>
<InfoCollapse
title='Online Printers'
icon={null}
active={collapseState.onlinePrinters}
onToggle={(isActive) =>
updateCollapseState('onlinePrinters', isActive)
}
collapseKey='onlinePrinters'
canCollapse={false}
>
<ObjectTable type='printer' masterFilter={{ online: true }} />
</InfoCollapse>
</Flex>
<Flex flex={1} vertical style={{ minWidth: '300px' }}>
<Flex flex={1} vertical style={{ minWidth: '300px' }} gap='large'>
<InfoCollapse
title='Job History'
icon={null}
@ -103,6 +119,21 @@ const ProductionOverview = () => {
>
<HistoryDisplay objectType='job' />
</InfoCollapse>
<InfoCollapse
title='Queued Jobs'
icon={null}
active={collapseState.queuedJobs}
onToggle={(isActive) =>
updateCollapseState('queuedJobs', isActive)
}
canCollapse={false}
collapseKey='queuedJobs'
>
<ObjectTable
type='job'
masterFilter={{ 'state.type': 'queued' }}
/>
</InfoCollapse>
</Flex>
</Flex>
</Flex>

View File

@ -1,6 +1,5 @@
import { useEffect, useState, useContext, useMemo } from 'react'
import { Card, Spin, Segmented, Flex } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import { Card, Segmented, Flex, Popover, DatePicker, Button, Space } from 'antd'
import { Column } from '@ant-design/charts'
import PropTypes from 'prop-types'
import { getModelByName } from '../../../database/ObjectModels'
@ -8,14 +7,25 @@ import { ApiServerContext } from '../context/ApiServerContext'
import { AuthContext } from '../context/AuthContext'
import dayjs from 'dayjs'
import { useThemeContext } from '../context/ThemeContext'
import LoadingPlaceholder from './LoadingPlaceholder'
import MissingPlaceholder from './MissingPlaceholder'
import CheckIcon from '../../Icons/CheckIcon'
const HistoryDisplay = ({ objectType, startDate, endDate, styles }) => {
const HistoryDisplay = ({
objectType,
startDate,
endDate,
styles,
height = 400
}) => {
const { getModelHistory, connected } = useContext(ApiServerContext)
const { token } = useContext(AuthContext)
const [historyData, setHistoryData] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [timeRange, setTimeRange] = useState('4hrs')
const [startCustomDate, setStartCustomDate] = useState(null)
const [endCustomDate, setEndCustomDate] = useState(null)
const { isDarkMode, getColors } = useThemeContext()
// Calculate dates based on selected time range or provided props
const { defaultStartDate, defaultEndDate } = useMemo(() => {
@ -30,6 +40,14 @@ const HistoryDisplay = ({ objectType, startDate, endDate, styles }) => {
}
}
// Handle custom date range
if (timeRange === 'custom' && startCustomDate && endCustomDate) {
return {
defaultStartDate: startCustomDate.toDate(),
defaultEndDate: endCustomDate.toDate()
}
}
// Otherwise, calculate based on selected time range
const hoursMap = {
'8hrs': 8,
@ -43,7 +61,7 @@ const HistoryDisplay = ({ objectType, startDate, endDate, styles }) => {
defaultStartDate: new Date(now.getTime() - hours * 60 * 60 * 1000),
defaultEndDate: now
}
}, [startDate, endDate, timeRange])
}, [startDate, endDate, timeRange, startCustomDate, endCustomDate])
useEffect(() => {
if (!objectType || !getModelHistory || !token || !connected) {
@ -251,6 +269,7 @@ const HistoryDisplay = ({ objectType, startDate, endDate, styles }) => {
const config = {
data: chartData,
height,
xField: 'dateFormatted',
yField: 'value',
theme: {
@ -282,14 +301,58 @@ const HistoryDisplay = ({ objectType, startDate, endDate, styles }) => {
}
}
const customTimeRangeContent = (
<Space.Compact>
<DatePicker.RangePicker
onChange={(dates) => {
if (dates) {
setStartCustomDate(dates[0])
setEndCustomDate(dates[1])
} else {
setStartCustomDate(null)
setEndCustomDate(null)
}
}}
value={[startCustomDate, endCustomDate]}
/>
<Button
type='primary'
onClick={() => {
if (startCustomDate && endCustomDate) {
setTimeRange('custom')
}
}}
disabled={!startCustomDate || !endCustomDate}
icon={<CheckIcon />}
></Button>
</Space.Compact>
)
return (
<Spin spinning={loading} indicator={<LoadingOutlined spin />}>
<Card
style={{ width: '100%' }}
styles={{ body: { padding: '12px', ...styles } }}
>
{!startDate && !endDate && (
<Flex justify='flex-end'>
<Flex justify='space-between'>
<Flex align='center' gap='5px'>
<Popover
content={customTimeRangeContent}
trigger='hover'
arrow={false}
placement='bottomLeft'
styles={{ body: { borderRadius: '22.5px' } }}
>
<Segmented
size='small'
options={[{ label: 'Custom', value: 'custom' }]}
value={timeRange}
onChange={setTimeRange}
disabled={loading}
/>
</Popover>
</Flex>
<Segmented
size='small'
options={[
@ -301,12 +364,22 @@ const HistoryDisplay = ({ objectType, startDate, endDate, styles }) => {
]}
value={timeRange}
onChange={setTimeRange}
disabled={loading}
/>
</Flex>
)}
<Column {...config} />
{loading == true && (
<Flex justify='center' align='center' style={{ height: `${height}px` }}>
<LoadingPlaceholder message='Loading history data...' />
</Flex>
)}
{chartData.length > 0 && <Column {...config} />}
{loading == false && chartData.length == 0 && (
<Flex justify='center' align='center' style={{ height: `${height}px` }}>
<MissingPlaceholder message='No data available.' />
</Flex>
)}
</Card>
</Spin>
)
}
@ -317,7 +390,8 @@ HistoryDisplay.propTypes = {
PropTypes.instanceOf(Date)
]),
styles: PropTypes.object,
endDate: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)])
endDate: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]),
height: PropTypes.string
}
export default HistoryDisplay

View File

@ -56,6 +56,26 @@ const ObjectSelect = ({
// Normalize a value to an identity string so we can detect in-place _id updates
const getValueIdentity = useCallback((val) => {
if (val && typeof val === 'object') {
// Handle arrays
if (Array.isArray(val)) {
const ids = val
.map((item) => {
if (item && typeof item === 'object') {
if (item._id) return String(item._id)
if (
item.value &&
typeof item.value === 'object' &&
item.value._id
)
return String(item.value._id)
}
return null
})
.filter(Boolean)
.sort()
return ids.length > 0 ? ids.join(',') : JSON.stringify(val)
}
// Handle single objects
if (val._id) return String(val._id)
if (val.value && typeof val.value === 'object' && val.value._id)
return String(val.value._id)

View File

@ -969,10 +969,16 @@ const ApiServerProvider = ({ children }) => {
const timeRangeMs = endDate.getTime() - startDate.getTime()
const oneHourMs = 60 * 60 * 1000
const twelveHoursMs = 12 * 60 * 60 * 1000
const oneDayMs = 24 * 60 * 60 * 1000
const threeDaysMs = 3 * 24 * 60 * 60 * 1000
// Determine interval based on time range
let intervalMinutes = 1 // Default: 1 minute
if (timeRangeMs > twelveHoursMs) {
if (timeRangeMs > threeDaysMs) {
intervalMinutes = 60 // Over 1 day: 60 minutes
} else if (timeRangeMs > oneDayMs) {
intervalMinutes = 30 // Over 2 days: 30 minutes
} else if (timeRangeMs > twelveHoursMs) {
intervalMinutes = 10 // Over 12 hours: 10 minutes
} else if (timeRangeMs > oneHourMs) {
intervalMinutes = 5 // Over 1 hour: 5 minutes

View File

@ -2,6 +2,7 @@ import JobIcon from '../../components/Icons/JobIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import ReloadIcon from '../../components/Icons/ReloadIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import dayjs from 'dayjs'
export const Job = {
name: 'job',
@ -37,7 +38,7 @@ export const Job = {
url: (_id) => `/dashboard/production/jobs/info?jobId=${_id}&action=reload`
}
],
columns: ['_id', 'gcodeFile', 'quantity', 'state', 'createdAt'],
columns: ['_id', 'quantity', 'state', 'gcodeFile', 'createdAt'],
filters: ['state', '_id', 'gcodeFile', 'quantity'],
sorters: ['createdAt', 'state', 'quantity', 'gcodeFile'],
properties: [
@ -77,6 +78,7 @@ export const Job = {
name: 'quantity',
label: 'Quantity',
type: 'number',
columnFixed: 'left',
columnWidth: 125,
required: true
},
@ -103,10 +105,37 @@ export const Job = {
name: 'gcodeFile',
label: 'GCode File',
type: 'object',
columnFixed: 'left',
objectType: 'gcodeFile',
required: true,
showHyperlink: true
},
{
name: 'totalTime',
label: 'Total Time',
type: 'text',
readOnly: true,
value: (objectData) => {
if (!objectData?.startedAt || !objectData?.finishedAt) {
return '-'
}
const totalSeconds = dayjs(objectData?.finishedAt).diff(
dayjs(objectData?.startedAt),
'seconds'
)
const days = Math.floor(totalSeconds / 86400)
const hours = Math.floor((totalSeconds % 86400) / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
const parts = []
if (days > 0) parts.push(`${days}d`)
if (hours > 0) parts.push(`${hours}h`)
if (minutes > 0) parts.push(`${minutes}m`)
if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`)
return parts.join(' ')
}
}
],
stats: [

View File

@ -49,13 +49,13 @@ export const OrderItem = {
type: 'dateTime',
readOnly: true
},
{
name: 'orderType',
label: 'Order Type',
type: 'objectType',
masterFilter: ['purchaseOrder', 'salesOrder'],
required: true
name: '_reference',
label: 'Reference',
type: 'reference',
objectType: 'orderItem',
showCopy: true,
readOnly: true
},
{
name: 'updatedAt',
@ -63,6 +63,15 @@ export const OrderItem = {
type: 'dateTime',
readOnly: true
},
{ name: 'state', label: 'State', type: 'state', readOnly: true },
{
name: 'orderType',
label: 'Order Type',
type: 'objectType',
masterFilter: ['purchaseOrder', 'salesOrder'],
required: true
},
{
name: 'order',
label: 'Order',

View File

@ -1,6 +1,7 @@
import SubJobIcon from '../../components/Icons/SubJobIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
import dayjs from 'dayjs'
export const SubJob = {
name: 'subJob',
@ -68,7 +69,6 @@ export const SubJob = {
readOnly: true,
columnWidth: 175
},
{
name: 'moonrakerJobId',
label: 'Moonraker Job ID',
@ -76,28 +76,12 @@ export const SubJob = {
columnWidth: 140,
showCopy: true
},
{
name: 'startedAt',
label: 'Started At',
type: 'dateTime',
readOnly: true
},
{
name: 'createdPartStock',
label: 'Created Part Stock',
type: 'bool',
readOnly: true
},
{
name: 'finishedAt',
label: 'Finished At',
type: 'dateTime',
readOnly: true
},
{
name: 'job',
label: 'Job',
@ -105,7 +89,12 @@ export const SubJob = {
objectType: 'job',
showHyperlink: true
},
{
name: 'finishedAt',
label: 'Finished At',
type: 'dateTime',
readOnly: true
},
{
name: 'printer',
label: 'Printer',
@ -113,6 +102,33 @@ export const SubJob = {
columnFixed: 'left',
objectType: 'printer',
showHyperlink: true
},
{
name: 'totalTime',
label: 'Total Time',
type: 'text',
readOnly: true,
value: (objectData) => {
if (!objectData?.startedAt || !objectData?.finishedAt) {
return '-'
}
const totalSeconds = dayjs(objectData?.finishedAt).diff(
dayjs(objectData?.startedAt),
'seconds'
)
const days = Math.floor(totalSeconds / 86400)
const hours = Math.floor((totalSeconds % 86400) / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
const parts = []
if (days > 0) parts.push(`${days}d`)
if (hours > 0) parts.push(`${hours}h`)
if (minutes > 0) parts.push(`${minutes}m`)
if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`)
return parts.join(' ')
}
}
]
}

View File

@ -21,7 +21,7 @@
dependencies:
lodash "^4.17.21"
"@ant-design/charts@^2.6.2":
"@ant-design/charts@^2.6.5":
version "2.6.6"
resolved "https://registry.yarnpkg.com/@ant-design/charts/-/charts-2.6.6.tgz#236430f56bc4a130ca3119c9cd14873f45d51d1c"
integrity sha512-Mw2XqB9c7JoENyewJmtxU+5TU2sW5VEyct2f6n4HjJ/6hBo4ht3qdu965G3UrNLyiRctd47Qje32u+8DeFZ6Bg==
@ -1041,9 +1041,9 @@
optionalDependencies:
global-agent "^3.0.0"
"@electron/node-gyp@git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2":
"@electron/node-gyp@https://github.com/electron/node-gyp#06b29aafb7708acef8b3669835c8a7857ebc92d2":
version "10.2.0-electron.1"
resolved "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2"
resolved "https://github.com/electron/node-gyp#06b29aafb7708acef8b3669835c8a7857ebc92d2"
dependencies:
env-paths "^2.2.0"
exponential-backoff "^3.1.1"