diff --git a/src/components/Dashboard/Production/ProductionOverview.jsx b/src/components/Dashboard/Production/ProductionOverview.jsx
index 7cf6513..1f1e2f1 100644
--- a/src/components/Dashboard/Production/ProductionOverview.jsx
+++ b/src/components/Dashboard/Production/ProductionOverview.jsx
@@ -1,179 +1,51 @@
-import { useEffect, useState, useCallback, useContext } from 'react'
-import {
- Descriptions,
- Space,
- Flex,
- Alert,
- Typography,
- Spin,
- message,
- Button,
- Collapse,
- Segmented,
- Card
-} from 'antd'
-import { LoadingOutlined, CaretLeftOutlined } from '@ant-design/icons'
-import { Line } from '@ant-design/charts'
-import axios from 'axios'
-import PrinterIcon from '../../Icons/PrinterIcon'
-import JobIcon from '../../Icons/JobIcon'
-import ReloadIcon from '../../Icons/ReloadIcon'
+import { useContext } from 'react'
+import { Flex } from 'antd'
import useCollapseState from '../hooks/useCollapseState'
+import StatsDisplay from '../common/StatsDisplay'
+import HistoryDisplay from '../common/HistoryDisplay'
+import InfoCollapse from '../common/InfoCollapse'
+import ScrollBox from '../common/ScrollBox'
-import config from '../../../config'
-import { AuthContext } from '../context/AuthContext'
-
-const { Title, Text } = Typography
+import { ApiServerContext } from '../context/ApiServerContext'
const ProductionOverview = () => {
- const { token } = useContext(AuthContext)
- const [messageApi, contextHolder] = message.useMessage()
- const [error, setError] = useState(null)
- const [fetchPrinterStatsLoading, setFetchPrinterStatsLoading] = useState(true)
- const [chartData, setChartData] = useState([])
+ const { connected } = useContext(ApiServerContext)
+
const [collapseState, updateCollapseState] = useCollapseState(
'ProductionOverview',
{
- overview: true,
printerStats: true,
- jobStats: true
+ printerHistory: true,
+ jobStats: true,
+ productionStats: true,
+ jobStatsDetails: true
}
)
- const [timeRanges, setTimeRanges] = useState({
- overview: '24h',
- printerStats: '24h',
- jobStats: '24h'
- })
- const [stats, setStats] = useState({
- totalPrinters: 0,
- activePrinters: 0,
- totalJobs: 0,
- activeJobs: 0,
- completedJobs: 0,
- printerStatus: {
- idle: 0,
- printing: 0,
- error: 0,
- offline: 0
- }
- })
-
- const fetchAllStats = useCallback(async () => {
- await fetchPrinterStats()
- await fetchJobstats()
- await fetchChartData()
- }, [])
-
- const fetchPrinterStats = async () => {
- try {
- setFetchPrinterStatsLoading(true)
- const response = await axios.get(`${config.backendUrl}/printers/stats`, {
- headers: {
- Accept: 'application/json',
- Authorization: `Bearer ${token}`
- }
- })
- const printStats = response.data
- setStats((prev) => ({ ...prev, printers: printStats }))
- setError(null)
- } catch (err) {
- console.error(err)
- setError('Failed to fetch printer details')
- messageApi.error('Failed to fetch printer details')
- } finally {
- setFetchPrinterStatsLoading(false)
- }
- }
-
- const fetchJobstats = async () => {
- try {
- setFetchPrinterStatsLoading(true)
- const response = await axios.get(`${config.backendUrl}/jobs/stats`, {
- headers: {
- Accept: 'application/json',
- Authorization: `Bearer ${token}`
- }
- })
- const jobstats = response.data
- setStats((prev) => ({ ...prev, jobs: jobstats }))
- setError(null)
- } catch (err) {
- console.error(err)
- setError('Failed to fetch printer details')
- messageApi.error('Failed to fetch printer details')
- } finally {
- setFetchPrinterStatsLoading(false)
- }
- }
-
- const fetchChartData = async () => {
- try {
- const response = await axios.get(`${config.backendUrl}/stats/history`, {
- headers: {
- Accept: 'application/json',
- Authorization: `Bearer ${token}`
- }
- })
- setChartData(response.data)
- } catch (err) {
- console.error(err)
- console.error('Failed to fetch chart data:', err)
- }
- }
-
- useEffect(() => {
- if (token != null) {
- fetchAllStats()
- }
- }, [fetchAllStats, token])
-
- if (fetchPrinterStatsLoading || fetchPrinterStatsLoading) {
- return (
-
- } />
-
- )
- }
-
- if (error || !stats) {
- return (
-
- {error || 'Printer not found'}
- } onClick={fetchAllStats}>
- Retry
-
-
- )
+ if (!connected) {
+ return null
}
return (
-
- {contextHolder}
-
- updateCollapseState('overview', keys.length > 0)}
- expandIcon={({ isActive }) => (
-
- )}
- className='no-h-padding-collapse no-t-padding-collapse'
- >
-
- Status Overview
-
+
+
+
+
+ updateCollapseState('printerStats', isActive)
}
- key='1'
+ className='no-t-padding-collapse'
+ collapseKey='printerStats'
>
{
wrap='wrap'
align='flex-start'
>
-
- Ready
-
-
-
- {(stats.printers.standby || 0) +
- (stats.printers.complete || 0)}
-
-
-
- }
- />
-
- Printing
-
-
-
- {stats.printers.printing || 0}
-
-
-
- }
- />
-
- Queued
-
-
-
- {stats.jobs.queued || 0}
-
-
-
- }
- />
-
- Printing
-
-
-
- {stats.jobs.printing || 0}
-
-
-
- }
- />
-
- Failed
-
-
-
- {(stats.jobs.failed || 0) + (stats.jobs.cancelled || 0)}
-
-
-
- }
- />
-
- Complete
-
-
-
- {stats.jobs.complete || 0}
-
-
-
- }
- />
+
-
-
+
-
-
-
- updateCollapseState('printerStats', keys.length > 0)
- }
- expandIcon={({ isActive }) => (
-
- )}
- className='no-h-padding-collapse'
+ updateCollapseState('jobStats', isActive)}
+ className='no-t-padding-collapse'
+ collapseKey='jobStats'
+ >
+
-
-
- Production Statistics
-
-
- setTimeRanges((prev) => ({
- ...prev,
- printerStats: value
- }))
- }
- size='small'
- />
-
+
+
+
+
+
+
+
+ updateCollapseState('printerHistory', isActive)
}
- key='2'
+ collapseKey='printerHistory'
+ canCollapse={false}
>
-
-
-
-
-
-
- {stats.totalPrinters}
-
-
- {stats.activePrinters}
-
-
- {stats.activePrinters}
-
-
-
-
-
-
-
-
- updateCollapseState('jobStats', keys.length > 0)
- }
- expandIcon={({ isActive }) => (
-
- )}
- className='no-h-padding-collapse'
- >
-
-
- Job Statistics
-
-
- setTimeRanges((prev) => ({ ...prev, jobStats: value }))
- }
- size='small'
- />
-
+
+
+
+
+
+ updateCollapseState('jobHistory', isActive)
}
- key='3'
+ canCollapse={false}
+ collapseKey='jobHistory'
>
-
-
-
-
-
-
- {stats.totalJobs}
-
-
- {stats.activeJobs}
-
-
- {stats.completedJobs}
-
-
-
-
-
+
+
+
-
-
+
+
)
}
diff --git a/src/components/Dashboard/common/HistoryDisplay.jsx b/src/components/Dashboard/common/HistoryDisplay.jsx
new file mode 100644
index 0000000..b57e1d8
--- /dev/null
+++ b/src/components/Dashboard/common/HistoryDisplay.jsx
@@ -0,0 +1,323 @@
+import { useEffect, useState, useContext, useMemo } from 'react'
+import { Card, Spin, Segmented, Flex } from 'antd'
+import { LoadingOutlined } from '@ant-design/icons'
+import { Column } from '@ant-design/charts'
+import PropTypes from 'prop-types'
+import { getModelByName } from '../../../database/ObjectModels'
+import { ApiServerContext } from '../context/ApiServerContext'
+import { AuthContext } from '../context/AuthContext'
+import dayjs from 'dayjs'
+import { useThemeContext } from '../context/ThemeContext'
+
+const HistoryDisplay = ({ objectType, startDate, endDate, styles }) => {
+ 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 { isDarkMode, getColors } = useThemeContext()
+ // Calculate dates based on selected time range or provided props
+ const { defaultStartDate, defaultEndDate } = useMemo(() => {
+ const now = new Date()
+
+ // If startDate/endDate props are provided, use them (ignore time range)
+ if (startDate || endDate) {
+ return {
+ defaultStartDate:
+ startDate || new Date(now.getTime() - 24 * 60 * 60 * 1000),
+ defaultEndDate: endDate || now
+ }
+ }
+
+ // Otherwise, calculate based on selected time range
+ const hoursMap = {
+ '8hrs': 8,
+ '12hrs': 12,
+ '24hrs': 24,
+ '4hrs': 4,
+ '1hrs': 1
+ }
+ const hours = hoursMap[timeRange] || 1
+ return {
+ defaultStartDate: new Date(now.getTime() - hours * 60 * 60 * 1000),
+ defaultEndDate: now
+ }
+ }, [startDate, endDate, timeRange])
+
+ useEffect(() => {
+ if (!objectType || !getModelHistory || !token || !connected) {
+ return
+ }
+
+ const fetchHistory = async () => {
+ try {
+ setLoading(true)
+ setError(null)
+ const fetchedHistory = await getModelHistory(
+ objectType,
+ defaultStartDate,
+ defaultEndDate
+ )
+ if (fetchedHistory) {
+ setHistoryData(fetchedHistory)
+ } else {
+ setError('Failed to fetch history')
+ }
+ } catch (err) {
+ console.error('Error fetching history:', err)
+ setError('Failed to fetch history')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ fetchHistory()
+ }, [
+ objectType,
+ getModelHistory,
+ token,
+ connected,
+ defaultStartDate,
+ defaultEndDate
+ ])
+
+ if (!objectType) {
+ return null
+ }
+
+ const model = getModelByName(objectType)
+
+ if (!model) {
+ return null
+ }
+
+ const modelStats = model.stats || []
+
+ if (modelStats.length === 0) {
+ return null
+ }
+
+ if (error || !Array.isArray(historyData)) {
+ return null
+ }
+
+ /**
+ * Extracts the stat key from history data based on stat definition name
+ * e.g., "states.standby.count" -> "standby", "standbyPrinters" -> "standby"
+ * The history data keys are the rollup names (like "standby", "printing")
+ */
+ const extractStatKey = (statName) => {
+ // Handle dot notation paths like "states.standby.count"
+ if (statName.includes('.')) {
+ const parts = statName.split('.')
+ // Extract the middle part (e.g., "standby" from "states.standby.count")
+ // Skip first part (usually "states") and last part (usually "count")
+ if (parts.length >= 2) {
+ return parts[parts.length - 2] // Get second-to-last part
+ }
+ return parts[parts.length - 1] // Fallback to last part
+ }
+ // Remove common suffixes
+ return statName
+ .replace(/Printers?$/i, '')
+ .replace(/Jobs?$/i, '')
+ .toLowerCase()
+ }
+
+ /**
+ * Gets a nested value from an object using dot notation
+ */
+ const getNestedValue = (obj, path) => {
+ if (!obj || !path) return undefined
+ const keys = path.split('.')
+ let value = obj
+ for (const key of keys) {
+ if (value === null || value === undefined) return undefined
+ value = value[key]
+ }
+ return value
+ }
+
+ /**
+ * Extracts numeric value from a rollup object
+ * Handles both old format (number) and new format ({count: 0}, {sum: 100}, {avg: 5.5})
+ */
+ const extractValue = (value, preferredOperation) => {
+ if (typeof value === 'number') {
+ return value
+ }
+ if (value && typeof value === 'object') {
+ // If a preferred operation is specified and exists, use it
+ if (preferredOperation && value[preferredOperation] !== undefined) {
+ return value[preferredOperation]
+ }
+
+ // Handle new format: {count: 0}, {sum: 100}, {avg: 5.5}
+ const operationKeys = ['count', 'sum', 'avg']
+ for (const key of operationKeys) {
+ if (value[key] !== undefined) {
+ return value[key]
+ }
+ }
+ // Fallback: return first numeric value found
+ const firstValue = Object.values(value)[0]
+ return typeof firstValue === 'number' ? firstValue : 0
+ }
+ return 0
+ }
+
+ /**
+ * Gets the stat value from history data point
+ */
+ const getStatValueFromPoint = (point, statDef) => {
+ // Handle special combined stats (support both 'combine' and 'sum' properties)
+ const combineList = statDef.combine || statDef.sum
+ if (combineList) {
+ return combineList.reduce((sum, statName) => {
+ const tempStatDef = { name: statName }
+ const value = getStatValueFromPoint(point, tempStatDef)
+ return sum + (value || 0)
+ }, 0)
+ }
+
+ const statName = statDef.name
+
+ // Determine operation from stat name
+ let operation
+ if (statName.endsWith('.count')) operation = 'count'
+ else if (statName.endsWith('.sum')) operation = 'sum'
+ else if (statName.endsWith('.avg')) operation = 'avg'
+
+ let value
+
+ // Extract the key that matches the history data structure
+ // History data has keys like "standby", "printing" (the rollup names)
+ const statKey = extractStatKey(statName)
+ value = point[statKey]
+
+ // If not found with extracted key, try direct access with full path
+ if (value === undefined && statName.includes('.')) {
+ value = getNestedValue(point, statName)
+ }
+
+ // If still not found, try with original stat name
+ if (value === undefined) {
+ value = point[statName]
+ }
+
+ return extractValue(value, operation)
+ }
+
+ // Transform data for the chart using model stats configuration
+ // Each data point has: { date: '2024-01-01', standby: {count: 0}, printing: {count: 0}, ... }
+ // We need to transform to: [{ date: '2024-01-01', category: 'Ready', value: 0 }, ...]
+ const chartData = historyData.flatMap((point) => {
+ return modelStats.map((statDef) => {
+ const statValue = getStatValueFromPoint(point, statDef)
+ const label = statDef.label || statDef.name
+ return {
+ date: point.date,
+ dateFormatted: dayjs(point.date).format('DD/MM HH:mm'),
+ category: label,
+ value: statValue || 0
+ }
+ })
+ })
+
+ // Sort by date
+ chartData.sort((a, b) => {
+ return new Date(a.date) - new Date(b.date)
+ })
+
+ const themeColors = getColors()
+
+ // Create color mapping from model stats
+ const colors = {
+ success: themeColors.colorSuccess,
+ info: themeColors.colorInfo,
+ error: themeColors.colorError,
+ warning: themeColors.colorWarning,
+ default: '#8c8c8c'
+ }
+
+ // Build color range array based on model stats order
+ const colorRange = modelStats.map((stat) => {
+ if (stat.color) {
+ return colors[stat.color] || stat.color
+ }
+ return colors.default
+ })
+
+ const config = {
+ data: chartData,
+ xField: 'dateFormatted',
+ yField: 'value',
+ theme: {
+ type: isDarkMode ? 'dark' : 'light',
+ fontFamily: '"DM Sans", -apple-system, BlinkMacSystemFont, sans-serif'
+ },
+ stack: true,
+ colorField: 'category',
+ columnWidthRatio: 1,
+ scale: {
+ color: {
+ range: colorRange
+ }
+ },
+ smooth: true,
+ interaction: {
+ tooltip: {
+ marker: false
+ }
+ },
+ legend: {
+ position: 'top'
+ },
+ animation: {
+ appear: {
+ animation: 'wave-in',
+ duration: 1000
+ }
+ }
+ }
+
+ return (
+ }>
+
+ {!startDate && !endDate && (
+
+
+
+ )}
+
+
+
+ )
+}
+
+HistoryDisplay.propTypes = {
+ objectType: PropTypes.string.isRequired,
+ startDate: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.instanceOf(Date)
+ ]),
+ styles: PropTypes.object,
+ endDate: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)])
+}
+
+export default HistoryDisplay
diff --git a/src/components/Dashboard/common/InfoCollapse.jsx b/src/components/Dashboard/common/InfoCollapse.jsx
index 6606be2..bb5ec40 100644
--- a/src/components/Dashboard/common/InfoCollapse.jsx
+++ b/src/components/Dashboard/common/InfoCollapse.jsx
@@ -11,17 +11,18 @@ const InfoCollapse = ({
active,
onToggle,
className = '',
- collapseKey = 'default'
+ collapseKey = 'default',
+ canCollapse = true
}) => {
return (
onToggle(keys.length > 0)}
- expandIcon={({ isActive }) => (
-
- )}
+ expandIcon={({ isActive }) =>
+ canCollapse ? : null
+ }
className={`no-h-padding-collapse ${className}`}
items={[
{
@@ -48,7 +49,8 @@ InfoCollapse.propTypes = {
active: PropTypes.bool.isRequired,
onToggle: PropTypes.func.isRequired,
className: PropTypes.string,
- collapseKey: PropTypes.string
+ collapseKey: PropTypes.string,
+ canCollapse: PropTypes.bool
}
export default InfoCollapse
diff --git a/src/components/Dashboard/common/StatsDisplay.jsx b/src/components/Dashboard/common/StatsDisplay.jsx
new file mode 100644
index 0000000..988d5f6
--- /dev/null
+++ b/src/components/Dashboard/common/StatsDisplay.jsx
@@ -0,0 +1,319 @@
+import { useEffect, useState, useContext, useRef } from 'react'
+import { Flex, Alert, Card, Typography, Skeleton } from 'antd'
+import PropTypes from 'prop-types'
+import { getModelByName } from '../../../database/ObjectModels'
+import { ApiServerContext } from '../context/ApiServerContext'
+import { AuthContext } from '../context/AuthContext'
+import { round } from '../utils/Utils'
+
+const { Text } = Typography
+
+/**
+ * Maps stat names to Alert types for visual indication
+ */
+const getAlertType = (statName) => {
+ const name = statName.toLowerCase()
+
+ // Success states
+ if (
+ name.includes('complete') ||
+ name.includes('ready') ||
+ name.includes('standby') ||
+ name.includes('success')
+ ) {
+ return 'success'
+ }
+
+ // Error states
+ if (
+ name.includes('error') ||
+ name.includes('failed') ||
+ name.includes('cancelled')
+ ) {
+ return 'error'
+ }
+
+ // Warning states
+ if (
+ name.includes('queued') ||
+ name.includes('pending') ||
+ name.includes('waiting')
+ ) {
+ return 'warning'
+ }
+
+ if (name.includes('printing')) {
+ return 'info'
+ }
+
+ // Default states
+ return 'default'
+}
+
+/**
+ * Gets a nested value from an object using dot notation
+ * e.g., getNestedValue(obj, 'states.ready') -> obj.states.ready
+ */
+const getNestedValue = (obj, path) => {
+ if (!obj || !path) return undefined
+ const keys = path.split('.')
+ let value = obj
+ for (const key of keys) {
+ if (value === null || value === undefined) return undefined
+ value = value[key]
+ }
+ return value
+}
+
+/**
+ * Extracts numeric value from a rollup object
+ * Handles both old format (number) and new format ({count: 0}, {sum: 100}, {avg: 5.5})
+ */
+const extractRollupValue = (value) => {
+ if (typeof value === 'number') {
+ return value
+ }
+ if (value && typeof value === 'object') {
+ // Handle new format: {count: 0}, {sum: 100}, {avg: 5.5}
+ const operationKeys = ['count', 'sum', 'avg']
+ for (const key of operationKeys) {
+ if (value[key] !== undefined) {
+ return value[key]
+ }
+ }
+ // Fallback: return first numeric value found
+ const firstValue = Object.values(value)[0]
+ return typeof firstValue === 'number' ? firstValue : 0
+ }
+ return 0
+}
+
+/**
+ * Gets the stat value from the stats data object
+ * Handles both flattened and nested structures, including dot notation paths
+ * Now also handles new format with operation objects ({count: 0}, {sum: 100}, etc.)
+ */
+const getStatValue = (stats, statName, statDef) => {
+ if (!stats) return 0
+
+ // Handle special combined stats (support both 'combine' and 'sum' properties)
+ const combineList = statDef.combine || statDef.sum
+ if (combineList) {
+ return combineList.reduce((sum, name) => {
+ const value = getStatValue(stats, name, {})
+ return sum + (value || 0)
+ }, 0)
+ }
+
+ // Handle dot notation paths (e.g., "states.ready")
+ if (statName.includes('.')) {
+ const value = getNestedValue(stats, statName)
+ return extractRollupValue(value)
+ }
+
+ // Try direct access first (flattened structure)
+ if (stats[statName] !== undefined) {
+ return extractRollupValue(stats[statName])
+ }
+
+ // Try nested structure (stats.states.statName)
+ if (stats.states && stats.states[statName] !== undefined) {
+ return extractRollupValue(stats.states[statName])
+ }
+
+ // Try with objectType prefix removed (e.g., "standbyPrinters" -> "standby")
+ const simplifiedName = statName.replace(
+ /^(standby|printing|error|offline|queued|complete|failed|cancelled|draft)/i,
+ (match) => {
+ return match.toLowerCase()
+ }
+ )
+ if (stats[simplifiedName] !== undefined) {
+ return extractRollupValue(stats[simplifiedName])
+ }
+
+ if (stats.states && stats.states[simplifiedName] !== undefined) {
+ return extractRollupValue(stats.states[simplifiedName])
+ }
+
+ return 0
+}
+
+/**
+ * Extracts the base stat name from model stat names
+ * e.g., "standbyPrinters" -> "standby", "printingPrinters" -> "printing"
+ * Preserves dot notation paths (e.g., "states.ready" -> "states.ready")
+ */
+const extractBaseStatName = (statName) => {
+ // If it's a dot notation path, return as-is
+ if (statName.includes('.')) {
+ return statName
+ }
+
+ // Remove common suffixes
+ const baseName = statName
+ .replace(/Printers?$/i, '')
+ .replace(/Jobs?$/i, '')
+ .toLowerCase()
+
+ return baseName
+}
+
+const StatsDisplay = ({ objectType, stats: statsProp }) => {
+ const { getModelStats, connected, subscribeToModelStats } =
+ useContext(ApiServerContext)
+ const { token } = useContext(AuthContext)
+ const [stats, setStats] = useState(statsProp || null)
+ const [loading, setLoading] = useState(!statsProp)
+ const [error, setError] = useState(null)
+ const initializedRef = useRef(false)
+
+ useEffect(() => {
+ // If stats are provided as prop, use them and don't fetch
+ if (statsProp) {
+ setStats(statsProp)
+ setLoading(false)
+ initializedRef.current = true
+ return
+ }
+
+ // Otherwise, fetch stats
+ if (
+ !objectType ||
+ !getModelStats ||
+ !token ||
+ !connected ||
+ initializedRef.current == true
+ ) {
+ return
+ }
+
+ const fetchStats = async () => {
+ try {
+ setLoading(true)
+ setError(null)
+ const fetchedStats = await getModelStats(objectType)
+ if (fetchedStats) {
+ setStats(fetchedStats)
+ } else {
+ setError('Failed to fetch stats')
+ }
+ } catch (err) {
+ console.error('Error fetching stats:', err)
+ setError('Failed to fetch stats')
+ } finally {
+ setLoading(false)
+ initializedRef.current = true
+ }
+ }
+
+ fetchStats()
+ }, [objectType, getModelStats, token, connected, statsProp])
+
+ // Subscribe to real-time stats updates
+ useEffect(() => {
+ // Only subscribe if stats are not provided as prop and we have the required dependencies
+ if (!objectType || !connected || !subscribeToModelStats) {
+ return
+ }
+
+ const statsUnsubscribe = subscribeToModelStats(
+ objectType,
+ (updatedStats) => {
+ setStats(updatedStats)
+ }
+ )
+
+ return () => {
+ if (statsUnsubscribe) statsUnsubscribe()
+ }
+ }, [objectType, connected, subscribeToModelStats, statsProp])
+
+ if (!objectType) {
+ return null
+ }
+
+ const model = getModelByName(objectType)
+
+ if (!model) {
+ return null
+ }
+
+ const Icon = model.icon
+ const modelStats = model.stats || []
+
+ if (modelStats.length === 0) {
+ return null
+ }
+
+ if (error) {
+ return null
+ }
+
+ return (
+
+ {modelStats.map((statDef) => {
+ const baseStatName = extractBaseStatName(statDef.name)
+ var statValue = getStatValue(stats, baseStatName, statDef)
+ const alertType = getAlertType(statDef.name)
+ const label = statDef.label || statDef.name
+
+ if (statDef?.roundNumber) {
+ statValue = round(statValue, statDef?.roundNumber)
+ }
+
+ const content = (
+
+ {label}
+
+ {Icon && }
+ {loading ? (
+
+
+
+ ) : (
+
+ {statDef?.prefix}
+ {statValue}
+ {statDef?.suffix}
+
+ )}
+
+
+ )
+
+ if (alertType === 'default') {
+ return (
+
+ {content}
+
+ )
+ }
+
+ return (
+
+ )
+ })}
+
+ )
+}
+
+StatsDisplay.propTypes = {
+ objectType: PropTypes.string.isRequired,
+ stats: PropTypes.object // Optional - if not provided, will fetch automatically
+}
+
+export default StatsDisplay
diff --git a/src/database/models/Job.js b/src/database/models/Job.js
index a80670a..e49bb2b 100644
--- a/src/database/models/Job.js
+++ b/src/database/models/Job.js
@@ -37,14 +37,7 @@ export const Job = {
url: (_id) => `/dashboard/production/jobs/info?jobId=${_id}&action=reload`
}
],
- columns: [
- '_id',
- 'gcodeFile',
- 'quantity',
- 'state',
-
- 'createdAt'
- ],
+ columns: ['_id', 'gcodeFile', 'quantity', 'state', 'createdAt'],
filters: ['state', '_id', 'gcodeFile', 'quantity'],
sorters: ['createdAt', 'state', 'quantity', 'gcodeFile'],
properties: [
@@ -115,5 +108,38 @@ export const Job = {
required: true,
showHyperlink: true
}
+ ],
+ stats: [
+ {
+ name: 'draft.count',
+ label: 'Draft',
+ type: 'number',
+ color: 'default'
+ },
+ {
+ name: 'queued.count',
+ label: 'Queued',
+ type: 'number',
+ color: 'warning'
+ },
+ {
+ name: 'printing.count',
+ label: 'Printing',
+ type: 'number',
+ color: 'info'
+ },
+ {
+ name: 'failed.count',
+ label: 'Failed',
+ type: 'number',
+ combine: ['failed.count', 'cancelled.count'],
+ color: 'error'
+ },
+ {
+ name: 'complete.count',
+ label: 'Complete',
+ type: 'number',
+ color: 'success'
+ }
]
}
diff --git a/src/database/models/Printer.js b/src/database/models/Printer.js
index 30fc7a5..e845d85 100644
--- a/src/database/models/Printer.js
+++ b/src/database/models/Printer.js
@@ -364,5 +364,32 @@ export const Printer = {
required: false,
readOnly: true
}
+ ],
+ stats: [
+ {
+ name: 'standby.count',
+ label: 'Ready',
+ type: 'number',
+ sum: ['standby.count', 'complete.count'],
+ color: 'success'
+ },
+ {
+ name: 'printing.count',
+ label: 'Printing',
+ type: 'number',
+ color: 'info'
+ },
+ {
+ name: 'error.count',
+ label: 'Error',
+ type: 'number',
+ color: 'error'
+ },
+ {
+ name: 'offline.count',
+ label: 'Offline',
+ type: 'number',
+ color: 'default'
+ }
]
}