443 lines
14 KiB
JavaScript
443 lines
14 KiB
JavaScript
import React, { useEffect, useState, useCallback } 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 useCollapseState from '../hooks/useCollapseState'
|
|
|
|
import config from '../../../config'
|
|
|
|
const { Title, Text } = Typography
|
|
|
|
const ProductionOverview = () => {
|
|
const [messageApi, contextHolder] = message.useMessage()
|
|
const [error, setError] = useState(null)
|
|
const [fetchPrinterStatsLoading, setFetchPrinterStatsLoading] = useState(true)
|
|
const [chartData, setChartData] = useState([])
|
|
const [collapseState, updateCollapseState] = useCollapseState(
|
|
'ProductionOverview',
|
|
{
|
|
overview: true,
|
|
printerStats: true,
|
|
jobStats: 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'
|
|
},
|
|
withCredentials: true
|
|
})
|
|
const printStats = response.data
|
|
setStats((prev) => ({ ...prev, printers: printStats }))
|
|
setError(null)
|
|
} catch (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'
|
|
},
|
|
withCredentials: true
|
|
})
|
|
const jobstats = response.data
|
|
setStats((prev) => ({ ...prev, jobs: jobstats }))
|
|
setError(null)
|
|
} catch (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'
|
|
},
|
|
withCredentials: true
|
|
})
|
|
setChartData(response.data)
|
|
} catch (err) {
|
|
console.error('Failed to fetch chart data:', err)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchAllStats()
|
|
}, [fetchAllStats])
|
|
|
|
if (fetchPrinterStatsLoading || fetchPrinterStatsLoading) {
|
|
return (
|
|
<div style={{ textAlign: 'center', padding: '20px' }}>
|
|
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (error || !stats) {
|
|
return (
|
|
<Space
|
|
direction='vertical'
|
|
style={{ width: '100%', textAlign: 'center' }}
|
|
>
|
|
<p>{error || 'Printer not found'}</p>
|
|
<Button icon={<ReloadIcon />} onClick={fetchAllStats}>
|
|
Retry
|
|
</Button>
|
|
</Space>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div style={{ height: '100%', minHeight: 0, overflowY: 'auto' }}>
|
|
{contextHolder}
|
|
<Flex gap='large' vertical>
|
|
<Collapse
|
|
ghost
|
|
expandIconPosition='end'
|
|
activeKey={collapseState.overview ? ['1'] : []}
|
|
onChange={(keys) => updateCollapseState('overview', keys.length > 0)}
|
|
expandIcon={({ isActive }) => (
|
|
<CaretLeftOutlined
|
|
rotate={isActive ? 90 : 0}
|
|
style={{ paddingTop: '2px' }}
|
|
/>
|
|
)}
|
|
className='no-h-padding-collapse no-t-padding-collapse'
|
|
>
|
|
<Collapse.Panel
|
|
header={
|
|
<Title level={5} style={{ margin: 0 }}>
|
|
Status Overview
|
|
</Title>
|
|
}
|
|
key='1'
|
|
>
|
|
<Flex
|
|
justify='flex-start'
|
|
gap='middle'
|
|
wrap='wrap'
|
|
align='flex-start'
|
|
>
|
|
<Alert
|
|
type='success'
|
|
style={{ minWidth: 150 }}
|
|
description={
|
|
<Flex vertical>
|
|
<Text type='secondary'>Ready</Text>
|
|
<Flex gap={'small'}>
|
|
<PrinterIcon style={{ fontSize: 26 }} />
|
|
<Text style={{ fontSize: 28, fontWeight: 600 }}>
|
|
{(stats.printers.standby || 0) +
|
|
(stats.printers.complete || 0)}
|
|
</Text>
|
|
</Flex>
|
|
</Flex>
|
|
}
|
|
/>
|
|
<Alert
|
|
type='info'
|
|
style={{ minWidth: 150 }}
|
|
description={
|
|
<Flex vertical>
|
|
<Text type='secondary'>Printing</Text>
|
|
<Flex gap={'small'}>
|
|
<PrinterIcon style={{ fontSize: 26 }} />
|
|
<Text style={{ fontSize: 28, fontWeight: 600 }}>
|
|
{stats.printers.printing || 0}
|
|
</Text>
|
|
</Flex>
|
|
</Flex>
|
|
}
|
|
/>
|
|
<Alert
|
|
type='warning'
|
|
style={{ minWidth: 150 }}
|
|
description={
|
|
<Flex vertical>
|
|
<Text type='secondary'>Queued</Text>
|
|
<Flex gap={'small'}>
|
|
<JobIcon style={{ fontSize: 26 }} />
|
|
<Text style={{ fontSize: 28, fontWeight: 600 }}>
|
|
{stats.jobs.queued || 0}
|
|
</Text>
|
|
</Flex>
|
|
</Flex>
|
|
}
|
|
/>
|
|
<Alert
|
|
type='info'
|
|
style={{ minWidth: 150 }}
|
|
description={
|
|
<Flex vertical>
|
|
<Text type='secondary'>Printing</Text>
|
|
<Flex gap={'small'}>
|
|
<JobIcon style={{ fontSize: 26 }} />
|
|
<Text style={{ fontSize: 28, fontWeight: 600 }}>
|
|
{stats.jobs.printing || 0}
|
|
</Text>
|
|
</Flex>
|
|
</Flex>
|
|
}
|
|
/>
|
|
<Alert
|
|
type='error'
|
|
style={{ minWidth: 150 }}
|
|
description={
|
|
<Flex vertical>
|
|
<Text type='secondary'>Failed</Text>
|
|
<Flex gap={'small'}>
|
|
<JobIcon style={{ fontSize: 26 }} />
|
|
<Text style={{ fontSize: 28, fontWeight: 600 }}>
|
|
{(stats.jobs.failed || 0) + (stats.jobs.cancelled || 0)}
|
|
</Text>
|
|
</Flex>
|
|
</Flex>
|
|
}
|
|
/>
|
|
<Alert
|
|
type='success'
|
|
style={{ minWidth: 150 }}
|
|
description={
|
|
<Flex vertical>
|
|
<Text type='secondary'>Complete</Text>
|
|
<Flex gap={'small'}>
|
|
<JobIcon style={{ fontSize: 26 }} />
|
|
<Text style={{ fontSize: 28, fontWeight: 600 }}>
|
|
{stats.jobs.complete || 0}
|
|
</Text>
|
|
</Flex>
|
|
</Flex>
|
|
}
|
|
/>
|
|
</Flex>
|
|
</Collapse.Panel>
|
|
</Collapse>
|
|
|
|
<Flex gap='large' wrap='wrap'>
|
|
<Flex flex={1} vertical>
|
|
<Collapse
|
|
ghost
|
|
expandIconPosition='end'
|
|
activeKey={collapseState.printerStats ? ['2'] : []}
|
|
onChange={(keys) =>
|
|
updateCollapseState('printerStats', keys.length > 0)
|
|
}
|
|
expandIcon={({ isActive }) => (
|
|
<CaretLeftOutlined
|
|
rotate={isActive ? 90 : 0}
|
|
style={{ paddingTop: '2px' }}
|
|
/>
|
|
)}
|
|
className='no-h-padding-collapse'
|
|
>
|
|
<Collapse.Panel
|
|
header={
|
|
<Flex
|
|
align='center'
|
|
justify='space-between'
|
|
style={{ width: '100%' }}
|
|
>
|
|
<Title level={5} style={{ margin: 0 }}>
|
|
Production Statistics
|
|
</Title>
|
|
<Segmented
|
|
options={['4h', '8h', '12h', '24h']}
|
|
value={timeRanges.printerStats}
|
|
onChange={(value) =>
|
|
setTimeRanges((prev) => ({
|
|
...prev,
|
|
printerStats: value
|
|
}))
|
|
}
|
|
size='small'
|
|
/>
|
|
</Flex>
|
|
}
|
|
key='2'
|
|
>
|
|
<Flex vertical gap={'middle'}>
|
|
<Card style={{ height: 250, width: '100%' }}>
|
|
<Line
|
|
data={chartData}
|
|
xField='timestamp'
|
|
yField='value'
|
|
seriesField='type'
|
|
smooth
|
|
animation={{
|
|
appear: {
|
|
animation: 'wave-in',
|
|
duration: 1000
|
|
}
|
|
}}
|
|
point={{
|
|
size: 4,
|
|
shape: 'circle'
|
|
}}
|
|
tooltip={{
|
|
showMarkers: false
|
|
}}
|
|
legend={{
|
|
position: 'top'
|
|
}}
|
|
/>
|
|
</Card>
|
|
<Descriptions column={1} bordered>
|
|
<Descriptions.Item label='Completed'>
|
|
{stats.totalPrinters}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='Error'>
|
|
{stats.activePrinters}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='Paused'>
|
|
{stats.activePrinters}
|
|
</Descriptions.Item>
|
|
</Descriptions>
|
|
</Flex>
|
|
</Collapse.Panel>
|
|
</Collapse>
|
|
</Flex>
|
|
<Flex flex={1} vertical>
|
|
<Collapse
|
|
ghost
|
|
expandIconPosition='end'
|
|
activeKey={collapseState.jobStats ? ['3'] : []}
|
|
onChange={(keys) =>
|
|
updateCollapseState('jobStats', keys.length > 0)
|
|
}
|
|
expandIcon={({ isActive }) => (
|
|
<CaretLeftOutlined
|
|
rotate={isActive ? 90 : 0}
|
|
style={{ paddingTop: '2px' }}
|
|
/>
|
|
)}
|
|
className='no-h-padding-collapse'
|
|
>
|
|
<Collapse.Panel
|
|
header={
|
|
<Flex
|
|
align='center'
|
|
justify='space-between'
|
|
style={{ width: '100%' }}
|
|
>
|
|
<Title level={5} style={{ margin: 0 }}>
|
|
Job Statistics
|
|
</Title>
|
|
<Segmented
|
|
options={['4h', '8h', '12h', '24h']}
|
|
value={timeRanges.jobStats}
|
|
onChange={(value) =>
|
|
setTimeRanges((prev) => ({ ...prev, jobStats: value }))
|
|
}
|
|
size='small'
|
|
/>
|
|
</Flex>
|
|
}
|
|
key='3'
|
|
>
|
|
<Flex vertical gap={'middle'}>
|
|
<Card
|
|
style={{ height: 250, width: '100%', minWidth: '300px' }}
|
|
>
|
|
<Line
|
|
data={chartData}
|
|
xField='timestamp'
|
|
yField='value'
|
|
seriesField='type'
|
|
smooth
|
|
animation={{
|
|
appear: {
|
|
animation: 'wave-in',
|
|
duration: 1000
|
|
}
|
|
}}
|
|
point={{
|
|
size: 4,
|
|
shape: 'circle'
|
|
}}
|
|
tooltip={{
|
|
showMarkers: false
|
|
}}
|
|
legend={{
|
|
position: 'top'
|
|
}}
|
|
/>
|
|
</Card>
|
|
<Descriptions column={1} bordered>
|
|
<Descriptions.Item label='Completed'>
|
|
{stats.totalJobs}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='Failed'>
|
|
{stats.activeJobs}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label='Queued'>
|
|
{stats.completedJobs}
|
|
</Descriptions.Item>
|
|
</Descriptions>
|
|
</Flex>
|
|
</Collapse.Panel>
|
|
</Collapse>
|
|
</Flex>
|
|
</Flex>
|
|
</Flex>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default ProductionOverview
|