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