403 lines
13 KiB
JavaScript

import React, { useState, useEffect, useContext } from 'react'
import { useLocation } from 'react-router-dom'
import axios from 'axios'
import {
Descriptions,
Spin,
Space,
Button,
message,
Progress,
Typography,
Collapse,
Flex,
Dropdown,
Popover,
Checkbox,
Card
} from 'antd'
import { LoadingOutlined, CaretLeftOutlined } from '@ant-design/icons'
import TimeDisplay from '../../common/TimeDisplay'
import JobState from '../../common/JobState'
import IdText from '../../common/IdText'
import SubJobsTree from '../../common/SubJobsTree'
import { SocketContext } from '../../context/SocketContext'
import GCodeFileIcon from '../../../Icons/GCodeFileIcon'
import ReloadIcon from '../../../Icons/ReloadIcon'
import useCollapseState from '../../hooks/useCollapseState'
import config from '../../../../config'
import AuditLogTable from '../../common/AuditLogTable'
import DashboardNotes from '../../common/DashboardNotes'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon'
import JobIcon from '../../../Icons/JobIcon'
import AuditLogIcon from '../../../Icons/AuditLogIcon'
import NoteIcon from '../../../Icons/NoteIcon'
const { Title, Text } = Typography
const JobInfo = () => {
const [jobData, setJobData] = useState(null)
const [fetchLoading, setFetchLoading] = useState(true)
const [error, setError] = useState(null)
const location = useLocation()
const [messageApi] = message.useMessage()
const jobId = new URLSearchParams(location.search).get('jobId')
const { socket } = useContext(SocketContext)
const [collapseState, updateCollapseState] = useCollapseState('JobInfo', {
info: true,
subJobs: true,
notes: true,
auditLogs: true
})
useEffect(() => {
if (jobId) {
fetchJobDetails()
}
}, [jobId])
useEffect(() => {
if (socket && jobId) {
socket.on('notify_job_update', (updateData) => {
if (updateData._id === jobId) {
setJobData((prevData) => {
if (!prevData) return prevData
return {
...prevData,
state: updateData.state,
...updateData
}
})
}
})
}
return () => {
if (socket) {
socket.off('notify_job_update')
}
}
}, [socket, jobId])
const fetchJobDetails = async () => {
try {
setFetchLoading(true)
const response = await axios.get(`${config.backendUrl}/jobs/${jobId}`, {
headers: {
Accept: 'application/json'
},
withCredentials: true // Important for including cookies
})
setJobData(response.data)
setError(null)
} catch (err) {
setError('Failed to fetch print job details')
messageApi.error('Failed to fetch print job details')
} finally {
setFetchLoading(false)
}
}
const getViewDropdownItems = () => {
const sections = [
{ key: 'info', label: 'Job Information' },
{ key: 'subJobs', label: 'Sub Jobs' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]
return (
<Flex vertical>
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
{sections.map((section) => (
<Checkbox
checked={collapseState[section.key]}
key={section.key}
onChange={(e) => {
updateCollapseState(section.key, e.target.checked)
}}
>
{section.label}
</Checkbox>
))}
</Flex>
</Flex>
)
}
const actionItems = {
items: [
{
label: 'Reload Job',
key: 'reload',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'edit') {
// TODO: Implement edit functionality
messageApi.info('Edit functionality coming soon')
} else if (key === 'reload') {
fetchJobDetails()
}
}
}
return (
<>
<Flex
gap='large'
vertical='true'
style={{ height: '100%', minHeight: 0 }}
>
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<Popover
content={getViewDropdownItems()}
placement='bottomLeft'
arrow={false}
>
<Button>View</Button>
</Popover>
</Space>
</Flex>
{error ? (
<Space
direction='vertical'
style={{ width: '100%', textAlign: 'center' }}
>
<p>{error || 'Print job not found'}</p>
<Button icon={<ReloadIcon />} onClick={fetchJobDetails}>
Retry
</Button>
</Space>
) : (
<div style={{ height: '100%', overflow: 'auto' }}>
<Flex vertical style={{ flexGrow: 1 }} gap={'large'}>
<Collapse
ghost
expandIconPosition='end'
activeKey={collapseState.info ? ['info'] : []}
onChange={(keys) =>
updateCollapseState('info', keys.length > 0)
}
expandIcon={({ isActive }) => (
<CaretLeftOutlined rotate={isActive ? -90 : 0} />
)}
className='no-h-padding-collapse no-t-padding-collapse'
>
<Collapse.Panel
header={
<Flex align='center' gap={'middle'}>
<InfoCircleIcon />
<Title level={5} style={{ margin: 0 }}>
Job Information
</Title>
</Flex>
}
key='info'
>
<Spin spinning={fetchLoading} indicator={<LoadingOutlined />}>
<Descriptions
bordered
column={{
xs: 1,
sm: 1,
md: 1,
lg: 2,
xl: 2,
xxl: 2
}}
>
<Descriptions.Item label='ID'>
{jobData?._id ? (
<IdText id={jobData._id} type={'job'} />
) : (
<Text>n/a</Text>
)}
</Descriptions.Item>
<Descriptions.Item label='Status'>
{jobData?.state ? (
<JobState
job={jobData}
showProgress={false}
showQuantity={false}
showId={false}
/>
) : (
<Text>n/a</Text>
)}
</Descriptions.Item>
<Descriptions.Item label='GCode File Name'>
{jobData?.gcodeFile ? (
<Space>
<GCodeFileIcon />
<Text>
{jobData.gcodeFile.name || 'Not specified'}
</Text>
</Space>
) : (
<Text>n/a</Text>
)}
</Descriptions.Item>
<Descriptions.Item label='GCode File ID'>
{jobData?.gcodeFile?._id ? (
<IdText
id={jobData.gcodeFile._id}
type={'gcodefile'}
showHyperlink={true}
/>
) : (
<Text>n/a</Text>
)}
</Descriptions.Item>
<Descriptions.Item label='Quantity'>
{jobData?.quantity ? (
<Text>{jobData.quantity}</Text>
) : (
<Text>n/a</Text>
)}
</Descriptions.Item>
<Descriptions.Item label='Created At'>
{jobData?.createdAt ? (
<TimeDisplay
dateTime={jobData.createdAt}
showSince={true}
/>
) : (
<Text>n/a</Text>
)}
</Descriptions.Item>
<Descriptions.Item label='Started At'>
{jobData?.startedAt ? (
<TimeDisplay
dateTime={jobData.startedAt}
showSince={true}
/>
) : (
<Text>n/a</Text>
)}
</Descriptions.Item>
{jobData?.state?.type === 'printing' && (
<Descriptions.Item label='Progress'>
<Progress
percent={Math.round(
(jobData.state.progress || 0) * 100
)}
/>
</Descriptions.Item>
)}
<Descriptions.Item label='Assigned Printers'>
{jobData?.printers ? (
<Text>
{jobData.printers.length} printers assigned
</Text>
) : (
<Text>n/a</Text>
)}
</Descriptions.Item>
</Descriptions>
</Spin>
</Collapse.Panel>
</Collapse>
<Collapse
ghost
expandIconPosition='end'
activeKey={collapseState.subJobs ? ['2'] : []}
onChange={(keys) =>
updateCollapseState('subJobs', keys.length > 0)
}
expandIcon={({ isActive }) => (
<CaretLeftOutlined rotate={isActive ? -90 : 0} />
)}
className='no-h-padding-collapse'
>
<Collapse.Panel
header={
<Flex align='center' gap={'middle'}>
<JobIcon />
<Title level={5} style={{ margin: 0 }}>
Sub Job Information
</Title>
</Flex>
}
key='2'
>
<SubJobsTree jobData={jobData} loading={fetchLoading} />
</Collapse.Panel>
</Collapse>
<Collapse
ghost
expandIconPosition='end'
activeKey={collapseState.notes ? ['notes'] : []}
onChange={(keys) =>
updateCollapseState('notes', keys.length > 0)
}
expandIcon={({ isActive }) => (
<CaretLeftOutlined rotate={isActive ? -90 : 0} />
)}
className='no-h-padding-collapse'
>
<Collapse.Panel
header={
<Flex align='center' gap={'middle'}>
<NoteIcon />
<Title level={5} style={{ margin: 0 }}>
Notes
</Title>
</Flex>
}
key='notes'
>
<Card>
<DashboardNotes _id={jobId} />
</Card>
</Collapse.Panel>
</Collapse>
<Collapse
ghost
expandIconPosition='end'
activeKey={collapseState.auditLogs ? ['auditLogs'] : []}
onChange={(keys) =>
updateCollapseState('auditLogs', keys.length > 0)
}
expandIcon={({ isActive }) => (
<CaretLeftOutlined rotate={isActive ? -90 : 0} />
)}
className='no-h-padding-collapse'
>
<Collapse.Panel
header={
<Flex align='center' gap={'middle'}>
<AuditLogIcon />
<Title level={5} style={{ margin: 0 }}>
Audit Logs
</Title>
</Flex>
}
key='auditLogs'
>
<AuditLogTable
items={jobData?.auditLogs || []}
loading={fetchLoading}
showTargetColumn={false}
/>
</Collapse.Panel>
</Collapse>
</Flex>
</div>
)}
</Flex>
</>
)
}
export default JobInfo