chore: prepare for webpack configuration
This commit is contained in:
parent
e56a563e98
commit
4e11392f03
738
package-lock.json
generated
738
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -60,6 +60,9 @@
|
|||||||
"eslint-plugin-react-hooks": "^4.6.2",
|
"eslint-plugin-react-hooks": "^4.6.2",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"prettier-eslint": "^16.3.0",
|
"prettier-eslint": "^16.3.0",
|
||||||
"standard": "^17.1.0"
|
"standard": "^17.1.0",
|
||||||
|
"svgo-loader": "^4.0.0",
|
||||||
|
"webpack": "^5.99.9",
|
||||||
|
"webpack-cli": "^6.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@ -1,10 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" version="1.1" viewBox="0 0 12 12" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
<svg width="100%" height="100%" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
<g transform="matrix(.14501 0 0 .14501 .60055 -.62488)">
|
<g transform="matrix(0.164595,0,0,0.164595,-0.128594,-1.66652)">
|
||||||
<path d="m50 13.094 17.219 17.531c2.5 2.563 2.969 4.313 2.969 8.344v28.375c0 6.5-3.219 9.781-9.688 9.781h-22.939c0.923-1.552 1.627-3.242 2.066-5.031h20.623c3.25 0 4.906-1.719 4.906-4.844v-28.062h-17.812c-3.906 0-5.875-1.938-5.875-5.875v-18.157h-13.906c-3.25 0-4.907 1.782-4.907 4.875v27.144c-0.816-0.122-1.652-0.175-2.5-0.175-0.858 0-1.705 0.055-2.531 0.179v-27.241c0-6.5 3.25-9.813 9.688-9.813h14.843c3.5 0 5.469 0.531 7.844 2.969zm-3.812 19.625c0 1.25 0.468 1.75 1.718 1.75h16.282l-18-18.344v16.594z" fill-rule="nonzero"/>
|
<path d="M50,13.094L67.219,30.625C69.719,33.188 70.188,34.938 70.188,38.969L70.188,67.344C70.188,73.844 66.969,77.125 60.5,77.125L37.561,77.125C38.484,75.573 39.188,73.883 39.627,72.094L60.25,72.094C63.5,72.094 65.156,70.375 65.156,67.25L65.156,39.188L47.344,39.188C43.438,39.188 41.469,37.25 41.469,33.313L41.469,15.156L27.563,15.156C24.313,15.156 22.656,16.938 22.656,20.031L22.656,47.175C21.84,47.053 21.004,47 20.156,47C19.298,47 18.451,47.055 17.625,47.179L17.625,19.938C17.625,13.438 20.875,10.125 27.313,10.125L42.156,10.125C45.656,10.125 47.625,10.656 50,13.094ZM46.188,32.719C46.188,33.969 46.656,34.469 47.906,34.469L64.188,34.469L46.188,16.125L46.188,32.719Z" style="fill-rule:nonzero;"/>
|
||||||
<g transform="matrix(6.896 0 0 6.896 .57772 4.3091)">
|
<g transform="matrix(6.89596,0,0,6.89596,0.577725,4.30912)">
|
||||||
<path d="m5.141 9.114c0 1.255-1.056 2.302-2.302 2.302-1.264 0-2.302-1.038-2.302-2.302 0-1.265 1.038-2.302 2.302-2.302s2.302 1.037 2.302 2.302zm-3.473 0.174c0 0.782 0.448 1.252 1.209 1.252 0.677 0 1.133-0.41 1.133-1.015v-0.166c0-0.255-0.116-0.37-0.381-0.37h-0.493c-0.176 0-0.271 0.082-0.271 0.233 0 0.153 0.097 0.237 0.271 0.237h0.248v0.117c0 0.251-0.193 0.419-0.481 0.419-0.369 0-0.572-0.251-0.572-0.709v-0.331c0-0.464 0.199-0.707 0.578-0.707 0.26 0 0.41 0.156 0.573 0.319 0.058 0.058 0.118 0.085 0.197 0.085 0.159 0 0.269-0.109 0.269-0.268 0-0.158-0.119-0.331-0.299-0.462-0.201-0.15-0.476-0.235-0.788-0.235-0.739 0-1.193 0.476-1.193 1.236v0.365z" fill-rule="nonzero"/>
|
<path d="M5.141,9.114C5.141,10.369 4.085,11.416 2.839,11.416C1.575,11.416 0.537,10.378 0.537,9.114C0.537,7.849 1.575,6.812 2.839,6.812C4.103,6.812 5.141,7.849 5.141,9.114ZM1.668,9.288C1.668,10.07 2.116,10.54 2.877,10.54C3.554,10.54 4.01,10.13 4.01,9.525L4.01,9.359C4.01,9.104 3.894,8.989 3.629,8.989L3.136,8.989C2.96,8.989 2.865,9.071 2.865,9.222C2.865,9.375 2.962,9.459 3.136,9.459L3.384,9.459L3.384,9.576C3.384,9.827 3.191,9.995 2.903,9.995C2.534,9.995 2.331,9.744 2.331,9.286L2.331,8.955C2.331,8.491 2.53,8.248 2.909,8.248C3.169,8.248 3.319,8.404 3.482,8.567C3.54,8.625 3.6,8.652 3.679,8.652C3.838,8.652 3.948,8.543 3.948,8.384C3.948,8.226 3.829,8.053 3.649,7.922C3.448,7.772 3.173,7.687 2.861,7.687C2.122,7.687 1.668,8.163 1.668,8.923L1.668,9.288Z" style="fill-rule:nonzero;"/>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.1 KiB |
BIN
src/assets/icons/jobicon.afdesign
Normal file
BIN
src/assets/icons/jobicon.afdesign
Normal file
Binary file not shown.
21
src/assets/icons/jobicon.svg
Normal file
21
src/assets/icons/jobicon.svg
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(1,0,0,1,12.4182,0)">
|
||||||
|
<g transform="matrix(0.83795,0,0,0.83795,-2.8271,-3.06585)">
|
||||||
|
<path d="M53.485,22.46L53.485,69.739C53.485,76.482 49.963,80.036 43.319,80.036L10.16,80.036C3.517,80.036 0,76.482 0,69.739L0,22.46C0,15.717 3.517,12.164 10.16,12.164L10.52,12.164C10.45,12.571 10.417,12.998 10.417,13.44L10.417,16.721C10.417,17.219 10.46,17.699 10.543,18.157L10.427,18.157C7.509,18.157 5.993,19.749 5.993,22.523L5.993,69.677C5.993,72.476 7.509,74.043 10.427,74.043L43.058,74.043C45.971,74.043 47.492,72.476 47.492,69.677L47.492,22.523C47.492,19.749 45.971,18.157 43.058,18.157L42.937,18.157C43.02,17.699 43.063,17.219 43.063,16.721L43.063,13.44C43.063,12.998 43.029,12.571 42.959,12.164L43.319,12.164C49.963,12.164 53.485,15.717 53.485,22.46Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.83795,0,0,0.83795,-2.8271,-3.06585)">
|
||||||
|
<path d="M17.109,19.927L36.371,19.927C38.251,19.927 39.384,18.726 39.384,16.721L39.384,13.44C39.384,11.429 38.251,10.234 36.371,10.234L33.721,10.234C33.485,6.601 30.469,3.659 26.743,3.659C23.016,3.659 20,6.601 19.764,10.234L17.109,10.234C15.234,10.234 14.101,11.429 14.101,13.44L14.101,16.721C14.101,18.726 15.234,19.927 17.109,19.927ZM26.743,13.248C25.22,13.248 23.984,11.981 23.984,10.495C23.984,8.952 25.22,7.717 26.743,7.717C28.265,7.717 29.495,8.952 29.495,10.495C29.495,11.981 28.265,13.248 26.743,13.248Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.83795,0,0,0.83795,-2.8271,-3.06585)">
|
||||||
|
<path d="M13.473,60.281L27.067,60.281C28.27,60.281 29.263,59.294 29.263,58.085C29.263,56.882 28.301,55.9 27.067,55.9L13.473,55.9C12.244,55.9 11.251,56.882 11.251,58.085C11.251,59.294 12.27,60.281 13.473,60.281Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.83795,0,0,0.83795,-2.8271,-3.06585)">
|
||||||
|
<path d="M13.473,48.622L40.018,48.622C41.252,48.622 42.234,47.64 42.234,46.406C42.234,45.197 41.246,44.241 40.018,44.241L13.473,44.241C12.244,44.241 11.251,45.197 11.251,46.406C11.251,47.64 12.233,48.622 13.473,48.622Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.83795,0,0,0.83795,-2.8271,-3.06585)">
|
||||||
|
<path d="M13.473,37.562L40.018,37.562C41.221,37.562 42.234,36.569 42.234,35.366C42.234,34.157 41.221,33.144 40.018,33.144L13.473,33.144C12.27,33.144 11.251,34.157 11.251,35.366C11.251,36.569 12.27,37.562 13.473,37.562Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.9 KiB |
BIN
src/assets/icons/plusminusicon.afdesign
Normal file
BIN
src/assets/icons/plusminusicon.afdesign
Normal file
Binary file not shown.
7
src/assets/icons/plusminusicon.svg
Normal file
7
src/assets/icons/plusminusicon.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
|
||||||
|
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" version="1.1" viewBox="0 0 60 60" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g transform="matrix(.95686 0 0 .95686 0 1.1133)">
|
||||||
|
<path d="m0 15.926c0 1.983 1.613 3.596 3.627 3.596h4.983v4.967c0 2.014 1.621 3.644 3.62 3.62 1.99 0 3.604-1.614 3.604-3.62v-4.967h4.966c2.007 0 3.627-1.613 3.627-3.596 0-2.006-1.62-3.627-3.627-3.627h-4.966v-4.99c0-2.007-1.621-3.628-3.604-3.628-2.006 0-3.62 1.621-3.62 3.628v4.99h-4.983c-2.014 0-3.627 1.621-3.627 3.627zm34.679 14.927 16.526-24.521c0.604-0.928 0.975-1.769 0.975-2.729 0-2.085-1.69-3.603-3.807-3.603-1.953 0-2.929 0.702-4.162 2.532l-16.935 25.384-16.962 25.079c-0.574 0.856-0.96 1.619-0.96 2.807 0 2.18 1.937 3.516 4.028 3.516 1.652 0 2.652-0.629 3.846-2.406l17.451-26.059zm2.693 14.552c0 2.006 1.613 3.627 3.62 3.627h17.204c1.982 0 3.603-1.621 3.603-3.627 0-1.983-1.621-3.62-3.603-3.62h-17.204c-2.007 0-3.62 1.637-3.62 3.62z" fill-rule="nonzero"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@ -93,14 +93,14 @@ const FilamentStocks = () => {
|
|||||||
// Add WebSocket event listener for real-time updates
|
// Add WebSocket event listener for real-time updates
|
||||||
if (socket && !initialized) {
|
if (socket && !initialized) {
|
||||||
setInitialized(true)
|
setInitialized(true)
|
||||||
socket.on('notify_filamentstock_update', (statusUpdate) => {
|
socket.on('notify_filamentstock_update', (updateData) => {
|
||||||
console.log('Received filament stock update:', statusUpdate)
|
console.log('Received filament stock update:', updateData)
|
||||||
setFilamentStocksData((prevData) => {
|
setFilamentStocksData((prevData) => {
|
||||||
return prevData.map((stock) => {
|
return prevData.map((stock) => {
|
||||||
if (stock._id === statusUpdate.id) {
|
if (stock._id === updateData._id) {
|
||||||
return {
|
return {
|
||||||
...stock,
|
...stock,
|
||||||
...statusUpdate
|
...updateData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return stock
|
return stock
|
||||||
@ -163,7 +163,12 @@ const FilamentStocks = () => {
|
|||||||
<IdText id={text} type={'filamentstock'} longId={false} />
|
<IdText id={text} type={'filamentstock'} longId={false} />
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'State',
|
||||||
|
key: 'state',
|
||||||
|
width: 350,
|
||||||
|
render: (record) => <FilamentStockState filamentStock={record} />
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Current (g)',
|
title: 'Current (g)',
|
||||||
dataIndex: 'currentNetWeight',
|
dataIndex: 'currentNetWeight',
|
||||||
@ -182,13 +187,6 @@ const FilamentStocks = () => {
|
|||||||
<Text ellipsis>{startingNetWeight.toFixed(2) + 'g'}</Text>
|
<Text ellipsis>{startingNetWeight.toFixed(2) + 'g'}</Text>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'State',
|
|
||||||
key: 'state',
|
|
||||||
width: 350,
|
|
||||||
render: (record) => <FilamentStockState filamentStock={record} />
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
title: 'Created At',
|
title: 'Created At',
|
||||||
dataIndex: 'createdAt',
|
dataIndex: 'createdAt',
|
||||||
@ -203,6 +201,20 @@ const FilamentStocks = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Updated At',
|
||||||
|
dataIndex: 'updatedAt',
|
||||||
|
key: 'updatedAt',
|
||||||
|
width: 180,
|
||||||
|
render: (updatedAt) => {
|
||||||
|
if (updatedAt) {
|
||||||
|
const formattedDate = moment(updatedAt).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
return <span>{formattedDate}</span>
|
||||||
|
} else {
|
||||||
|
return 'n/a'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Actions',
|
title: 'Actions',
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
|
|||||||
@ -8,18 +8,23 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
message,
|
message,
|
||||||
Typography,
|
Typography,
|
||||||
Flex,
|
|
||||||
Form,
|
Form,
|
||||||
Badge
|
Badge,
|
||||||
|
Collapse
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import { LoadingOutlined, ReloadOutlined } from '@ant-design/icons'
|
import {
|
||||||
|
LoadingOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
CaretRightOutlined
|
||||||
|
} from '@ant-design/icons'
|
||||||
import IdText from '../../common/IdText'
|
import IdText from '../../common/IdText'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { SocketContext } from '../../context/SocketContext'
|
import { SocketContext } from '../../context/SocketContext'
|
||||||
import FilamentStockState from '../../common/FilamentStockState'
|
import FilamentStockState from '../../common/FilamentStockState'
|
||||||
import StockEventTable from '../../common/StockEventTable'
|
import StockEventTable from '../../common/StockEventTable'
|
||||||
|
import useCollapseState from '../../hooks/useCollapseState'
|
||||||
|
|
||||||
const { Title, Text } = Typography
|
const { Text, Title } = Typography
|
||||||
|
|
||||||
const FilamentStockInfo = () => {
|
const FilamentStockInfo = () => {
|
||||||
const [filamentStockData, setFilamentStockData] = useState(null)
|
const [filamentStockData, setFilamentStockData] = useState(null)
|
||||||
@ -33,6 +38,13 @@ const FilamentStockInfo = () => {
|
|||||||
)
|
)
|
||||||
const [form] = Form.useForm()
|
const [form] = Form.useForm()
|
||||||
const { socket } = useContext(SocketContext)
|
const { socket } = useContext(SocketContext)
|
||||||
|
const [collapseState, updateCollapseState] = useCollapseState(
|
||||||
|
'FilamentStockInfo',
|
||||||
|
{
|
||||||
|
info: true,
|
||||||
|
events: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (filamentStockId) {
|
if (filamentStockId) {
|
||||||
@ -64,6 +76,11 @@ const FilamentStockInfo = () => {
|
|||||||
return prevData
|
return prevData
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Add WebSocket event listener for filament stock updates
|
||||||
|
socket.on('notify_filamentstock_update', (filamentStockUpdate) => {
|
||||||
|
console.log('GOT FILAMENT STOCK UPDATE', filamentStockUpdate)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
if (socket && initialized) {
|
if (socket && initialized) {
|
||||||
@ -120,111 +137,142 @@ const FilamentStockInfo = () => {
|
|||||||
return (
|
return (
|
||||||
<div style={{ height: '100%', minHeight: 0, overflowY: 'auto' }}>
|
<div style={{ height: '100%', minHeight: 0, overflowY: 'auto' }}>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
<Flex
|
<Collapse
|
||||||
align={'center'}
|
ghost
|
||||||
style={{ marginTop: 0, marginBottom: 12, minHeight: '32px' }}
|
activeKey={collapseState.info ? ['1'] : []}
|
||||||
justify={'space-between'}
|
onChange={(keys) => updateCollapseState('info', keys.length > 0)}
|
||||||
|
expandIcon={({ isActive }) => (
|
||||||
|
<CaretRightOutlined
|
||||||
|
rotate={isActive ? 90 : 0}
|
||||||
|
style={{ paddingTop: '2px' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Title level={5} style={{ margin: 0 }}>
|
<Collapse.Panel
|
||||||
Filament Stock Information
|
header={
|
||||||
</Title>
|
<Title level={5} style={{ margin: 0 }}>
|
||||||
</Flex>
|
Filament Stock Information
|
||||||
|
</Title>
|
||||||
|
}
|
||||||
|
key='1'
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout='vertical'
|
||||||
|
initialValues={{
|
||||||
|
filament: filamentStockData.filament || {}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Descriptions bordered column={2}>
|
||||||
|
{/* Read-only fields */}
|
||||||
|
<Descriptions.Item label='ID' span={1}>
|
||||||
|
{filamentStockData.id ? (
|
||||||
|
<IdText id={filamentStockData.id} type={'filamentstock'} />
|
||||||
|
) : (
|
||||||
|
'n/a'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label='Created At'>
|
||||||
|
{moment(filamentStockData.createdAt).format(
|
||||||
|
'YYYY-MM-DD HH:mm:ss'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
|
||||||
<Form
|
<Descriptions.Item label='State'>
|
||||||
form={form}
|
<FilamentStockState filamentStock={filamentStockData} />
|
||||||
layout='vertical'
|
</Descriptions.Item>
|
||||||
initialValues={{
|
|
||||||
filament: filamentStockData.filament || {}
|
<Descriptions.Item label='Updated At'>
|
||||||
}}
|
{moment(filamentStockData.updatedAt).format(
|
||||||
|
'YYYY-MM-DD HH:mm:ss'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
|
||||||
|
<Descriptions.Item label='Filament Name'>
|
||||||
|
{filamentStockData.filament ? (
|
||||||
|
<Badge
|
||||||
|
color={filamentStockData.filament.color}
|
||||||
|
text={filamentStockData.filament.name}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
'n/a'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
|
||||||
|
<Descriptions.Item label='Filament ID' span={1}>
|
||||||
|
{filamentStockData.filament ? (
|
||||||
|
<IdText
|
||||||
|
id={filamentStockData.filament.id}
|
||||||
|
type={'filament'}
|
||||||
|
showHyperlink={true}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
'n/a'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label='Current Gross Weight'>
|
||||||
|
{filamentStockData.currentGrossWeight ? (
|
||||||
|
<Text>
|
||||||
|
{filamentStockData.currentGrossWeight.toFixed(2) + 'g'}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
'n/a'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label='Starting Gross Weight'>
|
||||||
|
{filamentStockData.startingGrossWeight ? (
|
||||||
|
<Text>
|
||||||
|
{filamentStockData.startingGrossWeight.toFixed(2) + 'g'}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
'n/a'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label='Current Net Weight'>
|
||||||
|
{filamentStockData.currentNetWeight ? (
|
||||||
|
<Text>
|
||||||
|
{filamentStockData.currentNetWeight.toFixed(2) + 'g'}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
'n/a'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label='Starting Net Weight'>
|
||||||
|
{filamentStockData.startingNetWeight ? (
|
||||||
|
<Text>
|
||||||
|
{filamentStockData.startingNetWeight.toFixed(2) + 'g'}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
'n/a'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Form>
|
||||||
|
</Collapse.Panel>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
|
<Collapse
|
||||||
|
ghost
|
||||||
|
activeKey={collapseState.events ? ['2'] : []}
|
||||||
|
onChange={(keys) => updateCollapseState('events', keys.length > 0)}
|
||||||
|
expandIcon={({ isActive }) => (
|
||||||
|
<CaretRightOutlined
|
||||||
|
rotate={isActive ? 90 : 0}
|
||||||
|
style={{ paddingTop: '2px' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Descriptions bordered column={2}>
|
<Collapse.Panel
|
||||||
{/* Read-only fields */}
|
header={
|
||||||
<Descriptions.Item label='ID' span={1}>
|
<Title level={5} style={{ margin: 0 }}>
|
||||||
{filamentStockData.id ? (
|
Filament Stock Events
|
||||||
<IdText id={filamentStockData.id} type={'filamentstock'} />
|
</Title>
|
||||||
) : (
|
}
|
||||||
'n/a'
|
key='2'
|
||||||
)}
|
>
|
||||||
</Descriptions.Item>
|
<StockEventTable stockEvents={filamentStockData.stockEvents} />
|
||||||
<Descriptions.Item label='Created At'>
|
</Collapse.Panel>
|
||||||
{moment(filamentStockData.createdAt).format('YYYY-MM-DD HH:mm:ss')}
|
</Collapse>
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
<Descriptions.Item label='State'>
|
|
||||||
<FilamentStockState filamentStock={filamentStockData} />
|
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
<Descriptions.Item label='Updated At'>
|
|
||||||
{moment(filamentStockData.updatedAt).format('YYYY-MM-DD HH:mm:ss')}
|
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
<Descriptions.Item label='Filament Name'>
|
|
||||||
{filamentStockData.filament ? (
|
|
||||||
<Badge
|
|
||||||
color={filamentStockData.filament.color}
|
|
||||||
text={filamentStockData.filament.name}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
'n/a'
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
<Descriptions.Item label='Filament ID' span={1}>
|
|
||||||
{filamentStockData.filament ? (
|
|
||||||
<IdText
|
|
||||||
id={filamentStockData.filament.id}
|
|
||||||
type={'filament'}
|
|
||||||
showHyperlink={true}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
'n/a'
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label='Current Gross Weight'>
|
|
||||||
{filamentStockData.currentGrossWeight ? (
|
|
||||||
<Text>
|
|
||||||
{filamentStockData.currentGrossWeight.toFixed(2) + 'g'}
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
'n/a'
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label='Starting Gross Weight'>
|
|
||||||
{filamentStockData.startingGrossWeight ? (
|
|
||||||
<Text>
|
|
||||||
{filamentStockData.startingGrossWeight.toFixed(2) + 'g'}
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
'n/a'
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label='Current Net Weight'>
|
|
||||||
{filamentStockData.currentNetWeight ? (
|
|
||||||
<Text>{filamentStockData.currentNetWeight.toFixed(2) + 'g'}</Text>
|
|
||||||
) : (
|
|
||||||
'n/a'
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label='Starting Net Weight'>
|
|
||||||
{filamentStockData.startingNetWeight ? (
|
|
||||||
<Text>
|
|
||||||
{filamentStockData.startingNetWeight.toFixed(2) + 'g'}
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
'n/a'
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
</Descriptions>
|
|
||||||
</Form>
|
|
||||||
<Flex
|
|
||||||
align={'center'}
|
|
||||||
style={{ marginTop: 32, marginBottom: 12, minHeight: '32px' }}
|
|
||||||
>
|
|
||||||
<Title level={5} style={{ margin: 0 }}>
|
|
||||||
Filament Stock Events
|
|
||||||
</Title>
|
|
||||||
</Flex>
|
|
||||||
<StockEventTable stockEvents={filamentStockData.stockEvents} />
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -91,7 +91,8 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
|
|||||||
setNextEnabled(
|
setNextEnabled(
|
||||||
Boolean(unloadFilamentStockFormValues.printer) &&
|
Boolean(unloadFilamentStockFormValues.printer) &&
|
||||||
!unloadFilamentStockLoading &&
|
!unloadFilamentStockLoading &&
|
||||||
currentTemperature > targetTemperature
|
currentTemperature + 1 > targetTemperature &&
|
||||||
|
targetTemperature != 0
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.catch(() => setNextEnabled(false))
|
.catch(() => setNextEnabled(false))
|
||||||
@ -144,7 +145,7 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
|
|||||||
<>
|
<>
|
||||||
{targetTemperature == 0 ? (
|
{targetTemperature == 0 ? (
|
||||||
<Alert
|
<Alert
|
||||||
message={'Heat the extruder to begin unloading filament.'}
|
message={'Heat the extruder to start unloading filament.'}
|
||||||
type='info'
|
type='info'
|
||||||
showIcon
|
showIcon
|
||||||
/>
|
/>
|
||||||
@ -161,7 +162,7 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{targetTemperature > 0 &&
|
{targetTemperature > 0 &&
|
||||||
currentTemperature >= targetTemperature &&
|
currentTemperature + 1 > targetTemperature &&
|
||||||
filamentSensorDetected ? (
|
filamentSensorDetected ? (
|
||||||
<Alert
|
<Alert
|
||||||
message={'Ready to unload filament stock.'}
|
message={'Ready to unload filament stock.'}
|
||||||
@ -233,21 +234,11 @@ const UnloadFilamentStock = ({ onOk, reset, printer = null }) => {
|
|||||||
>
|
>
|
||||||
Previous
|
Previous
|
||||||
</Button>
|
</Button>
|
||||||
{currentStep < steps.length - 1 && (
|
|
||||||
<Button
|
|
||||||
type='primary'
|
|
||||||
disabled={!nextEnabled}
|
|
||||||
onClick={() => {
|
|
||||||
setCurrentStep(currentStep + 1)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{currentStep === steps.length - 1 && (
|
{currentStep === steps.length - 1 && (
|
||||||
<Button
|
<Button
|
||||||
type='primary'
|
type='primary'
|
||||||
loading={unloadFilamentStockLoading}
|
loading={unloadFilamentStockLoading}
|
||||||
|
disabled={!nextEnabled}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
unloadFilamentStockForm.submit()
|
unloadFilamentStockForm.submit()
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// src/filaments.js
|
// src/filaments.js
|
||||||
|
|
||||||
import React, { useEffect, useState, useContext, useCallback } from 'react'
|
import React, { useState, useContext, useCallback, useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
@ -14,21 +14,27 @@ import {
|
|||||||
Modal,
|
Modal,
|
||||||
message,
|
message,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
Typography
|
Typography,
|
||||||
|
Checkbox,
|
||||||
|
Popover,
|
||||||
|
Input,
|
||||||
|
Spin
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import { createStyles } from 'antd-style'
|
import { createStyles } from 'antd-style'
|
||||||
import {
|
import {
|
||||||
LoadingOutlined,
|
LoadingOutlined,
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
InfoCircleOutlined
|
InfoCircleOutlined,
|
||||||
|
CheckOutlined,
|
||||||
|
CloseOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
|
|
||||||
import { AuthContext } from '../../Auth/AuthContext'
|
import { AuthContext } from '../../Auth/AuthContext'
|
||||||
|
|
||||||
import NewFilament from './Filaments/NewFilament'
|
import NewFilament from './Filaments/NewFilament'
|
||||||
import IdText from '../common/IdText'
|
import IdText from '../common/IdText'
|
||||||
import FilamentIcon from '../../Icons/FilamentIcon'
|
import FilamentIcon from '../../Icons/FilamentIcon'
|
||||||
|
import useColumnVisibility from '../hooks/useColumnVisibility'
|
||||||
|
|
||||||
const { Text } = Typography
|
const { Text } = Typography
|
||||||
|
|
||||||
@ -56,40 +62,172 @@ const Filaments = () => {
|
|||||||
const { styles } = useStyle()
|
const { styles } = useStyle()
|
||||||
|
|
||||||
const [filamentsData, setFilamentsData] = useState([])
|
const [filamentsData, setFilamentsData] = useState([])
|
||||||
|
|
||||||
const [newFilamentOpen, setNewFilamentOpen] = useState(false)
|
const [newFilamentOpen, setNewFilamentOpen] = useState(false)
|
||||||
//const [newFilament, setNewFilament] = useState(null)
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
const { authenticated } = useContext(AuthContext)
|
const { authenticated } = useContext(AuthContext)
|
||||||
|
|
||||||
const fetchFilamentsData = useCallback(async () => {
|
const [page, setPage] = useState(1)
|
||||||
try {
|
const [hasMore, setHasMore] = useState(true)
|
||||||
const response = await axios.get('http://localhost:8080/filaments', {
|
const [lazyLoading, setLazyLoading] = useState(false)
|
||||||
params: {
|
const [filters, setFilters] = useState({})
|
||||||
page: 1,
|
const [sorter, setSorter] = useState({})
|
||||||
limit: 25
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json'
|
|
||||||
},
|
|
||||||
withCredentials: true // Important for including cookies
|
|
||||||
})
|
|
||||||
setFilamentsData(response.data)
|
|
||||||
setLoading(false)
|
|
||||||
//setPagination({ ...pagination, total: response.data.totalItems }); // Update total count
|
|
||||||
} catch (err) {
|
|
||||||
messageApi.info(err)
|
|
||||||
}
|
|
||||||
}, [messageApi])
|
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchFilamentsData = useCallback(
|
||||||
// Fetch initial data
|
async (pageNum = 1, append = false) => {
|
||||||
if (authenticated) {
|
try {
|
||||||
fetchFilamentsData()
|
const response = await axios.get('http://localhost:8080/filaments', {
|
||||||
|
params: {
|
||||||
|
page: pageNum,
|
||||||
|
limit: 25,
|
||||||
|
...filters,
|
||||||
|
sort: sorter.field,
|
||||||
|
order: sorter.order
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json'
|
||||||
|
},
|
||||||
|
withCredentials: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const newData = response.data
|
||||||
|
setHasMore(newData.length === 25)
|
||||||
|
|
||||||
|
if (append) {
|
||||||
|
setFilamentsData((prev) => [...prev, ...newData])
|
||||||
|
} else {
|
||||||
|
setFilamentsData(newData)
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false)
|
||||||
|
setLazyLoading(false)
|
||||||
|
} catch (err) {
|
||||||
|
messageApi.info(err)
|
||||||
|
setLoading(false)
|
||||||
|
setLazyLoading(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[messageApi, filters, sorter]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleScroll = useCallback(
|
||||||
|
(e) => {
|
||||||
|
const { target } = e
|
||||||
|
const scrollHeight = target.scrollHeight
|
||||||
|
const scrollTop = target.scrollTop
|
||||||
|
const clientHeight = target.clientHeight
|
||||||
|
|
||||||
|
if (
|
||||||
|
scrollHeight - scrollTop - clientHeight < 100 &&
|
||||||
|
!lazyLoading &&
|
||||||
|
hasMore
|
||||||
|
) {
|
||||||
|
setLazyLoading(true)
|
||||||
|
const nextPage = page + 1
|
||||||
|
setPage(nextPage)
|
||||||
|
fetchFilamentsData(nextPage, true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[page, lazyLoading, hasMore, fetchFilamentsData]
|
||||||
|
)
|
||||||
|
|
||||||
|
const getFilterDropdown = ({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters,
|
||||||
|
propertyName
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 8 }}>
|
||||||
|
<Space.Compact>
|
||||||
|
<Input
|
||||||
|
placeholder={'Search ' + propertyName}
|
||||||
|
value={selectedKeys[0]}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||||
|
}
|
||||||
|
onPressEnter={() => confirm()}
|
||||||
|
style={{ width: 200, display: 'block' }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
clearFilters()
|
||||||
|
confirm()
|
||||||
|
}}
|
||||||
|
icon={<CloseOutlined />}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type='primary'
|
||||||
|
onClick={() => confirm()}
|
||||||
|
icon={<CheckOutlined />}
|
||||||
|
/>
|
||||||
|
</Space.Compact>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getViewDropdownItems = () => {
|
||||||
|
const columnItems = columns
|
||||||
|
.filter((col) => col.key && col.title !== '')
|
||||||
|
.map((col) => (
|
||||||
|
<Checkbox
|
||||||
|
checked={columnVisibility[col.key]}
|
||||||
|
key={col.key}
|
||||||
|
onChange={(e) => {
|
||||||
|
updateColumnVisibility(col.key, e.target.checked)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{col.title}
|
||||||
|
</Checkbox>
|
||||||
|
))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex vertical>
|
||||||
|
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
|
||||||
|
{columnItems}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
const newFilters = {}
|
||||||
|
Object.entries(filters).forEach(([key, value]) => {
|
||||||
|
if (value && value.length > 0) {
|
||||||
|
newFilters[key] = value[0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setPage(1)
|
||||||
|
setFilters(newFilters)
|
||||||
|
setSorter({
|
||||||
|
field: sorter.field,
|
||||||
|
order: sorter.order
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionItems = {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'New Filament',
|
||||||
|
key: 'newFilament',
|
||||||
|
icon: <PlusOutlined />
|
||||||
|
},
|
||||||
|
{ type: 'divider' },
|
||||||
|
{
|
||||||
|
label: 'Reload List',
|
||||||
|
key: 'reloadList',
|
||||||
|
icon: <ReloadOutlined />
|
||||||
|
}
|
||||||
|
],
|
||||||
|
onClick: ({ key }) => {
|
||||||
|
if (key === 'reloadList') {
|
||||||
|
fetchFilamentsData()
|
||||||
|
} else if (key === 'newFilament') {
|
||||||
|
setNewFilamentOpen(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [authenticated, fetchFilamentsData])
|
}
|
||||||
|
|
||||||
const getFilamentActionItems = (id) => {
|
const getFilamentActionItems = (id) => {
|
||||||
return {
|
return {
|
||||||
@ -123,14 +261,46 @@ const Filaments = () => {
|
|||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
key: 'name',
|
key: 'name',
|
||||||
width: 200,
|
width: 200,
|
||||||
fixed: 'left'
|
fixed: 'left',
|
||||||
|
filterDropdown: ({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters
|
||||||
|
}) =>
|
||||||
|
getFilterDropdown({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters,
|
||||||
|
propertyName: 'name'
|
||||||
|
}),
|
||||||
|
onFilter: (value, record) =>
|
||||||
|
record.name.toLowerCase().includes(value.toLowerCase()),
|
||||||
|
sorter: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'ID',
|
title: 'ID',
|
||||||
dataIndex: '_id',
|
dataIndex: '_id',
|
||||||
key: 'id',
|
key: 'id',
|
||||||
width: 165,
|
width: 165,
|
||||||
render: (text) => <IdText id={text} type={'filament'} longId={false} />
|
render: (text) => <IdText id={text} type={'filament'} longId={false} />,
|
||||||
|
filterDropdown: ({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters
|
||||||
|
}) =>
|
||||||
|
getFilterDropdown({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters,
|
||||||
|
propertyName: 'ID'
|
||||||
|
}),
|
||||||
|
onFilter: (value, record) =>
|
||||||
|
record._id.toLowerCase().includes(value.toLowerCase()),
|
||||||
|
sorter: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Vendor',
|
title: 'Vendor',
|
||||||
@ -139,13 +309,45 @@ const Filaments = () => {
|
|||||||
width: 200,
|
width: 200,
|
||||||
render: (vendor) => {
|
render: (vendor) => {
|
||||||
return vendor.name
|
return vendor.name
|
||||||
}
|
},
|
||||||
|
filterDropdown: ({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters
|
||||||
|
}) =>
|
||||||
|
getFilterDropdown({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters,
|
||||||
|
propertyName: 'vendor'
|
||||||
|
}),
|
||||||
|
onFilter: (value, record) =>
|
||||||
|
record.vendor.name.toLowerCase().includes(value.toLowerCase()),
|
||||||
|
sorter: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Material',
|
title: 'Material',
|
||||||
dataIndex: 'type',
|
dataIndex: 'type',
|
||||||
width: 90,
|
width: 150,
|
||||||
key: 'material'
|
key: 'material',
|
||||||
|
filterDropdown: ({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters
|
||||||
|
}) =>
|
||||||
|
getFilterDropdown({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters,
|
||||||
|
propertyName: 'material'
|
||||||
|
}),
|
||||||
|
onFilter: (value, record) =>
|
||||||
|
record.type.toLowerCase().includes(value.toLowerCase()),
|
||||||
|
sorter: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Cost',
|
title: 'Cost',
|
||||||
@ -154,7 +356,8 @@ const Filaments = () => {
|
|||||||
key: 'cost',
|
key: 'cost',
|
||||||
render: (cost) => {
|
render: (cost) => {
|
||||||
return <Text ellipsis>{'£' + cost + ' per kg'}</Text>
|
return <Text ellipsis>{'£' + cost + ' per kg'}</Text>
|
||||||
}
|
},
|
||||||
|
sorter: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Colour',
|
title: 'Colour',
|
||||||
@ -163,7 +366,23 @@ const Filaments = () => {
|
|||||||
width: 120,
|
width: 120,
|
||||||
render: (color) => {
|
render: (color) => {
|
||||||
return <Badge color={color} text={color} />
|
return <Badge color={color} text={color} />
|
||||||
}
|
},
|
||||||
|
filterDropdown: ({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters
|
||||||
|
}) =>
|
||||||
|
getFilterDropdown({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters,
|
||||||
|
propertyName: 'color'
|
||||||
|
}),
|
||||||
|
onFilter: (value, record) =>
|
||||||
|
record.color.toLowerCase().includes(value.toLowerCase()),
|
||||||
|
sorter: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Created At',
|
title: 'Created At',
|
||||||
@ -177,7 +396,9 @@ const Filaments = () => {
|
|||||||
} else {
|
} else {
|
||||||
return 'n/a'
|
return 'n/a'
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
sorter: true,
|
||||||
|
defaultSortOrder: 'descend'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Updated At',
|
title: 'Updated At',
|
||||||
@ -191,7 +412,9 @@ const Filaments = () => {
|
|||||||
} else {
|
} else {
|
||||||
return 'n/a'
|
return 'n/a'
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
sorter: true,
|
||||||
|
defaultSortOrder: 'descend'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Actions',
|
title: 'Actions',
|
||||||
@ -218,46 +441,51 @@ const Filaments = () => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const actionItems = {
|
const [columnVisibility, updateColumnVisibility] = useColumnVisibility(
|
||||||
items: [
|
'Filaments',
|
||||||
{
|
columns
|
||||||
label: 'New Filament',
|
)
|
||||||
key: 'newFilament',
|
|
||||||
icon: <PlusOutlined />
|
const visibleColumns = columns.filter(
|
||||||
},
|
(col) => !col.key || columnVisibility[col.key]
|
||||||
{ type: 'divider' },
|
)
|
||||||
{
|
|
||||||
label: 'Reload List',
|
useEffect(() => {
|
||||||
key: 'reloadList',
|
if (authenticated) {
|
||||||
icon: <ReloadOutlined />
|
fetchFilamentsData()
|
||||||
}
|
|
||||||
],
|
|
||||||
onClick: ({ key }) => {
|
|
||||||
if (key === 'reloadList') {
|
|
||||||
fetchFilamentsData()
|
|
||||||
} else if (key === 'newFilament') {
|
|
||||||
setNewFilamentOpen(true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}, [authenticated, fetchFilamentsData])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex vertical={'true'} gap='large'>
|
<Flex vertical={'true'} gap='large'>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
<Space>
|
<Flex justify={'space-between'}>
|
||||||
<Dropdown menu={actionItems}>
|
<Space size='small'>
|
||||||
<Button>Actions</Button>
|
<Dropdown menu={actionItems}>
|
||||||
</Dropdown>
|
<Button>Actions</Button>
|
||||||
</Space>
|
</Dropdown>
|
||||||
|
<Popover
|
||||||
|
content={getViewDropdownItems()}
|
||||||
|
placement='bottomLeft'
|
||||||
|
arrow={false}
|
||||||
|
>
|
||||||
|
<Button>View</Button>
|
||||||
|
</Popover>
|
||||||
|
</Space>
|
||||||
|
{lazyLoading && <Spin indicator={<LoadingOutlined />} />}
|
||||||
|
</Flex>
|
||||||
<Table
|
<Table
|
||||||
dataSource={filamentsData}
|
dataSource={filamentsData}
|
||||||
|
columns={visibleColumns}
|
||||||
className={styles.customTable}
|
className={styles.customTable}
|
||||||
columns={columns}
|
|
||||||
pagination={false}
|
pagination={false}
|
||||||
|
scroll={{ y: 'calc(100vh - 270px)' }}
|
||||||
rowKey='_id'
|
rowKey='_id'
|
||||||
loading={{ spinning: loading, indicator: <LoadingOutlined spin /> }}
|
loading={{ spinning: loading, indicator: <LoadingOutlined spin /> }}
|
||||||
scroll={{ y: 'calc(100vh - 270px)' }}
|
onScroll={handleScroll}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
showSorterTooltip={false}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@ -15,7 +15,8 @@ import {
|
|||||||
InputNumber,
|
InputNumber,
|
||||||
ColorPicker,
|
ColorPicker,
|
||||||
Select,
|
Select,
|
||||||
Modal
|
Modal,
|
||||||
|
Collapse
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import {
|
import {
|
||||||
LoadingOutlined,
|
LoadingOutlined,
|
||||||
@ -23,12 +24,13 @@ import {
|
|||||||
EditOutlined,
|
EditOutlined,
|
||||||
CheckOutlined,
|
CheckOutlined,
|
||||||
CloseOutlined,
|
CloseOutlined,
|
||||||
ExportOutlined,
|
DeleteOutlined,
|
||||||
DeleteOutlined
|
CaretRightOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import IdText from '../../common/IdText'
|
import IdText from '../../common/IdText'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import VendorSelect from '../../common/VendorSelect'
|
import VendorSelect from '../../common/VendorSelect'
|
||||||
|
import useCollapseState from '../../hooks/useCollapseState'
|
||||||
|
|
||||||
const { Title, Link } = Typography
|
const { Title, Link } = Typography
|
||||||
|
|
||||||
@ -44,6 +46,13 @@ const FilamentInfo = () => {
|
|||||||
const [isEditing, setIsEditing] = useState(false)
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
const [form] = Form.useForm()
|
const [form] = Form.useForm()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const [collapseState, updateCollapseState] = useCollapseState(
|
||||||
|
'FilamentInfo',
|
||||||
|
{
|
||||||
|
info: true,
|
||||||
|
details: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (filamentId) {
|
if (filamentId) {
|
||||||
@ -198,297 +207,286 @@ const FilamentInfo = () => {
|
|||||||
return (
|
return (
|
||||||
<div style={{ height: '100%', minHeight: 0, overflowY: 'auto' }}>
|
<div style={{ height: '100%', minHeight: 0, overflowY: 'auto' }}>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
|
<Collapse
|
||||||
|
ghost
|
||||||
|
activeKey={collapseState.info ? ['1'] : []}
|
||||||
|
onChange={(keys) => updateCollapseState('info', keys.length > 0)}
|
||||||
|
expandIcon={({ isActive }) => (
|
||||||
|
<CaretRightOutlined
|
||||||
|
rotate={isActive ? 90 : 0}
|
||||||
|
style={{ paddingTop: '9px' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Collapse.Panel
|
||||||
|
header={
|
||||||
|
<Flex
|
||||||
|
align='center'
|
||||||
|
justify='space-between'
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
<Title level={5} style={{ margin: 0 }}>
|
||||||
|
Filament Information
|
||||||
|
</Title>
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
danger
|
||||||
|
onClick={() => setIsDeleteModalOpen(true)}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
{isEditing ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
icon={<CheckOutlined />}
|
||||||
|
type='primary'
|
||||||
|
onClick={updateFilamentInfo}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon={<CloseOutlined />}
|
||||||
|
onClick={cancelEditing}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Button icon={<EditOutlined />} onClick={startEditing} />
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Flex>
|
||||||
|
}
|
||||||
|
key='1'
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout='vertical'
|
||||||
|
initialValues={{
|
||||||
|
name: filamentData.name || '',
|
||||||
|
vendor: filamentData.vendor || { id: null, name: '' },
|
||||||
|
type: filamentData.type || '',
|
||||||
|
cost: filamentData.cost || null,
|
||||||
|
color: filamentData.color || '#000000',
|
||||||
|
diameter: filamentData.diameter || null,
|
||||||
|
density: filamentData.density || null,
|
||||||
|
url: filamentData.url || '',
|
||||||
|
barcode: filamentData.barcode || ''
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Descriptions bordered column={2}>
|
||||||
|
<Descriptions.Item label='ID' span={1}>
|
||||||
|
{filamentData.id ? (
|
||||||
|
<IdText id={filamentData.id} type={'filament'} />
|
||||||
|
) : (
|
||||||
|
'n/a'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label='Created At'>
|
||||||
|
{moment(filamentData.createdAt).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
|
</Descriptions.Item>
|
||||||
|
|
||||||
|
<Descriptions.Item label='Name'>
|
||||||
|
{isEditing ? (
|
||||||
|
<Form.Item
|
||||||
|
name='name'
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: 'Please enter a filament name'
|
||||||
|
},
|
||||||
|
{ max: 100, message: 'Name cannot exceed 100 characters' }
|
||||||
|
]}
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
>
|
||||||
|
<Input placeholder='Enter filament name' />
|
||||||
|
</Form.Item>
|
||||||
|
) : (
|
||||||
|
filamentData.name || 'n/a'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
|
||||||
|
<Descriptions.Item label='Updated At'>
|
||||||
|
{moment(filamentData.updatedAt).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
|
</Descriptions.Item>
|
||||||
|
|
||||||
|
<Descriptions.Item label='Vendor'>
|
||||||
|
{isEditing ? (
|
||||||
|
<Form.Item
|
||||||
|
name='vendor'
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: 'Please enter a vendor' }
|
||||||
|
]}
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
>
|
||||||
|
<VendorSelect />
|
||||||
|
</Form.Item>
|
||||||
|
) : (
|
||||||
|
filamentData.vendor.name || 'n/a'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
|
||||||
|
<Descriptions.Item label='Vendor ID'>
|
||||||
|
<IdText
|
||||||
|
id={filamentData.vendor.id}
|
||||||
|
type={'vendor'}
|
||||||
|
showHyperlink={true}
|
||||||
|
/>
|
||||||
|
</Descriptions.Item>
|
||||||
|
|
||||||
|
<Descriptions.Item label='Material'>
|
||||||
|
{isEditing ? (
|
||||||
|
<Form.Item
|
||||||
|
name='type'
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: 'Please select a material' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select>
|
||||||
|
<Select.Option value='PLA'>PLA</Select.Option>
|
||||||
|
<Select.Option value='PETG'>PETG</Select.Option>
|
||||||
|
<Select.Option value='ABS'>ABS</Select.Option>
|
||||||
|
<Select.Option value='ASA'>ASA</Select.Option>
|
||||||
|
<Select.Option value='HIPS'>HIPS</Select.Option>
|
||||||
|
<Select.Option value='TPU'>TPU</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
) : (
|
||||||
|
filamentData.type || 'n/a'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
|
||||||
|
<Descriptions.Item label='Cost'>
|
||||||
|
{isEditing ? (
|
||||||
|
<Form.Item
|
||||||
|
name='cost'
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
rules={[{ required: true, message: 'Please enter a cost' }]}
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
prefix='£'
|
||||||
|
suffix='/kg'
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
) : filamentData.cost ? (
|
||||||
|
`£${filamentData.cost}/kg`
|
||||||
|
) : (
|
||||||
|
'n/a'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
|
||||||
|
<Descriptions.Item label='Color'>
|
||||||
|
{isEditing ? (
|
||||||
|
<Form.Item
|
||||||
|
name='color'
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: 'Please select a color' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<ColorPicker />
|
||||||
|
</Form.Item>
|
||||||
|
) : (
|
||||||
|
<Badge color={filamentData.color} text={filamentData.color} />
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
|
||||||
|
<Descriptions.Item label='Diameter'>
|
||||||
|
{isEditing ? (
|
||||||
|
<Form.Item
|
||||||
|
name='diameter'
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: 'Please enter a diameter' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber suffix='mm' style={{ width: '100%' }} />
|
||||||
|
</Form.Item>
|
||||||
|
) : filamentData.diameter ? (
|
||||||
|
`${filamentData.diameter}mm`
|
||||||
|
) : (
|
||||||
|
'n/a'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
|
||||||
|
<Descriptions.Item label='Density'>
|
||||||
|
{isEditing ? (
|
||||||
|
<Form.Item
|
||||||
|
name='density'
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: 'Please enter a density' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber suffix='g/cm³' style={{ width: '100%' }} />
|
||||||
|
</Form.Item>
|
||||||
|
) : filamentData.density ? (
|
||||||
|
`${filamentData.density}g/cm³`
|
||||||
|
) : (
|
||||||
|
'n/a'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
|
||||||
|
<Descriptions.Item label='URL'>
|
||||||
|
{isEditing ? (
|
||||||
|
<Form.Item name='url' style={{ margin: 0 }}>
|
||||||
|
<Input placeholder='Enter URL' />
|
||||||
|
</Form.Item>
|
||||||
|
) : filamentData.url ? (
|
||||||
|
<Link href={filamentData.url} target='_blank'>
|
||||||
|
{filamentData.url}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
'n/a'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
|
||||||
|
<Descriptions.Item label='Barcode'>
|
||||||
|
{isEditing ? (
|
||||||
|
<Form.Item name='barcode' style={{ margin: 0 }}>
|
||||||
|
<Input placeholder='Enter barcode' />
|
||||||
|
</Form.Item>
|
||||||
|
) : (
|
||||||
|
filamentData.barcode || 'n/a'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Form>
|
||||||
|
</Collapse.Panel>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
|
<Collapse
|
||||||
|
ghost
|
||||||
|
activeKey={collapseState.details ? ['2'] : []}
|
||||||
|
onChange={(keys) => updateCollapseState('details', keys.length > 0)}
|
||||||
|
expandIcon={({ isActive }) => (
|
||||||
|
<CaretRightOutlined
|
||||||
|
rotate={isActive ? 90 : 0}
|
||||||
|
style={{ paddingTop: '2px' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Collapse.Panel
|
||||||
|
header={
|
||||||
|
<Title level={5} style={{ margin: 0 }}>
|
||||||
|
Additional Details
|
||||||
|
</Title>
|
||||||
|
}
|
||||||
|
key='2'
|
||||||
|
>
|
||||||
|
{/* Add any additional details sections here */}
|
||||||
|
</Collapse.Panel>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title='Delete Filament'
|
title='Delete Filament'
|
||||||
open={isDeleteModalOpen}
|
open={isDeleteModalOpen}
|
||||||
onOk={handleDelete}
|
onOk={handleDelete}
|
||||||
onCancel={() => setIsDeleteModalOpen(false)}
|
onCancel={() => setIsDeleteModalOpen(false)}
|
||||||
okText='Yes, Delete'
|
|
||||||
cancelText='No, Cancel'
|
|
||||||
okType='danger'
|
|
||||||
confirmLoading={loading}
|
confirmLoading={loading}
|
||||||
>
|
>
|
||||||
<p>
|
<p>Are you sure you want to delete this filament?</p>
|
||||||
Are you sure you want to delete this filament? This action cannot be
|
|
||||||
undone.
|
|
||||||
</p>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
<Flex
|
|
||||||
align={'center'}
|
|
||||||
style={{ marginTop: 0, marginBottom: 12, minHeight: '32px' }}
|
|
||||||
justify={'space-between'}
|
|
||||||
>
|
|
||||||
<Title level={5} style={{ margin: 0 }}>
|
|
||||||
Filament Information
|
|
||||||
</Title>
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
icon={<DeleteOutlined />}
|
|
||||||
danger
|
|
||||||
onClick={() => setIsDeleteModalOpen(true)}
|
|
||||||
loading={loading}
|
|
||||||
/>
|
|
||||||
{isEditing ? (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
icon={<CheckOutlined />}
|
|
||||||
type='primary'
|
|
||||||
onClick={updateFilamentInfo}
|
|
||||||
loading={loading}
|
|
||||||
></Button>
|
|
||||||
<Button
|
|
||||||
icon={<CloseOutlined />}
|
|
||||||
onClick={cancelEditing}
|
|
||||||
disabled={loading}
|
|
||||||
></Button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Button icon={<EditOutlined />} onClick={startEditing} />
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Form
|
|
||||||
form={form}
|
|
||||||
layout='vertical'
|
|
||||||
initialValues={{
|
|
||||||
name: filamentData.name || '',
|
|
||||||
vendor: filamentData.vendor || { id: null, name: '' },
|
|
||||||
type: filamentData.type || '',
|
|
||||||
cost: filamentData.cost || null,
|
|
||||||
color: filamentData.color || '#000000',
|
|
||||||
diameter: filamentData.diameter || null,
|
|
||||||
density: filamentData.density || null,
|
|
||||||
url: filamentData.url || '',
|
|
||||||
barcode: filamentData.barcode || ''
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Descriptions bordered column={2}>
|
|
||||||
{/* Read-only fields */}
|
|
||||||
<Descriptions.Item label='ID' span={1}>
|
|
||||||
{filamentData.id ? (
|
|
||||||
<IdText id={filamentData.id} type={'filament'} />
|
|
||||||
) : (
|
|
||||||
'n/a'
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label='Created At'>
|
|
||||||
{moment(filamentData.createdAt).format('YYYY-MM-DD HH:mm:ss')}
|
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
{/* Editable fields */}
|
|
||||||
<Descriptions.Item label='Name'>
|
|
||||||
{isEditing ? (
|
|
||||||
<Form.Item
|
|
||||||
name='name'
|
|
||||||
rules={[
|
|
||||||
{ required: true, message: 'Please enter a filament name' },
|
|
||||||
{ max: 100, message: 'Name cannot exceed 100 characters' }
|
|
||||||
]}
|
|
||||||
style={{ margin: 0 }}
|
|
||||||
>
|
|
||||||
<Input placeholder='Enter filament name' />
|
|
||||||
</Form.Item>
|
|
||||||
) : (
|
|
||||||
filamentData.name || 'n/a'
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
<Descriptions.Item label='Updated At'>
|
|
||||||
{moment(filamentData.updatedAt).format('YYYY-MM-DD HH:mm:ss')}
|
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
<Descriptions.Item label='Vendor'>
|
|
||||||
{isEditing ? (
|
|
||||||
<Form.Item
|
|
||||||
name='vendor'
|
|
||||||
rules={[{ required: true, message: 'Please enter a vendor' }]}
|
|
||||||
style={{ margin: 0 }}
|
|
||||||
>
|
|
||||||
<VendorSelect />
|
|
||||||
</Form.Item>
|
|
||||||
) : (
|
|
||||||
filamentData.vendor.name || 'n/a'
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
<Descriptions.Item label='Vendor ID'>
|
|
||||||
<IdText
|
|
||||||
id={filamentData.vendor.id}
|
|
||||||
type={'vendor'}
|
|
||||||
showHyperlink={true}
|
|
||||||
/>
|
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
<Descriptions.Item label='Material'>
|
|
||||||
{isEditing ? (
|
|
||||||
<Form.Item
|
|
||||||
name='type'
|
|
||||||
style={{ margin: 0 }}
|
|
||||||
rules={[
|
|
||||||
{ required: true, message: 'Please select a material' }
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Select>
|
|
||||||
<Select.Option value='PLA'>PLA</Select.Option>
|
|
||||||
<Select.Option value='PETG'>PETG</Select.Option>
|
|
||||||
<Select.Option value='ABS'>ABS</Select.Option>
|
|
||||||
<Select.Option value='ASA'>ASA</Select.Option>
|
|
||||||
<Select.Option value='HIPS'>HIPS</Select.Option>
|
|
||||||
<Select.Option value='TPU'>TPU</Select.Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
) : (
|
|
||||||
filamentData.type || 'n/a'
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
<Descriptions.Item label='Cost'>
|
|
||||||
{isEditing ? (
|
|
||||||
<Form.Item
|
|
||||||
name='cost'
|
|
||||||
style={{ margin: 0 }}
|
|
||||||
rules={[{ required: true, message: 'Please enter a cost' }]}
|
|
||||||
>
|
|
||||||
<InputNumber
|
|
||||||
placeholder='Enter cost'
|
|
||||||
addonBefore='£'
|
|
||||||
addonAfter='per kg'
|
|
||||||
min={0}
|
|
||||||
step={0.01}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
) : filamentData.cost ? (
|
|
||||||
`£${filamentData.cost} per kg`
|
|
||||||
) : (
|
|
||||||
'n/a'
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
<Descriptions.Item label='Colour'>
|
|
||||||
{isEditing ? (
|
|
||||||
<Form.Item
|
|
||||||
name='color'
|
|
||||||
style={{ margin: 0 }}
|
|
||||||
getValueFromEvent={(color) => {
|
|
||||||
return '#' + color.toHex()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ColorPicker showText disabledAlpha />
|
|
||||||
</Form.Item>
|
|
||||||
) : filamentData.color ? (
|
|
||||||
<Badge color={filamentData.color} text={filamentData.color} />
|
|
||||||
) : (
|
|
||||||
'n/a'
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
<Descriptions.Item label='Diameter'>
|
|
||||||
{isEditing ? (
|
|
||||||
<Form.Item
|
|
||||||
name='diameter'
|
|
||||||
style={{ margin: 0 }}
|
|
||||||
rules={[{ required: true, message: 'Please enter a diameter' }]}
|
|
||||||
>
|
|
||||||
<InputNumber
|
|
||||||
placeholder='Enter diameter'
|
|
||||||
addonAfter='mm'
|
|
||||||
min={0}
|
|
||||||
step={0.01}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
) : filamentData.diameter ? (
|
|
||||||
`${filamentData.diameter}mm`
|
|
||||||
) : (
|
|
||||||
'n/a'
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
<Descriptions.Item label='Density'>
|
|
||||||
{isEditing ? (
|
|
||||||
<Form.Item
|
|
||||||
name='density'
|
|
||||||
style={{ margin: 0 }}
|
|
||||||
rules={[{ required: true, message: 'Please enter a density' }]}
|
|
||||||
>
|
|
||||||
<InputNumber
|
|
||||||
placeholder='Enter density'
|
|
||||||
addonAfter='g/cm³'
|
|
||||||
min={0}
|
|
||||||
step={0.01}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
) : filamentData.density ? (
|
|
||||||
`${filamentData.density}g/cm³`
|
|
||||||
) : (
|
|
||||||
'n/a'
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
<Descriptions.Item label='Empty Spool Weight'>
|
|
||||||
{isEditing ? (
|
|
||||||
<Form.Item
|
|
||||||
name='emptySpoolWeight'
|
|
||||||
style={{ margin: 0 }}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: 'Please enter a empty spool weight'
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber
|
|
||||||
placeholder='Enter empty spool weight'
|
|
||||||
addonAfter='g'
|
|
||||||
min={0}
|
|
||||||
step={0.01}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
) : filamentData.emptySpoolWeight ? (
|
|
||||||
`${filamentData.emptySpoolWeight}g`
|
|
||||||
) : (
|
|
||||||
'n/a'
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
<Descriptions.Item label='URL'>
|
|
||||||
{isEditing ? (
|
|
||||||
<Form.Item
|
|
||||||
name='url'
|
|
||||||
rules={[{ type: 'url', message: 'Please enter a valid URL' }]}
|
|
||||||
style={{ margin: 0 }}
|
|
||||||
>
|
|
||||||
<Input placeholder='Enter URL' />
|
|
||||||
</Form.Item>
|
|
||||||
) : filamentData.url ? (
|
|
||||||
<Link
|
|
||||||
href={filamentData.url}
|
|
||||||
target='_blank'
|
|
||||||
rel='noopener noreferrer'
|
|
||||||
>
|
|
||||||
{new URL(filamentData.url).hostname + ' '}
|
|
||||||
<ExportOutlined />
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
'n/a'
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
<Descriptions.Item label='Barcode'>
|
|
||||||
{isEditing ? (
|
|
||||||
<Form.Item name='barcode' style={{ margin: 0 }}>
|
|
||||||
<Input placeholder='Enter barcode' />
|
|
||||||
</Form.Item>
|
|
||||||
) : (
|
|
||||||
filamentData.barcode || 'n/a'
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
</Descriptions>
|
|
||||||
</Form>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,10 +31,10 @@ import {
|
|||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
|
|
||||||
import { AuthContext } from '../../Auth/AuthContext'
|
import { AuthContext } from '../../Auth/AuthContext'
|
||||||
|
|
||||||
import IdText from '../common/IdText'
|
import IdText from '../common/IdText'
|
||||||
import NewProduct from './Products/NewProduct'
|
import NewProduct from './Products/NewProduct'
|
||||||
import PartIcon from '../../Icons/PartIcon'
|
import PartIcon from '../../Icons/PartIcon'
|
||||||
|
import useColumnVisibility from '../hooks/useColumnVisibility'
|
||||||
|
|
||||||
const { Text } = Typography
|
const { Text } = Typography
|
||||||
|
|
||||||
@ -201,13 +201,9 @@ const Parts = () => {
|
|||||||
|
|
||||||
const [filters, setFilters] = useState({})
|
const [filters, setFilters] = useState({})
|
||||||
const [sorter, setSorter] = useState({})
|
const [sorter, setSorter] = useState({})
|
||||||
const [columnVisibility, setColumnVisibility] = useState(
|
const [columnVisibility, updateColumnVisibility] = useColumnVisibility(
|
||||||
columns.reduce((acc, col) => {
|
'Parts',
|
||||||
if (col.key) {
|
columns
|
||||||
acc[col.key] = true
|
|
||||||
}
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const { authenticated } = useContext(AuthContext)
|
const { authenticated } = useContext(AuthContext)
|
||||||
@ -391,10 +387,7 @@ const Parts = () => {
|
|||||||
checked={columnVisibility[col.key]}
|
checked={columnVisibility[col.key]}
|
||||||
key={col.key}
|
key={col.key}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setColumnVisibility((prev) => ({
|
updateColumnVisibility(col.key, e.target.checked)
|
||||||
...prev,
|
|
||||||
[col.key]: e.target.checked
|
|
||||||
}))
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{col.title}
|
{col.title}
|
||||||
|
|||||||
@ -15,20 +15,23 @@ import {
|
|||||||
Checkbox,
|
Checkbox,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
Switch,
|
Switch,
|
||||||
Tag
|
Tag,
|
||||||
|
Collapse
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import {
|
import {
|
||||||
LoadingOutlined,
|
LoadingOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
CheckOutlined,
|
CheckOutlined,
|
||||||
CloseOutlined
|
CloseOutlined,
|
||||||
|
CaretRightOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import IdText from '../../common/IdText.jsx'
|
import IdText from '../../common/IdText.jsx'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
|
import { StlViewer } from 'react-stl-viewer'
|
||||||
|
import useCollapseState from '../../hooks/useCollapseState'
|
||||||
|
|
||||||
const { Title } = Typography
|
const { Title } = Typography
|
||||||
import { StlViewer } from 'react-stl-viewer'
|
|
||||||
|
|
||||||
const PartInfo = () => {
|
const PartInfo = () => {
|
||||||
const [partData, setPartData] = useState(null)
|
const [partData, setPartData] = useState(null)
|
||||||
@ -39,6 +42,10 @@ const PartInfo = () => {
|
|||||||
const partId = new URLSearchParams(location.search).get('partId')
|
const partId = new URLSearchParams(location.search).get('partId')
|
||||||
const [marginOrPrice, setMarginOrPrice] = useState(false)
|
const [marginOrPrice, setMarginOrPrice] = useState(false)
|
||||||
const [useGlobalPricing, setUseGlobalPricing] = useState(true)
|
const [useGlobalPricing, setUseGlobalPricing] = useState(true)
|
||||||
|
const [collapseState, updateCollapseState] = useCollapseState('PartInfo', {
|
||||||
|
info: true,
|
||||||
|
preview: true
|
||||||
|
})
|
||||||
|
|
||||||
const [partForm] = Form.useForm()
|
const [partForm] = Form.useForm()
|
||||||
const [partFormValues, setPartFormValues] = useState({})
|
const [partFormValues, setPartFormValues] = useState({})
|
||||||
@ -231,222 +238,270 @@ const PartInfo = () => {
|
|||||||
return (
|
return (
|
||||||
<div style={{ height: '100%', minHeight: 0, overflowY: 'auto' }}>
|
<div style={{ height: '100%', minHeight: 0, overflowY: 'auto' }}>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
<Flex
|
<Collapse
|
||||||
align={'center'}
|
ghost
|
||||||
style={{ marginTop: 0, marginBottom: 12, minHeight: '32px' }}
|
activeKey={collapseState.info ? ['1'] : []}
|
||||||
justify={'space-between'}
|
onChange={(keys) => updateCollapseState('info', keys.length > 0)}
|
||||||
|
expandIcon={({ isActive }) => (
|
||||||
|
<CaretRightOutlined
|
||||||
|
rotate={isActive ? 90 : 0}
|
||||||
|
style={{ paddingTop: '9px' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Title level={5} style={{ margin: 0 }}>
|
<Collapse.Panel
|
||||||
Part Information
|
header={
|
||||||
</Title>
|
<Flex
|
||||||
<Space>
|
align='center'
|
||||||
{isEditing ? (
|
justify='space-between'
|
||||||
<>
|
style={{ width: '100%' }}
|
||||||
<Button
|
>
|
||||||
icon={<CheckOutlined />}
|
<Title level={5} style={{ margin: 0 }}>
|
||||||
type='primary'
|
Part Information
|
||||||
onClick={updateInfo}
|
</Title>
|
||||||
loading={loading}
|
<Space>
|
||||||
></Button>
|
{isEditing ? (
|
||||||
<Button
|
<>
|
||||||
icon={<CloseOutlined />}
|
<Button
|
||||||
onClick={cancelEditing}
|
icon={<CheckOutlined />}
|
||||||
disabled={loading}
|
type='primary'
|
||||||
></Button>
|
onClick={updateInfo}
|
||||||
</>
|
loading={loading}
|
||||||
) : (
|
|
||||||
<Button icon={<EditOutlined />} onClick={startEditing}></Button>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Form
|
|
||||||
form={partForm}
|
|
||||||
layout='vertical'
|
|
||||||
onValuesChange={(changedValues) =>
|
|
||||||
setPartFormValues((prevValues) => ({
|
|
||||||
...prevValues,
|
|
||||||
...changedValues
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
initialValues={{
|
|
||||||
name: partData.name || ''
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Descriptions bordered column={2}>
|
|
||||||
<Descriptions.Item label='ID' span={1}>
|
|
||||||
{partData.id ? (
|
|
||||||
<IdText id={partData.id} type='part'></IdText>
|
|
||||||
) : (
|
|
||||||
'n/a'
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label='Created At'>
|
|
||||||
{moment(partData.createdAt).format('YYYY-MM-DD HH:mm:ss')}
|
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
<Descriptions.Item label='Name' span={1}>
|
|
||||||
{isEditing ? (
|
|
||||||
<Form.Item
|
|
||||||
name='name'
|
|
||||||
rules={[
|
|
||||||
{ required: true, message: 'Please enter a product name' },
|
|
||||||
{ max: 100, message: 'Name cannot exceed 100 characters' }
|
|
||||||
]}
|
|
||||||
style={{ margin: 0 }}
|
|
||||||
>
|
|
||||||
<Input placeholder='Enter product name' />
|
|
||||||
</Form.Item>
|
|
||||||
) : (
|
|
||||||
partData.name || 'n/a'
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
<Descriptions.Item label='Updated At'>
|
|
||||||
{moment(partData.updatedAt).format('YYYY-MM-DD HH:mm:ss')}
|
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
<Descriptions.Item label='Product Name' span={1}>
|
|
||||||
{partData.product.name || 'n/a'}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label='Product ID' span={1}>
|
|
||||||
{(
|
|
||||||
<IdText
|
|
||||||
id={partData.product._id}
|
|
||||||
type={'product'}
|
|
||||||
showHyperlink={true}
|
|
||||||
/>
|
|
||||||
) || 'n/a'}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item
|
|
||||||
label={!marginOrPrice ? 'Margin' : 'Price'}
|
|
||||||
span={1}
|
|
||||||
>
|
|
||||||
{isEditing && useGlobalPricing == false ? (
|
|
||||||
<Flex gap='middle'>
|
|
||||||
{marginOrPrice == false ? (
|
|
||||||
<Form.Item
|
|
||||||
name='margin'
|
|
||||||
style={{ margin: 0, flexGrow: 1 }}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: 'Please enter a margin.'
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber
|
|
||||||
controls={false}
|
|
||||||
step={0.01}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
addonAfter='%'
|
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
<Button
|
||||||
|
icon={<CloseOutlined />}
|
||||||
|
onClick={cancelEditing}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Form.Item
|
<Button icon={<EditOutlined />} onClick={startEditing} />
|
||||||
name='price'
|
|
||||||
style={{ margin: 0, flexGrow: 1 }}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: 'Please enter a price.'
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber
|
|
||||||
controls={false}
|
|
||||||
step={0.01}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
addonBefore='£'
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
)}
|
||||||
<Form.Item
|
</Space>
|
||||||
name='marginOrPrice'
|
</Flex>
|
||||||
valuePropName='checked'
|
}
|
||||||
style={{ margin: 0 }}
|
key='1'
|
||||||
>
|
>
|
||||||
<Checkbox>Price</Checkbox>
|
<Form
|
||||||
</Form.Item>
|
form={partForm}
|
||||||
</Flex>
|
layout='vertical'
|
||||||
) : partData.margin &&
|
onValuesChange={(changedValues) =>
|
||||||
marginOrPrice == false &&
|
setPartFormValues((prevValues) => ({
|
||||||
partData.useGlobalPricing == false ? (
|
...prevValues,
|
||||||
partData.margin + '%'
|
...changedValues
|
||||||
) : partData.price &&
|
}))
|
||||||
marginOrPrice == true &&
|
}
|
||||||
partData.useGlobalPricing == false ? (
|
initialValues={{
|
||||||
'£' + partData.price
|
name: partData.name || '',
|
||||||
) : (
|
version: partData.version || '',
|
||||||
'n/a'
|
tags: partData.tags || []
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label='Global Pricing'>
|
|
||||||
{isEditing ? (
|
|
||||||
<Form.Item
|
|
||||||
name='useGlobalPricing'
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: 'Please enter a global price method'
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
style={{ margin: 0 }}
|
|
||||||
valuePropName='checked'
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
) : partData.useGlobalPricing == true ? (
|
|
||||||
<Tag color='success' icon={<CheckOutlined />}>
|
|
||||||
Yes
|
|
||||||
</Tag>
|
|
||||||
) : partData.useGlobalPricing == false ? (
|
|
||||||
<Tag icon={<CloseOutlined />}>No</Tag>
|
|
||||||
) : (
|
|
||||||
'n/a'
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
</Descriptions>
|
|
||||||
</Form>
|
|
||||||
|
|
||||||
<Flex
|
|
||||||
align={'center'}
|
|
||||||
style={{ marginTop: 32, marginBottom: 12, minHeight: '32px' }}
|
|
||||||
>
|
|
||||||
<Title level={5} style={{ margin: 0 }}>
|
|
||||||
Part Preview
|
|
||||||
</Title>
|
|
||||||
</Flex>
|
|
||||||
<Card styles={{ body: { padding: '10px' } }}>
|
|
||||||
{stlLoadError ? (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
height: '40vw',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
backgroundColor: '#f5f5f5'
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Space direction='vertical' align='center'>
|
<Descriptions bordered column={2}>
|
||||||
<CloseOutlined style={{ fontSize: '24px', color: '#ff4d4f' }} />
|
<Descriptions.Item label='ID' span={1}>
|
||||||
<Typography.Text type='danger'>{stlLoadError}</Typography.Text>
|
{partData.id ? (
|
||||||
</Space>
|
<IdText id={partData.id} type='part'></IdText>
|
||||||
</div>
|
) : (
|
||||||
) : (
|
'n/a'
|
||||||
partFileObjectId && (
|
)}
|
||||||
<StlViewer
|
</Descriptions.Item>
|
||||||
url={partFileObjectId}
|
<Descriptions.Item label='Created At'>
|
||||||
orbitControls
|
{moment(partData.createdAt).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
shadows
|
</Descriptions.Item>
|
||||||
style={{ height: '40vw' }}
|
|
||||||
modelProps={{
|
<Descriptions.Item label='Name' span={1}>
|
||||||
color: '#008675'
|
{isEditing ? (
|
||||||
}}
|
<Form.Item
|
||||||
></StlViewer>
|
name='name'
|
||||||
)
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: 'Please enter a product name'
|
||||||
|
},
|
||||||
|
{ max: 100, message: 'Name cannot exceed 100 characters' }
|
||||||
|
]}
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
>
|
||||||
|
<Input placeholder='Enter product name' />
|
||||||
|
</Form.Item>
|
||||||
|
) : (
|
||||||
|
partData.name || 'n/a'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
|
||||||
|
<Descriptions.Item label='Updated At'>
|
||||||
|
{moment(partData.updatedAt).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
|
</Descriptions.Item>
|
||||||
|
|
||||||
|
<Descriptions.Item label='Product Name' span={1}>
|
||||||
|
{partData.product.name || 'n/a'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label='Product ID' span={1}>
|
||||||
|
{(
|
||||||
|
<IdText
|
||||||
|
id={partData.product._id}
|
||||||
|
type={'product'}
|
||||||
|
showHyperlink={true}
|
||||||
|
/>
|
||||||
|
) || 'n/a'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item
|
||||||
|
label={!marginOrPrice ? 'Margin' : 'Price'}
|
||||||
|
span={1}
|
||||||
|
>
|
||||||
|
{isEditing && useGlobalPricing == false ? (
|
||||||
|
<Flex gap='middle'>
|
||||||
|
{marginOrPrice == false ? (
|
||||||
|
<Form.Item
|
||||||
|
name='margin'
|
||||||
|
style={{ margin: 0, flexGrow: 1 }}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: 'Please enter a margin.'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
controls={false}
|
||||||
|
step={0.01}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
addonAfter='%'
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
) : (
|
||||||
|
<Form.Item
|
||||||
|
name='price'
|
||||||
|
style={{ margin: 0, flexGrow: 1 }}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: 'Please enter a price.'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
controls={false}
|
||||||
|
step={0.01}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
addonBefore='£'
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
<Form.Item
|
||||||
|
name='marginOrPrice'
|
||||||
|
valuePropName='checked'
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
>
|
||||||
|
<Checkbox>Price</Checkbox>
|
||||||
|
</Form.Item>
|
||||||
|
</Flex>
|
||||||
|
) : partData.margin &&
|
||||||
|
marginOrPrice == false &&
|
||||||
|
partData.useGlobalPricing == false ? (
|
||||||
|
partData.margin + '%'
|
||||||
|
) : partData.price &&
|
||||||
|
marginOrPrice == true &&
|
||||||
|
partData.useGlobalPricing == false ? (
|
||||||
|
'£' + partData.price
|
||||||
|
) : (
|
||||||
|
'n/a'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label='Global Pricing'>
|
||||||
|
{isEditing ? (
|
||||||
|
<Form.Item
|
||||||
|
name='useGlobalPricing'
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: 'Please enter a global price method'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
valuePropName='checked'
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
) : partData.useGlobalPricing == true ? (
|
||||||
|
<Tag color='success' icon={<CheckOutlined />}>
|
||||||
|
Yes
|
||||||
|
</Tag>
|
||||||
|
) : partData.useGlobalPricing == false ? (
|
||||||
|
<Tag icon={<CloseOutlined />}>No</Tag>
|
||||||
|
) : (
|
||||||
|
'n/a'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label='Version' span={1}>
|
||||||
|
{partData.version || 'n/a'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label='Tags'>
|
||||||
|
{partData.tags &&
|
||||||
|
partData.tags.map((tag, index) => (
|
||||||
|
<Tag key={index}>{tag}</Tag>
|
||||||
|
))}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Form>
|
||||||
|
</Collapse.Panel>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
|
<Collapse
|
||||||
|
ghost
|
||||||
|
activeKey={collapseState.preview ? ['2'] : []}
|
||||||
|
onChange={(keys) => updateCollapseState('preview', keys.length > 0)}
|
||||||
|
expandIcon={({ isActive }) => (
|
||||||
|
<CaretRightOutlined
|
||||||
|
rotate={isActive ? 90 : 0}
|
||||||
|
style={{ paddingTop: '2px' }}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Card>
|
>
|
||||||
|
<Collapse.Panel
|
||||||
|
header={
|
||||||
|
<Title level={5} style={{ margin: 0 }}>
|
||||||
|
Part Preview
|
||||||
|
</Title>
|
||||||
|
}
|
||||||
|
key='2'
|
||||||
|
>
|
||||||
|
<Card styles={{ body: { padding: '10px' } }}>
|
||||||
|
{stlLoadError ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '40vw',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: '#f5f5f5'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Space direction='vertical' align='center'>
|
||||||
|
<CloseOutlined
|
||||||
|
style={{ fontSize: '24px', color: '#ff4d4f' }}
|
||||||
|
/>
|
||||||
|
<Typography.Text type='danger'>
|
||||||
|
{stlLoadError}
|
||||||
|
</Typography.Text>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
partFileObjectId && (
|
||||||
|
<StlViewer
|
||||||
|
url={partFileObjectId}
|
||||||
|
orbitControls
|
||||||
|
shadows
|
||||||
|
style={{ height: '40vw' }}
|
||||||
|
modelProps={{
|
||||||
|
color: '#008675'
|
||||||
|
}}
|
||||||
|
></StlViewer>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</Collapse.Panel>
|
||||||
|
</Collapse>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,10 @@ import {
|
|||||||
Dropdown,
|
Dropdown,
|
||||||
message,
|
message,
|
||||||
Spin,
|
Spin,
|
||||||
Tag
|
Tag,
|
||||||
|
Checkbox,
|
||||||
|
Popover,
|
||||||
|
Input
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import { createStyles } from 'antd-style'
|
import { createStyles } from 'antd-style'
|
||||||
import {
|
import {
|
||||||
@ -22,7 +25,9 @@ import {
|
|||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
DownloadOutlined,
|
DownloadOutlined,
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
InfoCircleOutlined
|
InfoCircleOutlined,
|
||||||
|
CheckOutlined,
|
||||||
|
CloseOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
|
|
||||||
import { AuthContext } from '../../Auth/AuthContext'
|
import { AuthContext } from '../../Auth/AuthContext'
|
||||||
@ -30,6 +35,7 @@ import { AuthContext } from '../../Auth/AuthContext'
|
|||||||
import IdText from '../common/IdText'
|
import IdText from '../common/IdText'
|
||||||
import NewProduct from './Products/NewProduct'
|
import NewProduct from './Products/NewProduct'
|
||||||
import ProductIcon from '../../Icons/ProductIcon'
|
import ProductIcon from '../../Icons/ProductIcon'
|
||||||
|
import useColumnVisibility from '../hooks/useColumnVisibility'
|
||||||
|
|
||||||
const useStyle = createStyles(({ css, token }) => {
|
const useStyle = createStyles(({ css, token }) => {
|
||||||
const { antCls } = token
|
const { antCls } = token
|
||||||
@ -58,6 +64,8 @@ const Products = () => {
|
|||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const [hasMore, setHasMore] = useState(true)
|
const [hasMore, setHasMore] = useState(true)
|
||||||
const [lazyLoading, setLazyLoading] = useState(false)
|
const [lazyLoading, setLazyLoading] = useState(false)
|
||||||
|
const [filters, setFilters] = useState({})
|
||||||
|
const [sorter, setSorter] = useState({})
|
||||||
|
|
||||||
const [newProductOpen, setNewProductOpen] = useState(false)
|
const [newProductOpen, setNewProductOpen] = useState(false)
|
||||||
|
|
||||||
@ -71,16 +79,19 @@ const Products = () => {
|
|||||||
const response = await axios.get('http://localhost:8080/products', {
|
const response = await axios.get('http://localhost:8080/products', {
|
||||||
params: {
|
params: {
|
||||||
page: pageNum,
|
page: pageNum,
|
||||||
limit: 25
|
limit: 25,
|
||||||
|
...filters,
|
||||||
|
sort: sorter.field,
|
||||||
|
order: sorter.order
|
||||||
},
|
},
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json'
|
Accept: 'application/json'
|
||||||
},
|
},
|
||||||
withCredentials: true // Important for including cookies
|
withCredentials: true
|
||||||
})
|
})
|
||||||
|
|
||||||
const newData = response.data
|
const newData = response.data
|
||||||
setHasMore(newData.length === 25) // If we get less than 25 items, we've reached the end
|
setHasMore(newData.length === 25)
|
||||||
|
|
||||||
if (append) {
|
if (append) {
|
||||||
setProductsData((prev) => [...prev, ...newData])
|
setProductsData((prev) => [...prev, ...newData])
|
||||||
@ -105,7 +116,7 @@ const Products = () => {
|
|||||||
setLazyLoading(false)
|
setLazyLoading(false)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[messageApi]
|
[messageApi, filters, sorter]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -121,7 +132,6 @@ const Products = () => {
|
|||||||
const scrollTop = target.scrollTop
|
const scrollTop = target.scrollTop
|
||||||
const clientHeight = target.clientHeight
|
const clientHeight = target.clientHeight
|
||||||
|
|
||||||
// If we're near the bottom (within 100px) and not currently loading
|
|
||||||
if (
|
if (
|
||||||
scrollHeight - scrollTop - clientHeight < 100 &&
|
scrollHeight - scrollTop - clientHeight < 100 &&
|
||||||
!lazyLoading &&
|
!lazyLoading &&
|
||||||
@ -173,7 +183,23 @@ const Products = () => {
|
|||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
key: 'name',
|
key: 'name',
|
||||||
width: 200,
|
width: 200,
|
||||||
fixed: 'left'
|
fixed: 'left',
|
||||||
|
filterDropdown: ({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters
|
||||||
|
}) =>
|
||||||
|
getFilterDropdown({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters,
|
||||||
|
propertyName: 'name'
|
||||||
|
}),
|
||||||
|
onFilter: (value, record) =>
|
||||||
|
record.name.toLowerCase().includes(value.toLowerCase()),
|
||||||
|
sorter: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'ID',
|
title: 'ID',
|
||||||
@ -181,7 +207,23 @@ const Products = () => {
|
|||||||
key: 'id',
|
key: 'id',
|
||||||
fixed: 'left',
|
fixed: 'left',
|
||||||
width: 165,
|
width: 165,
|
||||||
render: (text) => <IdText id={text} type={'product'} longId={false} />
|
render: (text) => <IdText id={text} type={'product'} longId={false} />,
|
||||||
|
filterDropdown: ({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters
|
||||||
|
}) =>
|
||||||
|
getFilterDropdown({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters,
|
||||||
|
propertyName: 'ID'
|
||||||
|
}),
|
||||||
|
onFilter: (value, record) =>
|
||||||
|
record._id.toLowerCase().includes(value.toLowerCase()),
|
||||||
|
sorter: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Tags',
|
title: 'Tags',
|
||||||
@ -200,14 +242,48 @@ const Products = () => {
|
|||||||
))}
|
))}
|
||||||
</Space>
|
</Space>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
filterDropdown: ({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters
|
||||||
|
}) =>
|
||||||
|
getFilterDropdown({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters,
|
||||||
|
propertyName: 'tags'
|
||||||
|
}),
|
||||||
|
onFilter: (value, record) =>
|
||||||
|
record.tags?.some((tag) =>
|
||||||
|
tag.toLowerCase().includes(value.toLowerCase())
|
||||||
|
),
|
||||||
|
sorter: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Version',
|
title: 'Version',
|
||||||
dataIndex: 'version',
|
dataIndex: 'version',
|
||||||
key: 'version',
|
key: 'version',
|
||||||
width: 120,
|
width: 120,
|
||||||
render: (text) => (text ? <Tag>{text}</Tag> : 'n/a')
|
render: (text) => (text ? <Tag>{text}</Tag> : 'n/a'),
|
||||||
|
filterDropdown: ({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters
|
||||||
|
}) =>
|
||||||
|
getFilterDropdown({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters,
|
||||||
|
propertyName: 'version'
|
||||||
|
}),
|
||||||
|
onFilter: (value, record) =>
|
||||||
|
record.version?.toLowerCase().includes(value.toLowerCase()),
|
||||||
|
sorter: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Created At',
|
title: 'Created At',
|
||||||
@ -221,7 +297,9 @@ const Products = () => {
|
|||||||
} else {
|
} else {
|
||||||
return 'n/a'
|
return 'n/a'
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
sorter: true,
|
||||||
|
defaultSortOrder: 'descend'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Updated At',
|
title: 'Updated At',
|
||||||
@ -235,7 +313,9 @@ const Products = () => {
|
|||||||
} else {
|
} else {
|
||||||
return 'n/a'
|
return 'n/a'
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
sorter: true,
|
||||||
|
defaultSortOrder: 'descend'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Actions',
|
title: 'Actions',
|
||||||
@ -262,6 +342,11 @@ const Products = () => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const [columnVisibility, updateColumnVisibility] = useColumnVisibility(
|
||||||
|
'Products',
|
||||||
|
columns
|
||||||
|
)
|
||||||
|
|
||||||
const actionItems = {
|
const actionItems = {
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
@ -285,29 +370,115 @@ const Products = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getFilterDropdown = ({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters,
|
||||||
|
propertyName
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 8 }}>
|
||||||
|
<Space.Compact>
|
||||||
|
<Input
|
||||||
|
placeholder={'Search ' + propertyName}
|
||||||
|
value={selectedKeys[0]}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||||
|
}
|
||||||
|
onPressEnter={() => confirm()}
|
||||||
|
style={{ width: 200, display: 'block' }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
clearFilters()
|
||||||
|
confirm()
|
||||||
|
}}
|
||||||
|
icon={<CloseOutlined />}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type='primary'
|
||||||
|
onClick={() => confirm()}
|
||||||
|
icon={<CheckOutlined />}
|
||||||
|
/>
|
||||||
|
</Space.Compact>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getViewDropdownItems = () => {
|
||||||
|
const columnItems = columns
|
||||||
|
.filter((col) => col.key && col.title !== '')
|
||||||
|
.map((col) => (
|
||||||
|
<Checkbox
|
||||||
|
checked={columnVisibility[col.key]}
|
||||||
|
key={col.key}
|
||||||
|
onChange={(e) => {
|
||||||
|
updateColumnVisibility(col.key, e.target.checked)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{col.title}
|
||||||
|
</Checkbox>
|
||||||
|
))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex vertical>
|
||||||
|
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
|
||||||
|
{columnItems}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
const newFilters = {}
|
||||||
|
Object.entries(filters).forEach(([key, value]) => {
|
||||||
|
if (value && value.length > 0) {
|
||||||
|
newFilters[key] = value[0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setPage(1)
|
||||||
|
setFilters(newFilters)
|
||||||
|
setSorter({
|
||||||
|
field: sorter.field,
|
||||||
|
order: sorter.order
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleColumns = columns.filter(
|
||||||
|
(col) => !col.key || columnVisibility[col.key]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex vertical={'true'} gap='large'>
|
<Flex vertical={'true'} gap='large'>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
<Flex justify={'space-between'}>
|
<Flex justify={'space-between'}>
|
||||||
<Space>
|
<Space size='small'>
|
||||||
<Dropdown menu={actionItems}>
|
<Dropdown menu={actionItems}>
|
||||||
<Button>Actions</Button>
|
<Button>Actions</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
<Popover
|
||||||
|
content={getViewDropdownItems()}
|
||||||
|
placement='bottomLeft'
|
||||||
|
arrow={false}
|
||||||
|
>
|
||||||
|
<Button>View</Button>
|
||||||
|
</Popover>
|
||||||
</Space>
|
</Space>
|
||||||
{lazyLoading == true ? (
|
{lazyLoading && <Spin indicator={<LoadingOutlined />} />}
|
||||||
<Spin indicator={<LoadingOutlined />}></Spin>
|
|
||||||
) : null}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
<Table
|
<Table
|
||||||
dataSource={productsData}
|
dataSource={productsData}
|
||||||
columns={columns}
|
columns={visibleColumns}
|
||||||
className={styles.customTable}
|
className={styles.customTable}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
scroll={{ y: 'calc(100vh - 270px)' }}
|
scroll={{ y: 'calc(100vh - 270px)' }}
|
||||||
rowKey='_id'
|
rowKey='_id'
|
||||||
loading={{ spinning: loading, indicator: <LoadingOutlined spin /> }}
|
loading={{ spinning: loading, indicator: <LoadingOutlined spin /> }}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
showSorterTooltip={false}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@ -13,7 +13,8 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
Tag,
|
Tag,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
InputNumber
|
InputNumber,
|
||||||
|
Collapse
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import {
|
import {
|
||||||
LoadingOutlined,
|
LoadingOutlined,
|
||||||
@ -21,12 +22,14 @@ import {
|
|||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
CheckOutlined,
|
CheckOutlined,
|
||||||
CloseOutlined,
|
CloseOutlined,
|
||||||
PlusOutlined
|
PlusOutlined,
|
||||||
|
CaretRightOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import IdText from '../../common/IdText.jsx'
|
import IdText from '../../common/IdText.jsx'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import VendorSelect from '../../common/VendorSelect.jsx'
|
import VendorSelect from '../../common/VendorSelect.jsx'
|
||||||
import PartsTable from '../../common/PartsTable.jsx'
|
import PartsTable from '../../common/PartsTable.jsx'
|
||||||
|
import useCollapseState from '../../hooks/useCollapseState'
|
||||||
|
|
||||||
const { Title } = Typography
|
const { Title } = Typography
|
||||||
|
|
||||||
@ -40,6 +43,10 @@ const ProductInfo = () => {
|
|||||||
const [isEditing, setIsEditing] = useState(false)
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
const [fetchLoading, setFetchLoading] = useState(true)
|
const [fetchLoading, setFetchLoading] = useState(true)
|
||||||
const [marginOrPrice, setMarginOrPrice] = useState(false)
|
const [marginOrPrice, setMarginOrPrice] = useState(false)
|
||||||
|
const [collapseState, updateCollapseState] = useCollapseState('ProductInfo', {
|
||||||
|
info: true,
|
||||||
|
parts: true
|
||||||
|
})
|
||||||
|
|
||||||
const [productForm] = Form.useForm()
|
const [productForm] = Form.useForm()
|
||||||
const [productFormValues, setProductFormValues] = useState({})
|
const [productFormValues, setProductFormValues] = useState({})
|
||||||
@ -183,233 +190,268 @@ const ProductInfo = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex style={{ height: '100%', minHeight: 0, overflowY: 'auto' }} vertical>
|
<div style={{ height: '100%', minHeight: 0, overflowY: 'auto' }}>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
<Flex
|
<Collapse
|
||||||
align={'center'}
|
ghost
|
||||||
style={{ marginTop: 0, marginBottom: 12, minHeight: '32px' }}
|
activeKey={collapseState.info ? ['1'] : []}
|
||||||
justify={'space-between'}
|
onChange={(keys) => updateCollapseState('info', keys.length > 0)}
|
||||||
|
expandIcon={({ isActive }) => (
|
||||||
|
<CaretRightOutlined
|
||||||
|
rotate={isActive ? 90 : 0}
|
||||||
|
style={{ paddingTop: '9px' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Title level={5} style={{ margin: 0 }}>
|
<Collapse.Panel
|
||||||
Product Information
|
header={
|
||||||
</Title>
|
<Flex
|
||||||
<Space>
|
align='center'
|
||||||
{isEditing ? (
|
justify='space-between'
|
||||||
<>
|
style={{ width: '100%' }}
|
||||||
<Button
|
>
|
||||||
icon={<CheckOutlined />}
|
<Title level={5} style={{ margin: 0 }}>
|
||||||
type='primary'
|
Product Information
|
||||||
onClick={updateInfo}
|
</Title>
|
||||||
loading={loading}
|
<Space>
|
||||||
></Button>
|
{isEditing ? (
|
||||||
<Button
|
<>
|
||||||
icon={<CloseOutlined />}
|
<Button
|
||||||
onClick={cancelEditing}
|
icon={<CheckOutlined />}
|
||||||
disabled={loading}
|
type='primary'
|
||||||
></Button>
|
onClick={updateInfo}
|
||||||
</>
|
loading={loading}
|
||||||
) : (
|
/>
|
||||||
<Button icon={<EditOutlined />} onClick={startEditing}></Button>
|
<Button
|
||||||
)}
|
icon={<CloseOutlined />}
|
||||||
</Space>
|
onClick={cancelEditing}
|
||||||
</Flex>
|
disabled={loading}
|
||||||
|
/>
|
||||||
<Form
|
</>
|
||||||
form={productForm}
|
) : (
|
||||||
layout='vertical'
|
<Button icon={<EditOutlined />} onClick={startEditing} />
|
||||||
onValuesChange={(changedValues) =>
|
)}
|
||||||
setProductFormValues((prevValues) => ({
|
</Space>
|
||||||
...prevValues,
|
</Flex>
|
||||||
...changedValues
|
}
|
||||||
}))
|
key='1'
|
||||||
}
|
>
|
||||||
initialValues={{
|
<Form
|
||||||
name: productData.name || '',
|
form={productForm}
|
||||||
vendor: productData.vendor || { id: null, name: '' },
|
layout='vertical'
|
||||||
version: productData.version || '',
|
onValuesChange={(changedValues) =>
|
||||||
tags: productData.tags || []
|
setProductFormValues((prevValues) => ({
|
||||||
}}
|
...prevValues,
|
||||||
>
|
...changedValues
|
||||||
<Descriptions bordered column={2}>
|
}))
|
||||||
<Descriptions.Item label='ID' span={1}>
|
}
|
||||||
{productData.id ? (
|
initialValues={{
|
||||||
<IdText id={productData.id} type='product'></IdText>
|
name: productData.name || '',
|
||||||
) : (
|
vendor: productData.vendor || { id: null, name: '' },
|
||||||
'n/a'
|
version: productData.version || '',
|
||||||
)}
|
tags: productData.tags || []
|
||||||
</Descriptions.Item>
|
}}
|
||||||
|
|
||||||
<Descriptions.Item label='Created At'>
|
|
||||||
{moment(productData.createdAt).format('YYYY-MM-DD HH:mm:ss')}
|
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
<Descriptions.Item label='Name' span={1}>
|
|
||||||
{isEditing ? (
|
|
||||||
<Form.Item
|
|
||||||
name='name'
|
|
||||||
rules={[
|
|
||||||
{ required: true, message: 'Please enter a product name' },
|
|
||||||
{ max: 100, message: 'Name cannot exceed 100 characters' }
|
|
||||||
]}
|
|
||||||
style={{ margin: 0 }}
|
|
||||||
>
|
|
||||||
<Input placeholder='Enter product name' />
|
|
||||||
</Form.Item>
|
|
||||||
) : (
|
|
||||||
productData.name || 'n/a'
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
<Descriptions.Item label='Updated At'>
|
|
||||||
{moment(productData.updatedAt).format('YYYY-MM-DD HH:mm:ss')}
|
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
<Descriptions.Item label='Vendor'>
|
|
||||||
{isEditing ? (
|
|
||||||
<Form.Item
|
|
||||||
name='vendor'
|
|
||||||
rules={[{ required: true, message: 'Please enter a vendor' }]}
|
|
||||||
style={{ margin: 0 }}
|
|
||||||
>
|
|
||||||
<VendorSelect />
|
|
||||||
</Form.Item>
|
|
||||||
) : (
|
|
||||||
productData.vendor.name || 'n/a'
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
<Descriptions.Item label='Vendor ID'>
|
|
||||||
<IdText
|
|
||||||
id={productData.vendor.id}
|
|
||||||
type={'vendor'}
|
|
||||||
showHyperlink={true}
|
|
||||||
/>
|
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
<Descriptions.Item
|
|
||||||
label={!marginOrPrice ? 'Margin' : 'Price'}
|
|
||||||
span={1}
|
|
||||||
>
|
>
|
||||||
{isEditing ? (
|
<Descriptions bordered column={2}>
|
||||||
<Flex gap='middle'>
|
<Descriptions.Item label='ID' span={1}>
|
||||||
{marginOrPrice == false ? (
|
{productData.id ? (
|
||||||
|
<IdText id={productData.id} type='product'></IdText>
|
||||||
|
) : (
|
||||||
|
'n/a'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
|
||||||
|
<Descriptions.Item label='Created At'>
|
||||||
|
{moment(productData.createdAt).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
|
</Descriptions.Item>
|
||||||
|
|
||||||
|
<Descriptions.Item label='Name' span={1}>
|
||||||
|
{isEditing ? (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name='margin'
|
name='name'
|
||||||
style={{ margin: 0, flexGrow: 1 }}
|
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: 'Please enter a margin.'
|
message: 'Please enter a product name'
|
||||||
}
|
},
|
||||||
|
{ max: 100, message: 'Name cannot exceed 100 characters' }
|
||||||
]}
|
]}
|
||||||
|
style={{ margin: 0 }}
|
||||||
>
|
>
|
||||||
<InputNumber
|
<Input placeholder='Enter product name' />
|
||||||
controls={false}
|
|
||||||
step={0.01}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
addonAfter='%'
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
) : (
|
) : (
|
||||||
<Form.Item
|
productData.name || 'n/a'
|
||||||
name='price'
|
|
||||||
style={{ margin: 0, flexGrow: 1 }}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: 'Please enter a price.'
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber
|
|
||||||
controls={false}
|
|
||||||
step={0.01}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
addonBefore='£'
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
)}
|
||||||
<Form.Item
|
</Descriptions.Item>
|
||||||
name='marginOrPrice'
|
|
||||||
valuePropName='checked'
|
|
||||||
style={{ margin: 0 }}
|
|
||||||
>
|
|
||||||
<Checkbox>Price</Checkbox>
|
|
||||||
</Form.Item>
|
|
||||||
</Flex>
|
|
||||||
) : productData.margin && marginOrPrice == false ? (
|
|
||||||
productData.margin + '%'
|
|
||||||
) : productData.price && marginOrPrice == true ? (
|
|
||||||
'£' + productData.price
|
|
||||||
) : (
|
|
||||||
'n/a'
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
<Descriptions.Item label='Version' span={1}>
|
<Descriptions.Item label='Updated At'>
|
||||||
{isEditing ? (
|
{moment(productData.updatedAt).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
<Form.Item name='version' style={{ margin: 0 }}>
|
</Descriptions.Item>
|
||||||
<Input placeholder='Enter version' />
|
|
||||||
</Form.Item>
|
|
||||||
) : productData.version ? (
|
|
||||||
<Tag>{productData.version}</Tag>
|
|
||||||
) : (
|
|
||||||
'n/a'
|
|
||||||
)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
<Descriptions.Item label='Tags' span={1}>
|
<Descriptions.Item label='Vendor'>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<Form.Item name='tags' style={{ margin: 0 }}>
|
<Form.Item
|
||||||
<Space
|
name='vendor'
|
||||||
size={[0, 2]}
|
rules={[
|
||||||
wrap
|
{ required: true, message: 'Please enter a vendor' }
|
||||||
style={{ marginBottom: 4, maxWidth: '300px' }}
|
]}
|
||||||
>
|
style={{ margin: 0 }}
|
||||||
{productData.tags.map((tag) => (
|
>
|
||||||
<Tag
|
<VendorSelect />
|
||||||
key={tag}
|
|
||||||
color='blue'
|
|
||||||
closable
|
|
||||||
onClose={() => handleTagClose(tag)}
|
|
||||||
style={{ marginBottom: 12 }}
|
|
||||||
>
|
|
||||||
{tag}
|
|
||||||
</Tag>
|
|
||||||
))}
|
|
||||||
</Space>
|
|
||||||
<Space.Compact block>
|
|
||||||
<Form.Item name='newTag' noStyle>
|
|
||||||
<Input placeholder='Add new tag' />
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Button onClick={handleTagAdd} icon={<PlusOutlined />} />
|
) : (
|
||||||
</Space.Compact>
|
productData.vendor.name || 'n/a'
|
||||||
</Form.Item>
|
)}
|
||||||
) : productData.tags?.length > 0 ? (
|
</Descriptions.Item>
|
||||||
<Space size={[0, 2]} wrap style={{ maxWidth: '300px' }}>
|
|
||||||
{productData.tags.map((tag, index) => (
|
<Descriptions.Item label='Vendor ID'>
|
||||||
<Tag key={index} color='blue'>
|
<IdText
|
||||||
{tag}
|
id={productData.vendor.id}
|
||||||
</Tag>
|
type={'vendor'}
|
||||||
))}
|
showHyperlink={true}
|
||||||
</Space>
|
/>
|
||||||
) : (
|
</Descriptions.Item>
|
||||||
'n/a'
|
|
||||||
)}
|
<Descriptions.Item
|
||||||
</Descriptions.Item>
|
label={!marginOrPrice ? 'Margin' : 'Price'}
|
||||||
</Descriptions>
|
span={1}
|
||||||
</Form>
|
>
|
||||||
<Flex
|
{isEditing ? (
|
||||||
align={'center'}
|
<Flex gap='middle'>
|
||||||
style={{ marginTop: 24, marginBottom: 12, minHeight: '32px' }}
|
{marginOrPrice == false ? (
|
||||||
justify={'space-between'}
|
<Form.Item
|
||||||
|
name='margin'
|
||||||
|
style={{ margin: 0, flexGrow: 1 }}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: 'Please enter a margin.'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
controls={false}
|
||||||
|
step={0.01}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
addonAfter='%'
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
) : (
|
||||||
|
<Form.Item
|
||||||
|
name='price'
|
||||||
|
style={{ margin: 0, flexGrow: 1 }}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: 'Please enter a price.'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
controls={false}
|
||||||
|
step={0.01}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
addonBefore='£'
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
<Form.Item
|
||||||
|
name='marginOrPrice'
|
||||||
|
valuePropName='checked'
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
>
|
||||||
|
<Checkbox>Price</Checkbox>
|
||||||
|
</Form.Item>
|
||||||
|
</Flex>
|
||||||
|
) : productData.margin && marginOrPrice == false ? (
|
||||||
|
productData.margin + '%'
|
||||||
|
) : productData.price && marginOrPrice == true ? (
|
||||||
|
'£' + productData.price
|
||||||
|
) : (
|
||||||
|
'n/a'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
|
||||||
|
<Descriptions.Item label='Version' span={1}>
|
||||||
|
{isEditing ? (
|
||||||
|
<Form.Item name='version' style={{ margin: 0 }}>
|
||||||
|
<Input placeholder='Enter version' />
|
||||||
|
</Form.Item>
|
||||||
|
) : productData.version ? (
|
||||||
|
<Tag>{productData.version}</Tag>
|
||||||
|
) : (
|
||||||
|
'n/a'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
|
||||||
|
<Descriptions.Item label='Tags' span={1}>
|
||||||
|
{isEditing ? (
|
||||||
|
<Form.Item name='tags' style={{ margin: 0 }}>
|
||||||
|
<Space
|
||||||
|
size={[0, 2]}
|
||||||
|
wrap
|
||||||
|
style={{ marginBottom: 4, maxWidth: '300px' }}
|
||||||
|
>
|
||||||
|
{productData.tags.map((tag) => (
|
||||||
|
<Tag
|
||||||
|
key={tag}
|
||||||
|
color='blue'
|
||||||
|
closable
|
||||||
|
onClose={() => handleTagClose(tag)}
|
||||||
|
style={{ marginBottom: 12 }}
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
<Space.Compact block>
|
||||||
|
<Form.Item name='newTag' noStyle>
|
||||||
|
<Input placeholder='Add new tag' />
|
||||||
|
</Form.Item>
|
||||||
|
<Button onClick={handleTagAdd} icon={<PlusOutlined />} />
|
||||||
|
</Space.Compact>
|
||||||
|
</Form.Item>
|
||||||
|
) : productData.tags?.length > 0 ? (
|
||||||
|
<Space size={[0, 2]} wrap style={{ maxWidth: '300px' }}>
|
||||||
|
{productData.tags.map((tag, index) => (
|
||||||
|
<Tag key={index} color='blue'>
|
||||||
|
{tag}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
) : (
|
||||||
|
'n/a'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Form>
|
||||||
|
</Collapse.Panel>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
|
<Collapse
|
||||||
|
ghost
|
||||||
|
activeKey={collapseState.parts ? ['2'] : []}
|
||||||
|
onChange={(keys) => updateCollapseState('parts', keys.length > 0)}
|
||||||
|
expandIcon={({ isActive }) => (
|
||||||
|
<CaretRightOutlined
|
||||||
|
rotate={isActive ? 90 : 0}
|
||||||
|
style={{ paddingTop: '2px' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Title level={5} style={{ margin: 0 }}>
|
<Collapse.Panel
|
||||||
Product Parts
|
header={
|
||||||
</Title>
|
<Title level={5} style={{ margin: 0 }}>
|
||||||
</Flex>
|
Product Parts
|
||||||
<PartsTable data={productData.parts}></PartsTable>
|
</Title>
|
||||||
</Flex>
|
}
|
||||||
|
key='2'
|
||||||
|
>
|
||||||
|
<PartsTable data={productData.parts} />
|
||||||
|
</Collapse.Panel>
|
||||||
|
</Collapse>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState, useContext, useCallback } from 'react'
|
import React, { useState, useContext, useCallback, useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
@ -10,7 +10,11 @@ import {
|
|||||||
Modal,
|
Modal,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
message,
|
message,
|
||||||
Typography
|
Typography,
|
||||||
|
Checkbox,
|
||||||
|
Popover,
|
||||||
|
Input,
|
||||||
|
Spin
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import { createStyles } from 'antd-style'
|
import { createStyles } from 'antd-style'
|
||||||
import {
|
import {
|
||||||
@ -18,13 +22,16 @@ import {
|
|||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
InfoCircleOutlined,
|
InfoCircleOutlined,
|
||||||
ExportOutlined
|
ExportOutlined,
|
||||||
|
CheckOutlined,
|
||||||
|
CloseOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { AuthContext } from '../../Auth/AuthContext'
|
import { AuthContext } from '../../Auth/AuthContext'
|
||||||
import IdText from '../common/IdText'
|
import IdText from '../common/IdText'
|
||||||
import NewVendor from './Vendors/NewVendor'
|
import NewVendor from './Vendors/NewVendor'
|
||||||
import CountryDisplay from '../common/CountryDisplay'
|
import CountryDisplay from '../common/CountryDisplay'
|
||||||
import VendorIcon from '../../Icons/VendorIcon'
|
import VendorIcon from '../../Icons/VendorIcon'
|
||||||
|
import useColumnVisibility from '../hooks/useColumnVisibility'
|
||||||
|
|
||||||
const { Link } = Typography
|
const { Link } = Typography
|
||||||
|
|
||||||
@ -53,38 +60,152 @@ const Vendors = () => {
|
|||||||
const [vendorsData, setVendorsData] = useState([])
|
const [vendorsData, setVendorsData] = useState([])
|
||||||
const [newVendorOpen, setNewVendorOpen] = useState(false)
|
const [newVendorOpen, setNewVendorOpen] = useState(false)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [hasMore, setHasMore] = useState(true)
|
||||||
|
const [lazyLoading, setLazyLoading] = useState(false)
|
||||||
|
const [filters, setFilters] = useState({})
|
||||||
|
const [sorter, setSorter] = useState({})
|
||||||
|
|
||||||
const { authenticated } = useContext(AuthContext)
|
const { authenticated } = useContext(AuthContext)
|
||||||
|
|
||||||
const fetchVendorsData = useCallback(async () => {
|
const fetchVendorsData = useCallback(
|
||||||
try {
|
async (pageNum = 1, append = false) => {
|
||||||
const response = await axios.get('http://localhost:8080/vendors', {
|
try {
|
||||||
params: {
|
const response = await axios.get('http://localhost:8080/vendors', {
|
||||||
page: 1,
|
params: {
|
||||||
limit: 25
|
page: pageNum,
|
||||||
},
|
limit: 25,
|
||||||
headers: {
|
...filters,
|
||||||
Accept: 'application/json'
|
sort: sorter.field,
|
||||||
},
|
order: sorter.order
|
||||||
withCredentials: true
|
},
|
||||||
})
|
headers: {
|
||||||
setVendorsData(response.data)
|
Accept: 'application/json'
|
||||||
setLoading(false)
|
},
|
||||||
} catch (error) {
|
withCredentials: true
|
||||||
if (error.response) {
|
})
|
||||||
messageApi.error('Error fetching vendor data:', error.response.status)
|
|
||||||
} else {
|
|
||||||
messageApi.error(
|
|
||||||
'An unexpected error occurred. Please try again later.'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [messageApi])
|
|
||||||
|
|
||||||
useEffect(() => {
|
const newData = response.data
|
||||||
if (authenticated) {
|
setHasMore(newData.length === 25)
|
||||||
fetchVendorsData()
|
|
||||||
}
|
if (append) {
|
||||||
}, [authenticated, fetchVendorsData])
|
setVendorsData((prev) => [...prev, ...newData])
|
||||||
|
} else {
|
||||||
|
setVendorsData(newData)
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false)
|
||||||
|
setLazyLoading(false)
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response) {
|
||||||
|
messageApi.error('Error fetching vendor data:', error.response.status)
|
||||||
|
} else {
|
||||||
|
messageApi.error(
|
||||||
|
'An unexpected error occurred. Please try again later.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
setLazyLoading(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[messageApi, filters, sorter]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleScroll = useCallback(
|
||||||
|
(e) => {
|
||||||
|
const { target } = e
|
||||||
|
const scrollHeight = target.scrollHeight
|
||||||
|
const scrollTop = target.scrollTop
|
||||||
|
const clientHeight = target.clientHeight
|
||||||
|
|
||||||
|
if (
|
||||||
|
scrollHeight - scrollTop - clientHeight < 100 &&
|
||||||
|
!lazyLoading &&
|
||||||
|
hasMore
|
||||||
|
) {
|
||||||
|
setLazyLoading(true)
|
||||||
|
const nextPage = page + 1
|
||||||
|
setPage(nextPage)
|
||||||
|
fetchVendorsData(nextPage, true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[page, lazyLoading, hasMore, fetchVendorsData]
|
||||||
|
)
|
||||||
|
|
||||||
|
const getFilterDropdown = ({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters,
|
||||||
|
propertyName
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 8 }}>
|
||||||
|
<Space.Compact>
|
||||||
|
<Input
|
||||||
|
placeholder={'Search ' + propertyName}
|
||||||
|
value={selectedKeys[0]}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||||
|
}
|
||||||
|
onPressEnter={() => confirm()}
|
||||||
|
style={{ width: 200, display: 'block' }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
clearFilters()
|
||||||
|
confirm()
|
||||||
|
}}
|
||||||
|
icon={<CloseOutlined />}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type='primary'
|
||||||
|
onClick={() => confirm()}
|
||||||
|
icon={<CheckOutlined />}
|
||||||
|
/>
|
||||||
|
</Space.Compact>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getViewDropdownItems = () => {
|
||||||
|
const columnItems = columns
|
||||||
|
.filter((col) => col.key && col.title !== '')
|
||||||
|
.map((col) => (
|
||||||
|
<Checkbox
|
||||||
|
checked={columnVisibility[col.key]}
|
||||||
|
key={col.key}
|
||||||
|
onChange={(e) => {
|
||||||
|
updateColumnVisibility(col.key, e.target.checked)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{col.title}
|
||||||
|
</Checkbox>
|
||||||
|
))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex vertical>
|
||||||
|
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
|
||||||
|
{columnItems}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
const newFilters = {}
|
||||||
|
Object.entries(filters).forEach(([key, value]) => {
|
||||||
|
if (value && value.length > 0) {
|
||||||
|
newFilters[key] = value[0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setPage(1)
|
||||||
|
setFilters(newFilters)
|
||||||
|
setSorter({
|
||||||
|
field: sorter.field,
|
||||||
|
order: sorter.order
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const getVendorActionItems = (id) => {
|
const getVendorActionItems = (id) => {
|
||||||
return {
|
return {
|
||||||
@ -117,14 +238,46 @@ const Vendors = () => {
|
|||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
key: 'name',
|
key: 'name',
|
||||||
width: 200,
|
width: 200,
|
||||||
fixed: 'left'
|
fixed: 'left',
|
||||||
|
filterDropdown: ({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters
|
||||||
|
}) =>
|
||||||
|
getFilterDropdown({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters,
|
||||||
|
propertyName: 'name'
|
||||||
|
}),
|
||||||
|
onFilter: (value, record) =>
|
||||||
|
record.name.toLowerCase().includes(value.toLowerCase()),
|
||||||
|
sorter: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'ID',
|
title: 'ID',
|
||||||
dataIndex: '_id',
|
dataIndex: '_id',
|
||||||
key: 'id',
|
key: 'id',
|
||||||
width: 165,
|
width: 165,
|
||||||
render: (text) => <IdText id={text} type={'vendor'} longId={false} />
|
render: (text) => <IdText id={text} type={'vendor'} longId={false} />,
|
||||||
|
filterDropdown: ({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters
|
||||||
|
}) =>
|
||||||
|
getFilterDropdown({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters,
|
||||||
|
propertyName: 'ID'
|
||||||
|
}),
|
||||||
|
onFilter: (value, record) =>
|
||||||
|
record._id.toLowerCase().includes(value.toLowerCase()),
|
||||||
|
sorter: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Website',
|
title: 'Website',
|
||||||
@ -139,21 +292,69 @@ const Vendors = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
'n/a'
|
'n/a'
|
||||||
)
|
),
|
||||||
|
filterDropdown: ({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters
|
||||||
|
}) =>
|
||||||
|
getFilterDropdown({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters,
|
||||||
|
propertyName: 'website'
|
||||||
|
}),
|
||||||
|
onFilter: (value, record) =>
|
||||||
|
record.website?.toLowerCase().includes(value.toLowerCase()),
|
||||||
|
sorter: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Country',
|
title: 'Country',
|
||||||
dataIndex: 'country',
|
dataIndex: 'country',
|
||||||
key: 'country',
|
key: 'country',
|
||||||
width: 200,
|
width: 200,
|
||||||
render: (text) => (text ? <CountryDisplay countryCode={text} /> : 'n/a')
|
render: (text) => (text ? <CountryDisplay countryCode={text} /> : 'n/a'),
|
||||||
|
filterDropdown: ({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters
|
||||||
|
}) =>
|
||||||
|
getFilterDropdown({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters,
|
||||||
|
propertyName: 'country'
|
||||||
|
}),
|
||||||
|
onFilter: (value, record) =>
|
||||||
|
record.country?.toLowerCase().includes(value.toLowerCase()),
|
||||||
|
sorter: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Contact',
|
title: 'Contact',
|
||||||
dataIndex: 'contact',
|
dataIndex: 'contact',
|
||||||
key: 'contact',
|
key: 'contact',
|
||||||
width: 200,
|
width: 200,
|
||||||
render: (text) => (text ? text : 'n/a')
|
render: (text) => (text ? text : 'n/a'),
|
||||||
|
filterDropdown: ({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters
|
||||||
|
}) =>
|
||||||
|
getFilterDropdown({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters,
|
||||||
|
propertyName: 'contact'
|
||||||
|
}),
|
||||||
|
onFilter: (value, record) =>
|
||||||
|
record.contact?.toLowerCase().includes(value.toLowerCase()),
|
||||||
|
sorter: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Created At',
|
title: 'Created At',
|
||||||
@ -167,7 +368,9 @@ const Vendors = () => {
|
|||||||
} else {
|
} else {
|
||||||
return 'n/a'
|
return 'n/a'
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
sorter: true,
|
||||||
|
defaultSortOrder: 'descend'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Updated At',
|
title: 'Updated At',
|
||||||
@ -181,7 +384,9 @@ const Vendors = () => {
|
|||||||
} else {
|
} else {
|
||||||
return 'n/a'
|
return 'n/a'
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
sorter: true,
|
||||||
|
defaultSortOrder: 'descend'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Actions',
|
title: 'Actions',
|
||||||
@ -208,6 +413,15 @@ const Vendors = () => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const [columnVisibility, updateColumnVisibility] = useColumnVisibility(
|
||||||
|
'Vendors',
|
||||||
|
columns
|
||||||
|
)
|
||||||
|
|
||||||
|
const visibleColumns = columns.filter(
|
||||||
|
(col) => !col.key || columnVisibility[col.key]
|
||||||
|
)
|
||||||
|
|
||||||
const actionItems = {
|
const actionItems = {
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
@ -231,23 +445,42 @@ const Vendors = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (authenticated) {
|
||||||
|
fetchVendorsData()
|
||||||
|
}
|
||||||
|
}, [authenticated, fetchVendorsData])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex vertical={'true'} gap='large'>
|
<Flex vertical={'true'} gap='large'>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
<Space>
|
<Flex justify={'space-between'}>
|
||||||
<Dropdown menu={actionItems}>
|
<Space size='small'>
|
||||||
<Button>Actions</Button>
|
<Dropdown menu={actionItems}>
|
||||||
</Dropdown>
|
<Button>Actions</Button>
|
||||||
</Space>
|
</Dropdown>
|
||||||
|
<Popover
|
||||||
|
content={getViewDropdownItems()}
|
||||||
|
placement='bottomLeft'
|
||||||
|
arrow={false}
|
||||||
|
>
|
||||||
|
<Button>View</Button>
|
||||||
|
</Popover>
|
||||||
|
</Space>
|
||||||
|
{lazyLoading && <Spin indicator={<LoadingOutlined />} />}
|
||||||
|
</Flex>
|
||||||
<Table
|
<Table
|
||||||
dataSource={vendorsData}
|
dataSource={vendorsData}
|
||||||
columns={columns}
|
columns={visibleColumns}
|
||||||
className={styles.customTable}
|
className={styles.customTable}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
scroll={{ y: 'calc(100vh - 270px)' }}
|
scroll={{ y: 'calc(100vh - 270px)' }}
|
||||||
rowKey='_id'
|
rowKey='_id'
|
||||||
loading={{ spinning: loading, indicator: <LoadingOutlined spin /> }}
|
loading={{ spinning: loading, indicator: <LoadingOutlined spin /> }}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
showSorterTooltip={false}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@ -33,10 +33,10 @@ import {
|
|||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
|
|
||||||
import { AuthContext } from '../../Auth/AuthContext'
|
import { AuthContext } from '../../Auth/AuthContext'
|
||||||
|
|
||||||
import NewGCodeFile from './GCodeFiles/NewGCodeFile'
|
import NewGCodeFile from './GCodeFiles/NewGCodeFile'
|
||||||
import IdText from '../common/IdText'
|
import IdText from '../common/IdText'
|
||||||
import GCodeFileIcon from '../../Icons/GCodeFileIcon'
|
import GCodeFileIcon from '../../Icons/GCodeFileIcon'
|
||||||
|
import useColumnVisibility from '../hooks/useColumnVisibility'
|
||||||
|
|
||||||
const { Text } = Typography
|
const { Text } = Typography
|
||||||
|
|
||||||
@ -247,13 +247,9 @@ const GCodeFiles = () => {
|
|||||||
const [showDeleted, setShowDeleted] = useState(false)
|
const [showDeleted, setShowDeleted] = useState(false)
|
||||||
const [filters, setFilters] = useState({})
|
const [filters, setFilters] = useState({})
|
||||||
const [sorter, setSorter] = useState({})
|
const [sorter, setSorter] = useState({})
|
||||||
const [columnVisibility, setColumnVisibility] = useState(
|
const [columnVisibility, updateColumnVisibility] = useColumnVisibility(
|
||||||
columns.reduce((acc, col) => {
|
'GCodeFiles',
|
||||||
if (col.key) {
|
columns
|
||||||
acc[col.key] = true
|
|
||||||
}
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const { authenticated } = useContext(AuthContext)
|
const { authenticated } = useContext(AuthContext)
|
||||||
@ -442,16 +438,13 @@ const GCodeFiles = () => {
|
|||||||
|
|
||||||
const getViewDropdownItems = () => {
|
const getViewDropdownItems = () => {
|
||||||
const columnItems = columns
|
const columnItems = columns
|
||||||
.filter((col) => col.key && col.title !== '') // Filter out empty title columns and ensure key exists
|
.filter((col) => col.key && col.title !== '')
|
||||||
.map((col) => (
|
.map((col) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={columnVisibility[col.key]}
|
checked={columnVisibility[col.key]}
|
||||||
key={col.key}
|
key={col.key}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setColumnVisibility((prev) => ({
|
updateColumnVisibility(col.key, e.target.checked)
|
||||||
...prev,
|
|
||||||
[col.key]: e.target.checked
|
|
||||||
}))
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{col.title}
|
{col.title}
|
||||||
|
|||||||
@ -41,6 +41,7 @@ import NewPrintJob from './PrintJobs/NewPrintJob'
|
|||||||
import JobState from '../common/JobState'
|
import JobState from '../common/JobState'
|
||||||
import SubJobCounter from '../common/SubJobCounter'
|
import SubJobCounter from '../common/SubJobCounter'
|
||||||
import IdText from '../common/IdText'
|
import IdText from '../common/IdText'
|
||||||
|
import useColumnVisibility from '../hooks/useColumnVisibility'
|
||||||
|
|
||||||
const { Text } = Typography
|
const { Text } = Typography
|
||||||
|
|
||||||
@ -287,18 +288,14 @@ const PrintJobs = () => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const [columnVisibility, setColumnVisibility] = useState(
|
|
||||||
columns.reduce((acc, col) => {
|
|
||||||
if (col.key) {
|
|
||||||
acc[col.key] = true
|
|
||||||
}
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
)
|
|
||||||
|
|
||||||
const { authenticated } = useContext(AuthContext)
|
const { authenticated } = useContext(AuthContext)
|
||||||
const { socket } = useContext(SocketContext)
|
const { socket } = useContext(SocketContext)
|
||||||
|
|
||||||
|
const [columnVisibility, updateColumnVisibility] = useColumnVisibility(
|
||||||
|
'PrintJobs',
|
||||||
|
columns
|
||||||
|
)
|
||||||
|
|
||||||
const handleDeployPrintJob = (printJobId) => {
|
const handleDeployPrintJob = (printJobId) => {
|
||||||
if (socket) {
|
if (socket) {
|
||||||
messageApi.info(`Print job ${printJobId} deployment initiated`)
|
messageApi.info(`Print job ${printJobId} deployment initiated`)
|
||||||
@ -473,16 +470,13 @@ const PrintJobs = () => {
|
|||||||
|
|
||||||
const getViewDropdownItems = () => {
|
const getViewDropdownItems = () => {
|
||||||
const columnItems = columns
|
const columnItems = columns
|
||||||
.filter((col) => col.key && col.title !== '') // Filter out empty title columns and ensure key exists
|
.filter((col) => col.key && col.title !== '')
|
||||||
.map((col) => (
|
.map((col) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={columnVisibility[col.key]}
|
checked={columnVisibility[col.key]}
|
||||||
key={col.key}
|
key={col.key}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setColumnVisibility((prev) => ({
|
updateColumnVisibility(col.key, e.target.checked)
|
||||||
...prev,
|
|
||||||
[col.key]: e.target.checked
|
|
||||||
}))
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{col.title}
|
{col.title}
|
||||||
|
|||||||
@ -8,7 +8,8 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
message,
|
message,
|
||||||
Progress,
|
Progress,
|
||||||
Typography
|
Typography,
|
||||||
|
Flex
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import { LoadingOutlined, ReloadOutlined } from '@ant-design/icons'
|
import { LoadingOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
@ -37,7 +38,7 @@ const PrintJobInfo = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (socket && printJobId) {
|
if (socket && printJobId) {
|
||||||
socket.on('notify_job_update', (updateData) => {
|
socket.on('notify_job_update', (updateData) => {
|
||||||
if (updateData.id === printJobId) {
|
if (updateData._id === printJobId) {
|
||||||
setPrintJobData((prevData) => {
|
setPrintJobData((prevData) => {
|
||||||
if (!prevData) return prevData
|
if (!prevData) return prevData
|
||||||
return {
|
return {
|
||||||
@ -103,7 +104,16 @@ const PrintJobInfo = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ height: '100%', minHeight: 0, overflowY: 'auto' }}>
|
<div style={{ height: '100%', minHeight: 0, overflowY: 'auto' }}>
|
||||||
<Descriptions title='Print Job Information' bordered column={2}>
|
<Flex
|
||||||
|
align={'center'}
|
||||||
|
style={{ marginTop: 0, marginBottom: 12, minHeight: '32px' }}
|
||||||
|
justify={'space-between'}
|
||||||
|
>
|
||||||
|
<Title level={5} style={{ margin: 0 }}>
|
||||||
|
Print Job Information
|
||||||
|
</Title>
|
||||||
|
</Flex>
|
||||||
|
<Descriptions bordered column={2}>
|
||||||
<Descriptions.Item label='ID'>
|
<Descriptions.Item label='ID'>
|
||||||
<IdText id={printJobData._id} type={'job'} />
|
<IdText id={printJobData._id} type={'job'} />
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
@ -163,9 +173,15 @@ const PrintJobInfo = () => {
|
|||||||
)}
|
)}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
<Title level={5} style={{ marginBottom: 20 }}>
|
<Flex
|
||||||
Sub Job Information
|
align={'center'}
|
||||||
</Title>
|
style={{ marginTop: 24, marginBottom: 12, minHeight: '32px' }}
|
||||||
|
justify={'space-between'}
|
||||||
|
>
|
||||||
|
<Title level={5} style={{ margin: 0 }}>
|
||||||
|
Sub Job Information
|
||||||
|
</Title>
|
||||||
|
</Flex>
|
||||||
<SubJobsTree printJobData={printJobData} />
|
<SubJobsTree printJobData={printJobData} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -13,7 +13,8 @@ import {
|
|||||||
Progress,
|
Progress,
|
||||||
Modal,
|
Modal,
|
||||||
Typography,
|
Typography,
|
||||||
Badge
|
Badge,
|
||||||
|
Alert
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import {
|
import {
|
||||||
LoadingOutlined,
|
LoadingOutlined,
|
||||||
@ -38,6 +39,7 @@ import FilamentStockIcon from '../../../Icons/FilamentStockIcon'
|
|||||||
|
|
||||||
import LoadFilamentStock from '../../Inventory/FilamentStocks/LoadFilamentStock'
|
import LoadFilamentStock from '../../Inventory/FilamentStocks/LoadFilamentStock'
|
||||||
import UnloadFilamentStock from '../../Inventory/FilamentStocks/UnloadFilamentStock'
|
import UnloadFilamentStock from '../../Inventory/FilamentStocks/UnloadFilamentStock'
|
||||||
|
import FilamentStockState from '../../common/FilamentStockState'
|
||||||
|
|
||||||
const { Text } = Typography
|
const { Text } = Typography
|
||||||
|
|
||||||
@ -57,6 +59,9 @@ const ControlPrinter = () => {
|
|||||||
useState(false)
|
useState(false)
|
||||||
const [unloadFilamentStockModalOpen, setUnloadFilamentStockModalOpen] =
|
const [unloadFilamentStockModalOpen, setUnloadFilamentStockModalOpen] =
|
||||||
useState(false)
|
useState(false)
|
||||||
|
const [klippyErrorModalOpen, setKlippyErrorModalOpen] = useState(false)
|
||||||
|
const [klippyErrorMessage, setKlippyErrorMessage] = useState('')
|
||||||
|
const [klippyStartupMessage, setKlippyStartupMessage] = useState('')
|
||||||
|
|
||||||
const { socket } = useContext(SocketContext)
|
const { socket } = useContext(SocketContext)
|
||||||
const { authenticated } = useContext(AuthContext)
|
const { authenticated } = useContext(AuthContext)
|
||||||
@ -98,7 +103,7 @@ const ControlPrinter = () => {
|
|||||||
socket.on('notify_printer_update', (statusUpdate) => {
|
socket.on('notify_printer_update', (statusUpdate) => {
|
||||||
console.log('GOT STATUS', statusUpdate)
|
console.log('GOT STATUS', statusUpdate)
|
||||||
setPrinterData((prevData) => {
|
setPrinterData((prevData) => {
|
||||||
if (statusUpdate?.id === printerId) {
|
if (statusUpdate?._id === printerId) {
|
||||||
return {
|
return {
|
||||||
...prevData,
|
...prevData,
|
||||||
...statusUpdate
|
...statusUpdate
|
||||||
@ -146,13 +151,35 @@ const ControlPrinter = () => {
|
|||||||
}, [authenticated, fetchPrinterDetails])
|
}, [authenticated, fetchPrinterDetails])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
const loadFilamentStock = printerData?.alerts?.find(
|
||||||
printerData?.alerts?.some((alert) => alert.type === 'loadFilamentStock')
|
(alert) => alert.type === 'loadFilamentStock'
|
||||||
) {
|
)
|
||||||
|
const klippyError = printerData?.alerts?.find(
|
||||||
|
(alert) => alert.type === 'klippyError'
|
||||||
|
)
|
||||||
|
const klippyStartup = printerData?.alerts?.find(
|
||||||
|
(alert) => alert.type === 'klippyStartup'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (loadFilamentStock) {
|
||||||
setLoadFilamentStockModalOpen(true)
|
setLoadFilamentStockModalOpen(true)
|
||||||
} else {
|
} else {
|
||||||
setLoadFilamentStockModalOpen(false)
|
setLoadFilamentStockModalOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (klippyError) {
|
||||||
|
setKlippyErrorModalOpen(true)
|
||||||
|
setKlippyErrorMessage(klippyError.message)
|
||||||
|
} else {
|
||||||
|
setKlippyErrorModalOpen(false)
|
||||||
|
setKlippyErrorMessage('')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (klippyStartup) {
|
||||||
|
setKlippyStartupMessage(klippyStartup.message)
|
||||||
|
} else {
|
||||||
|
setKlippyStartupMessage('')
|
||||||
|
}
|
||||||
}, [printerData?.alerts])
|
}, [printerData?.alerts])
|
||||||
|
|
||||||
const actionItems = {
|
const actionItems = {
|
||||||
@ -344,6 +371,12 @@ const ControlPrinter = () => {
|
|||||||
{printerData ? (
|
{printerData ? (
|
||||||
<Flex gap={16}>
|
<Flex gap={16}>
|
||||||
<Flex gap={16} vertical style={{ flexGrow: 1 }}>
|
<Flex gap={16} vertical style={{ flexGrow: 1 }}>
|
||||||
|
{printerData?.alerts?.some(
|
||||||
|
(alert) => alert.type === 'klippyError'
|
||||||
|
) && <Alert message={klippyErrorMessage} type='error' />}
|
||||||
|
{printerData?.alerts?.some(
|
||||||
|
(alert) => alert.type === 'klippyStartup'
|
||||||
|
) && <Alert message={klippyStartupMessage} type='warning' />}
|
||||||
<Descriptions bordered column={2}>
|
<Descriptions bordered column={2}>
|
||||||
<Descriptions.Item label='Printer Name'>
|
<Descriptions.Item label='Printer Name'>
|
||||||
{printerData.name}
|
{printerData.name}
|
||||||
@ -406,6 +439,14 @@ const ControlPrinter = () => {
|
|||||||
)}
|
)}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
|
||||||
|
{printerData?.currentFilamentStock && (
|
||||||
|
<Descriptions.Item label='Filament Stock' span={2}>
|
||||||
|
<FilamentStockState
|
||||||
|
filamentStock={printerData?.currentFilamentStock}
|
||||||
|
/>
|
||||||
|
</Descriptions.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
<Descriptions.Item label='Filament Stock Net Weight'>
|
<Descriptions.Item label='Filament Stock Net Weight'>
|
||||||
{printerData.currentFilamentStock?.currentNetWeight ? (
|
{printerData.currentFilamentStock?.currentNetWeight ? (
|
||||||
<Text>
|
<Text>
|
||||||
@ -455,6 +496,17 @@ const ControlPrinter = () => {
|
|||||||
)}
|
)}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
|
||||||
|
{printerData?.state.type === 'printing' && (
|
||||||
|
<Descriptions.Item label='Progress' span={2}>
|
||||||
|
<Progress
|
||||||
|
percent={Math.round(
|
||||||
|
(printerData.state.progress || 0) * 100
|
||||||
|
)}
|
||||||
|
status='active'
|
||||||
|
/>
|
||||||
|
</Descriptions.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
<Descriptions.Item label='Est. Print Time'>
|
<Descriptions.Item label='Est. Print Time'>
|
||||||
{(() => {
|
{(() => {
|
||||||
if (
|
if (
|
||||||
@ -493,27 +545,18 @@ const ControlPrinter = () => {
|
|||||||
}
|
}
|
||||||
})()}
|
})()}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
|
||||||
{printerData?.state.type === 'printing' && (
|
|
||||||
<Descriptions.Item label='Progress'>
|
|
||||||
<Progress
|
|
||||||
percent={Math.round(
|
|
||||||
(printerData.state.progress || 0) * 100
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Descriptions.Item>
|
|
||||||
)}
|
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
<PrinterSubJobsTree subJobs={printerData.subJobs} />
|
<PrinterSubJobsTree subJobs={printerData.subJobs} />
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex gap={16} vertical>
|
<Flex gap={16} vertical>
|
||||||
<Card bordered={true}>
|
<Card>
|
||||||
<PrinterTemperaturePanel
|
<PrinterTemperaturePanel
|
||||||
printerId={printerId}
|
printerId={printerId}
|
||||||
|
disabled={!printerData.online}
|
||||||
></PrinterTemperaturePanel>
|
></PrinterTemperaturePanel>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card bordered={true}>
|
<Card>
|
||||||
<PrinterMovementPanel
|
<PrinterMovementPanel
|
||||||
printerId={printerId}
|
printerId={printerId}
|
||||||
></PrinterMovementPanel>
|
></PrinterMovementPanel>
|
||||||
@ -560,6 +603,38 @@ const ControlPrinter = () => {
|
|||||||
reset={unloadFilamentStockModalOpen}
|
reset={unloadFilamentStockModalOpen}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<Modal
|
||||||
|
open={klippyErrorModalOpen}
|
||||||
|
title={
|
||||||
|
<Space size={'middle'}>
|
||||||
|
<ExclamationCircleOutlined />
|
||||||
|
Klipper Error
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
onCancel={() => setKlippyErrorModalOpen(false)}
|
||||||
|
footer={[
|
||||||
|
<Button
|
||||||
|
key='close'
|
||||||
|
onClick={() => {
|
||||||
|
setKlippyErrorModalOpen(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>,
|
||||||
|
<Button
|
||||||
|
key='firmwareRestart'
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
socket.emit('printer.firmware_restart', { printerId })
|
||||||
|
setKlippyErrorModalOpen(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Restart Firmware
|
||||||
|
</Button>
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Typography.Paragraph>{klippyErrorMessage}</Typography.Paragraph>
|
||||||
|
</Modal>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,7 +26,7 @@ const breadcrumbNameMap = {
|
|||||||
'/dashboard/management/vendors/info': 'Info',
|
'/dashboard/management/vendors/info': 'Info',
|
||||||
'/dashboard/management/materials': 'Materials',
|
'/dashboard/management/materials': 'Materials',
|
||||||
'/dashboard/management/materials/info': 'Info',
|
'/dashboard/management/materials/info': 'Info',
|
||||||
'/dashboard/inventory/filamentstocks': 'Filaments',
|
'/dashboard/inventory/filamentstocks': 'Filament Stocks',
|
||||||
'/dashboard/inventory/filamentstocks/info': 'Info',
|
'/dashboard/inventory/filamentstocks/info': 'Info',
|
||||||
'/dashboard/inventory/partstocks': 'Parts',
|
'/dashboard/inventory/partstocks': 'Parts',
|
||||||
'/dashboard/inventory/partstocks/info': 'Info'
|
'/dashboard/inventory/partstocks/info': 'Info'
|
||||||
|
|||||||
@ -1,20 +1,17 @@
|
|||||||
// DashboardLayout.js
|
// DashboardLayout.js
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import React, { useContext } from 'react'
|
import React from 'react'
|
||||||
import { Layout, Flex, Spin } from 'antd'
|
import { Layout, Flex } from 'antd'
|
||||||
import { LoadingOutlined } from '@ant-design/icons'
|
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import DashboardNavigation from './DashboardNavigation'
|
import DashboardNavigation from './DashboardNavigation'
|
||||||
import ProductionSidebar from './ProductionSidebar'
|
import ProductionSidebar from './ProductionSidebar'
|
||||||
import InventorySidebar from './InventorySidebar'
|
import InventorySidebar from './InventorySidebar'
|
||||||
import ManagementSidebar from './ManagementSidebar'
|
import ManagementSidebar from './ManagementSidebar'
|
||||||
import DashboardBreadcrumb from './DashboardBreadcrumb'
|
import DashboardBreadcrumb from './DashboardBreadcrumb'
|
||||||
import { SocketContext } from '../context/SocketContext'
|
|
||||||
|
|
||||||
const { Content } = Layout
|
const { Content } = Layout
|
||||||
|
|
||||||
const DashboardLayout = ({ children }) => {
|
const DashboardLayout = ({ children }) => {
|
||||||
const { connecting } = useContext(SocketContext)
|
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const isProduction = location.pathname.startsWith('/dashboard/production')
|
const isProduction = location.pathname.startsWith('/dashboard/production')
|
||||||
const isInventory = location.pathname.startsWith('/dashboard/inventory')
|
const isInventory = location.pathname.startsWith('/dashboard/inventory')
|
||||||
@ -38,15 +35,6 @@ const DashboardLayout = ({ children }) => {
|
|||||||
<Flex vertical style={{ height: '100%' }} gap='20px'>
|
<Flex vertical style={{ height: '100%' }} gap='20px'>
|
||||||
<Flex justify='space-between'>
|
<Flex justify='space-between'>
|
||||||
<DashboardBreadcrumb style={{ margin: '16px 0' }} />
|
<DashboardBreadcrumb style={{ margin: '16px 0' }} />
|
||||||
{connecting ? (
|
|
||||||
<Spin
|
|
||||||
indicator={<LoadingOutlined spin />}
|
|
||||||
size='middle'
|
|
||||||
style={{ color: '#808080' }}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -1,14 +1,29 @@
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { Badge, Progress, Flex, Space, Tag, Typography } from 'antd'
|
import { Badge, Progress, Flex, Space, Tag, Typography } from 'antd'
|
||||||
import { green, red } from '@ant-design/colors'
|
import { green } from '@ant-design/colors'
|
||||||
import React, { useState, useContext, useEffect } from 'react'
|
import React, { useState, useContext, useEffect } from 'react'
|
||||||
import { SocketContext } from '../context/SocketContext'
|
import { SocketContext } from '../context/SocketContext'
|
||||||
|
|
||||||
|
const { Text } = Typography
|
||||||
|
|
||||||
|
const getProgressColor = (percent) => {
|
||||||
|
if (percent <= 50) {
|
||||||
|
return green[5]
|
||||||
|
} else if (percent <= 80) {
|
||||||
|
// Interpolate between green and yellow
|
||||||
|
const ratio = (percent - 50) / 30
|
||||||
|
return `rgb(${Math.round(255 * ratio)}, ${Math.round(255 * (1 - ratio))}, 0)`
|
||||||
|
} else {
|
||||||
|
// Interpolate between yellow and red
|
||||||
|
const ratio = (percent - 80) / 20
|
||||||
|
return `rgb(255, ${Math.round(255 * (1 - ratio))}, 0)`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const FilamentStockState = ({
|
const FilamentStockState = ({
|
||||||
filamentStock,
|
filamentStock,
|
||||||
showProgress = true,
|
showProgress = true,
|
||||||
showStatus = true,
|
showStatus = true
|
||||||
showFilamentStockName = true
|
|
||||||
}) => {
|
}) => {
|
||||||
const { socket } = useContext(SocketContext)
|
const { socket } = useContext(SocketContext)
|
||||||
const [badgeStatus, setBadgeStatus] = useState('unknown')
|
const [badgeStatus, setBadgeStatus] = useState('unknown')
|
||||||
@ -20,7 +35,6 @@ const FilamentStockState = ({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
const [initialized, setInitialized] = useState(false)
|
const [initialized, setInitialized] = useState(false)
|
||||||
const { Text } = Typography
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (socket && !initialized && filamentStock?._id) {
|
if (socket && !initialized && filamentStock?._id) {
|
||||||
@ -64,7 +78,6 @@ const FilamentStockState = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex gap='middle' align={'center'}>
|
<Flex gap='middle' align={'center'}>
|
||||||
{showFilamentStockName && <Text>{filamentStock.name}</Text>}
|
|
||||||
{showStatus && (
|
{showStatus && (
|
||||||
<Space>
|
<Space>
|
||||||
<Tag color={badgeStatus} style={{ marginRight: 0 }}>
|
<Tag color={badgeStatus} style={{ marginRight: 0 }}>
|
||||||
@ -76,20 +89,21 @@ const FilamentStockState = ({
|
|||||||
</Space>
|
</Space>
|
||||||
)}
|
)}
|
||||||
{showProgress && currentState.type === 'partiallyconsumed' ? (
|
{showProgress && currentState.type === 'partiallyconsumed' ? (
|
||||||
<Progress
|
<Flex style={{ width: '150px' }} gap={'small'}>
|
||||||
percent={Math.round(currentState.percent * 100)}
|
<div style={{ flexGrow: '1' }}>
|
||||||
style={{ width: '150px', marginBottom: '2px' }}
|
<Progress
|
||||||
steps={7}
|
percent={Math.round(currentState.percent * 100)}
|
||||||
strokeColor={[
|
style={{ marginBottom: '2px', width: '100%' }}
|
||||||
green[5],
|
strokeColor={getProgressColor(
|
||||||
green[5],
|
Math.round(currentState.percent * 100)
|
||||||
green[5],
|
)}
|
||||||
green[4],
|
showInfo={false}
|
||||||
green[3],
|
/>
|
||||||
red[4],
|
</div>
|
||||||
red[5]
|
<Text style={{ marginTop: '1px' }}>
|
||||||
]}
|
{Math.round(currentState.percent * 100) + '%'}
|
||||||
/>
|
</Text>
|
||||||
|
</Flex>
|
||||||
) : null}
|
) : null}
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
@ -111,8 +125,7 @@ FilamentStockState.propTypes = {
|
|||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
showProgress: PropTypes.bool,
|
showProgress: PropTypes.bool,
|
||||||
showStatus: PropTypes.bool,
|
showStatus: PropTypes.bool
|
||||||
showFilamentStockName: PropTypes.bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FilamentStockState
|
export default FilamentStockState
|
||||||
|
|||||||
@ -61,6 +61,10 @@ const IdText = ({
|
|||||||
prefix = 'FLS'
|
prefix = 'FLS'
|
||||||
hyperlink = `/dashboard/inventory/filamentstocks/info?filamentStockId=${id}`
|
hyperlink = `/dashboard/inventory/filamentstocks/info?filamentStockId=${id}`
|
||||||
break
|
break
|
||||||
|
case 'stockaudit':
|
||||||
|
prefix = 'SAU'
|
||||||
|
hyperlink = `/dashboard/inventory/stockaudits/info?stockAuditId=${id}`
|
||||||
|
break
|
||||||
case 'partstock':
|
case 'partstock':
|
||||||
prefix = 'PTS'
|
prefix = 'PTS'
|
||||||
hyperlink = `/dashboard/management/partstocks/info?partStockId=${id}`
|
hyperlink = `/dashboard/management/partstocks/info?partStockId=${id}`
|
||||||
|
|||||||
@ -40,19 +40,30 @@ const InventorySidebar = () => {
|
|||||||
label: <Link to='/dashboard/inventory/overview'>Overview</Link>,
|
label: <Link to='/dashboard/inventory/overview'>Overview</Link>,
|
||||||
icon: <DashboardOutlined />
|
icon: <DashboardOutlined />
|
||||||
},
|
},
|
||||||
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
key: 'filamentstocks',
|
key: 'filamentstocks',
|
||||||
label: <Link to='/dashboard/inventory/filamentstocks'>Filaments</Link>,
|
label: (
|
||||||
|
<Link to='/dashboard/inventory/filamentstocks'>Filament Stocks</Link>
|
||||||
|
),
|
||||||
icon: <FilamentStockIcon />
|
icon: <FilamentStockIcon />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'partstocks',
|
key: 'partstocks',
|
||||||
label: <Link to='/dashboard/inventory/partstocks'>Parts</Link>,
|
label: <Link to='/dashboard/inventory/partstocks'>Part Stocks</Link>,
|
||||||
icon: <PartStockIcon />
|
icon: <PartStockIcon />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'productstocks',
|
key: 'productstocks',
|
||||||
label: <Link to='/dashboard/inventory/productstocks'>Products</Link>,
|
label: (
|
||||||
|
<Link to='/dashboard/inventory/productstocks'>Product Stocks</Link>
|
||||||
|
),
|
||||||
|
icon: <ProductStockIcon />
|
||||||
|
},
|
||||||
|
{ type: 'divider' },
|
||||||
|
{
|
||||||
|
key: 'stockaudits',
|
||||||
|
label: <Link to='/dashboard/inventory/stockaudits'>Stock Audits</Link>,
|
||||||
icon: <ProductStockIcon />
|
icon: <ProductStockIcon />
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -71,7 +71,7 @@ const JobState = ({
|
|||||||
<Flex gap='small' align={'center'}>
|
<Flex gap='small' align={'center'}>
|
||||||
{showId && (
|
{showId && (
|
||||||
<>
|
<>
|
||||||
{'Sub Job '}
|
{'Job '}
|
||||||
<IdText id={job.id} showCopy={false} type='job' longId={false} />
|
<IdText id={job.id} showCopy={false} type='job' longId={false} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -91,6 +91,7 @@ const JobState = ({
|
|||||||
currentState.type === 'processing') ? (
|
currentState.type === 'processing') ? (
|
||||||
<Progress
|
<Progress
|
||||||
percent={Math.round(currentState.progress * 100)}
|
percent={Math.round(currentState.progress * 100)}
|
||||||
|
status='active'
|
||||||
style={{ width: '150px', marginBottom: '2px' }}
|
style={{ width: '150px', marginBottom: '2px' }}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@ -64,6 +64,7 @@ const ManagementSidebar = () => {
|
|||||||
label: <Link to='/dashboard/management/materials'>Materials</Link>,
|
label: <Link to='/dashboard/management/materials'>Materials</Link>,
|
||||||
icon: <MaterialIcon />
|
icon: <MaterialIcon />
|
||||||
},
|
},
|
||||||
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
key: 'settings',
|
key: 'settings',
|
||||||
label: <Link to='/dashboard/management/settings'>Settings</Link>,
|
label: <Link to='/dashboard/management/settings'>Settings</Link>,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { Card, Tree, Spin, Space, Button, message, Typography } from 'antd'
|
import { Card, Tree, Spin, Space, Button, message } from 'antd'
|
||||||
import { LoadingOutlined, ReloadOutlined } from '@ant-design/icons'
|
import { LoadingOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||||
import React, { useState, useEffect, useContext } from 'react'
|
import React, { useState, useEffect, useContext } from 'react'
|
||||||
import SubJobState from './SubJobState'
|
import SubJobState from './SubJobState'
|
||||||
@ -15,8 +15,6 @@ const PrinterJobsTree = ({ subJobs: initialSubJobs }) => {
|
|||||||
const [expandedKeys, setExpandedKeys] = useState([])
|
const [expandedKeys, setExpandedKeys] = useState([])
|
||||||
const [treeData, setTreeData] = useState([])
|
const [treeData, setTreeData] = useState([])
|
||||||
|
|
||||||
const { Text } = Typography
|
|
||||||
|
|
||||||
const buildTreeData = (subJobsData) => {
|
const buildTreeData = (subJobsData) => {
|
||||||
if (!subJobsData?.length) {
|
if (!subJobsData?.length) {
|
||||||
setTreeData([])
|
setTreeData([])
|
||||||
@ -42,20 +40,7 @@ const PrinterJobsTree = ({ subJobs: initialSubJobs }) => {
|
|||||||
({ printJob, subJobs }) => {
|
({ printJob, subJobs }) => {
|
||||||
setExpandedKeys((prev) => [...prev, `printjob-${printJob._id}`])
|
setExpandedKeys((prev) => [...prev, `printjob-${printJob._id}`])
|
||||||
return {
|
return {
|
||||||
title: (
|
title: <JobState job={printJob} />,
|
||||||
<JobState
|
|
||||||
job={printJob}
|
|
||||||
text={
|
|
||||||
<>
|
|
||||||
Print Job
|
|
||||||
<Text code>
|
|
||||||
{printJob._id.substring(printJob._id.length - 6)}
|
|
||||||
</Text>
|
|
||||||
printJob.quantity
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
key: `printjob-${printJob._id}`,
|
key: `printjob-${printJob._id}`,
|
||||||
children: subJobs.map((subJob) => ({
|
children: subJobs.map((subJob) => ({
|
||||||
title: (
|
title: (
|
||||||
@ -112,7 +97,7 @@ const PrinterJobsTree = ({ subJobs: initialSubJobs }) => {
|
|||||||
if (updateData.subJobId) {
|
if (updateData.subJobId) {
|
||||||
setSubJobs((prevSubJobs) =>
|
setSubJobs((prevSubJobs) =>
|
||||||
prevSubJobs.map((subJob) => {
|
prevSubJobs.map((subJob) => {
|
||||||
if (subJob._id === updateData.id) {
|
if (subJob._id === updateData._id) {
|
||||||
return {
|
return {
|
||||||
...subJob,
|
...subJob,
|
||||||
state: updateData.state,
|
state: updateData.state,
|
||||||
@ -161,6 +146,7 @@ const PrinterJobsTree = ({ subJobs: initialSubJobs }) => {
|
|||||||
treeData={treeData}
|
treeData={treeData}
|
||||||
expandedKeys={expandedKeys}
|
expandedKeys={expandedKeys}
|
||||||
onExpand={setExpandedKeys}
|
onExpand={setExpandedKeys}
|
||||||
|
showLine={true}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -32,7 +32,7 @@ const PrinterState = ({
|
|||||||
if (socket && !initialized && printer?.id) {
|
if (socket && !initialized && printer?.id) {
|
||||||
setInitialized(true)
|
setInitialized(true)
|
||||||
socket.on('notify_printer_update', (statusUpdate) => {
|
socket.on('notify_printer_update', (statusUpdate) => {
|
||||||
if (statusUpdate?.id === printer.id && statusUpdate?.state) {
|
if (statusUpdate?._id === printer.id && statusUpdate?.state) {
|
||||||
setCurrentState(statusUpdate.state)
|
setCurrentState(statusUpdate.state)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -98,6 +98,10 @@ const PrinterState = ({
|
|||||||
setBadgeStatus('error')
|
setBadgeStatus('error')
|
||||||
setBadgeText('Error')
|
setBadgeText('Error')
|
||||||
break
|
break
|
||||||
|
case 'startup':
|
||||||
|
setBadgeStatus('warning')
|
||||||
|
setBadgeText('Startup')
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
setBadgeStatus('default')
|
setBadgeStatus('default')
|
||||||
setBadgeText(currentState.type)
|
setBadgeText(currentState.type)
|
||||||
@ -122,6 +126,7 @@ const PrinterState = ({
|
|||||||
currentState.type === 'deploying') ? (
|
currentState.type === 'deploying') ? (
|
||||||
<Progress
|
<Progress
|
||||||
percent={Math.round(currentState.progress * 100)}
|
percent={Math.round(currentState.progress * 100)}
|
||||||
|
status='active'
|
||||||
style={{ width: '150px', marginBottom: '2px' }}
|
style={{ width: '150px', marginBottom: '2px' }}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@ -40,6 +40,7 @@ const ProductionSidebar = () => {
|
|||||||
label: <Link to='/dashboard/production/overview'>Overview</Link>,
|
label: <Link to='/dashboard/production/overview'>Overview</Link>,
|
||||||
icon: <DashboardOutlined />
|
icon: <DashboardOutlined />
|
||||||
},
|
},
|
||||||
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
key: 'printers',
|
key: 'printers',
|
||||||
label: <Link to='/dashboard/production/printers'>Printers</Link>,
|
label: <Link to='/dashboard/production/printers'>Printers</Link>,
|
||||||
|
|||||||
@ -1,52 +1,197 @@
|
|||||||
import React from 'react'
|
import React, { useEffect, useContext, useState } from 'react'
|
||||||
import { Table } from 'antd'
|
import { Table, Typography } from 'antd'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import IdText from './IdText'
|
import IdText from './IdText'
|
||||||
|
import { AuditOutlined, PlayCircleOutlined } from '@ant-design/icons'
|
||||||
|
import { SocketContext } from '../context/SocketContext'
|
||||||
|
|
||||||
|
import PlusMinusIcon from '../../Icons/PlusMinusIcon'
|
||||||
|
|
||||||
|
const { Text } = Typography
|
||||||
|
|
||||||
const StockEventTable = ({ stockEvents }) => {
|
const StockEventTable = ({ stockEvents }) => {
|
||||||
|
const { socket } = useContext(SocketContext)
|
||||||
|
const [initialized, setInitialized] = useState(false)
|
||||||
|
const [stockEventsData, setStockEventsData] = useState(stockEvents)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Add WebSocket event listener for real-time updates
|
||||||
|
if (socket && !initialized) {
|
||||||
|
setInitialized(true)
|
||||||
|
socket.on('notify_stockevent_update', (updateData) => {
|
||||||
|
console.log('Received stock event update:', updateData)
|
||||||
|
setStockEventsData((prevData) => {
|
||||||
|
return prevData.map((stockEvent) => {
|
||||||
|
if (stockEvent?._id) {
|
||||||
|
console.log('UD', updateData)
|
||||||
|
console.log('SE', stockEvent)
|
||||||
|
if (stockEvent._id === updateData._id) {
|
||||||
|
return {
|
||||||
|
...stockEvent,
|
||||||
|
...updateData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (socket && initialized) {
|
||||||
|
console.log('Deregistering stock event update listener')
|
||||||
|
socket.off('notify_stockevent_update')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [socket, initialized])
|
||||||
|
|
||||||
|
const getTypeFilterProps = () => {
|
||||||
|
// Get unique types from the data
|
||||||
|
const uniqueTypes = [
|
||||||
|
...new Set(
|
||||||
|
stockEventsData.map((record) => {
|
||||||
|
const type = record.type.toLowerCase()
|
||||||
|
if (type === 'subjob') return 'Sub Job'
|
||||||
|
if (type === 'audit') return 'Audit Adjustment'
|
||||||
|
return type.charAt(0).toUpperCase() + type.slice(1)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
filters: uniqueTypes.map((type) => ({ text: type, value: type })),
|
||||||
|
onFilter: (value, record) => {
|
||||||
|
const recordType = record.type.toLowerCase()
|
||||||
|
if (recordType === 'subjob') {
|
||||||
|
return value === 'Sub Job'
|
||||||
|
} else if (recordType === 'audit') {
|
||||||
|
return value === 'Audit Adjustment'
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
value === recordType.charAt(0).toUpperCase() + recordType.slice(1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
key: 'icon',
|
||||||
|
width: 50,
|
||||||
|
render: (record) => {
|
||||||
|
switch (record.type.toLowerCase()) {
|
||||||
|
case 'subjob':
|
||||||
|
return <PlayCircleOutlined />
|
||||||
|
case 'audit':
|
||||||
|
return <AuditOutlined />
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Type',
|
title: 'Type',
|
||||||
dataIndex: 'type',
|
dataIndex: 'type',
|
||||||
key: 'type',
|
key: 'type',
|
||||||
render: (type) => type.charAt(0).toUpperCase() + type.slice(1)
|
width: 200,
|
||||||
|
sorter: (a, b) => a.type.localeCompare(b.type),
|
||||||
|
...getTypeFilterProps(),
|
||||||
|
render: (type) => {
|
||||||
|
switch (type.toLowerCase()) {
|
||||||
|
case 'subjob':
|
||||||
|
return 'Sub Job'
|
||||||
|
case 'audit':
|
||||||
|
return 'Audit Adjustment'
|
||||||
|
default:
|
||||||
|
return type.charAt(0).toUpperCase() + type.slice(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Value',
|
title: <PlusMinusIcon />,
|
||||||
dataIndex: 'value',
|
dataIndex: 'value',
|
||||||
key: 'value',
|
key: 'value',
|
||||||
render: (value) => value.toFixed(2) + 'g'
|
width: 100,
|
||||||
|
sorter: (a, b) => a.value - b.value,
|
||||||
|
render: (value) => {
|
||||||
|
const formattedValue = value.toFixed(2) + 'g'
|
||||||
|
return (
|
||||||
|
<Text type={value < 0 ? 'danger' : 'success'}>
|
||||||
|
{value > 0 ? '+' + formattedValue : formattedValue}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Sub Job ID',
|
title: 'ID',
|
||||||
render: (record) =>
|
width: 100,
|
||||||
record.subJob ? (
|
render: (record) => {
|
||||||
<IdText
|
if (record.subJob) {
|
||||||
id={record.subJob.number.toString().padStart(6, '0')}
|
return (
|
||||||
longId={false}
|
<IdText
|
||||||
type={'subjob'}
|
id={record.subJob.number.toString().padStart(6, '0')}
|
||||||
/>
|
longId={false}
|
||||||
) : (
|
type={'subjob'}
|
||||||
'n/a'
|
/>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
if (record.stockAudit) {
|
||||||
|
return (
|
||||||
|
<IdText
|
||||||
|
id={record.stockAudit._id}
|
||||||
|
longId={false}
|
||||||
|
type={'stockaudit'}
|
||||||
|
showHyperlink={true}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return 'n/a'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Job ID',
|
title: 'Job ID',
|
||||||
render: (record) =>
|
width: 100,
|
||||||
record.subJob ? (
|
render: (record) => {
|
||||||
<IdText id={record.job._id} longId={false} type={'job'} />
|
if (record.subJob) {
|
||||||
) : (
|
return (
|
||||||
'n/a'
|
<IdText
|
||||||
)
|
id={record.job._id}
|
||||||
|
longId={false}
|
||||||
|
type={'job'}
|
||||||
|
showHyperlink={true}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return 'n/a'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Timestamp',
|
title: 'Created At',
|
||||||
dataIndex: ['timestamp', '$date'],
|
dataIndex: 'createdAt',
|
||||||
key: 'timestamp',
|
key: 'createdAt',
|
||||||
render: (timestamp) => {
|
width: 180,
|
||||||
if (timestamp) {
|
defaultSortOrder: 'descend',
|
||||||
const formattedDate = moment(timestamp).format('YYYY-MM-DD HH:mm:ss')
|
sorter: (a, b) => moment(a.createdAt).unix() - moment(b.createdAt).unix(),
|
||||||
|
render: (createdAt) => {
|
||||||
|
if (createdAt) {
|
||||||
|
const formattedDate = moment(createdAt).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
return <span>{formattedDate}</span>
|
||||||
|
} else {
|
||||||
|
return 'n/a'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Updated At',
|
||||||
|
dataIndex: 'updatedAt',
|
||||||
|
key: 'updatedAt',
|
||||||
|
width: 180,
|
||||||
|
sorter: (a, b) => moment(a.updatedAt).unix() - moment(b.updatedAt).unix(),
|
||||||
|
render: (updatedAt) => {
|
||||||
|
if (updatedAt) {
|
||||||
|
const formattedDate = moment(updatedAt).format('YYYY-MM-DD HH:mm:ss')
|
||||||
return <span>{formattedDate}</span>
|
return <span>{formattedDate}</span>
|
||||||
} else {
|
} else {
|
||||||
return 'n/a'
|
return 'n/a'
|
||||||
@ -57,9 +202,9 @@ const StockEventTable = ({ stockEvents }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Table
|
<Table
|
||||||
dataSource={stockEvents}
|
dataSource={stockEventsData}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey={(record) => record._id.$oid}
|
rowKey={(record) => record._id}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -110,6 +110,7 @@ const SubJobState = ({
|
|||||||
currentState.type === 'processing') ? (
|
currentState.type === 'processing') ? (
|
||||||
<Progress
|
<Progress
|
||||||
percent={Math.round(currentState.progress * 100)}
|
percent={Math.round(currentState.progress * 100)}
|
||||||
|
status='active'
|
||||||
style={{ width: '150px', marginBottom: '2px' }}
|
style={{ width: '150px', marginBottom: '2px' }}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@ -140,7 +140,7 @@ const SubJobsTree = ({ printJobData }) => {
|
|||||||
...prevData,
|
...prevData,
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
subJobs: prevData.subJobs.map((subJob) => {
|
subJobs: prevData.subJobs.map((subJob) => {
|
||||||
if (subJob._id === updateData.id) {
|
if (subJob._id === updateData._id) {
|
||||||
return {
|
return {
|
||||||
...subJob,
|
...subJob,
|
||||||
state: updateData.state,
|
state: updateData.state,
|
||||||
@ -193,6 +193,7 @@ const SubJobsTree = ({ printJobData }) => {
|
|||||||
treeData={treeData}
|
treeData={treeData}
|
||||||
expandedKeys={expandedKeys}
|
expandedKeys={expandedKeys}
|
||||||
onExpand={setExpandedKeys}
|
onExpand={setExpandedKeys}
|
||||||
|
showLine={true}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|||||||
31
src/components/Dashboard/hooks/useCollapseState.js
Normal file
31
src/components/Dashboard/hooks/useCollapseState.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
|
const useCollapseState = (componentName, defaultState = {}) => {
|
||||||
|
const getInitialState = () => {
|
||||||
|
const stored = sessionStorage.getItem(`${componentName}_collapseState`)
|
||||||
|
if (stored) {
|
||||||
|
return JSON.parse(stored)
|
||||||
|
}
|
||||||
|
return defaultState
|
||||||
|
}
|
||||||
|
|
||||||
|
const [collapseState, setCollapseState] = useState(getInitialState)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sessionStorage.setItem(
|
||||||
|
`${componentName}_collapseState`,
|
||||||
|
JSON.stringify(collapseState)
|
||||||
|
)
|
||||||
|
}, [collapseState, componentName])
|
||||||
|
|
||||||
|
const updateCollapseState = (key, value) => {
|
||||||
|
setCollapseState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[key]: value
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return [collapseState, updateCollapseState]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useCollapseState
|
||||||
37
src/components/Dashboard/hooks/useColumnVisibility.js
Normal file
37
src/components/Dashboard/hooks/useColumnVisibility.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
|
const useColumnVisibility = (componentName, columns) => {
|
||||||
|
const getInitialVisibility = () => {
|
||||||
|
const stored = sessionStorage.getItem(`${componentName}_columnVisibility`)
|
||||||
|
if (stored) {
|
||||||
|
return JSON.parse(stored)
|
||||||
|
}
|
||||||
|
// Default visibility - all columns visible
|
||||||
|
return columns.reduce((acc, col) => {
|
||||||
|
if (col.key) {
|
||||||
|
acc[col.key] = true
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const [columnVisibility, setColumnVisibility] = useState(getInitialVisibility)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sessionStorage.setItem(
|
||||||
|
`${componentName}_columnVisibility`,
|
||||||
|
JSON.stringify(columnVisibility)
|
||||||
|
)
|
||||||
|
}, [columnVisibility, componentName])
|
||||||
|
|
||||||
|
const updateColumnVisibility = (key, value) => {
|
||||||
|
setColumnVisibility((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[key]: value
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return [columnVisibility, updateColumnVisibility]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useColumnVisibility
|
||||||
7
src/components/Icons/PlusMinusIcon.jsx
Normal file
7
src/components/Icons/PlusMinusIcon.jsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Icon from '@ant-design/icons'
|
||||||
|
import { ReactComponent as CustomIconSvg } from '../../assets/icons/plusminusicon.svg'
|
||||||
|
|
||||||
|
const PlusMinusIcon = (props) => <Icon component={CustomIconSvg} {...props} />
|
||||||
|
|
||||||
|
export default PlusMinusIcon
|
||||||
Loading…
x
Reference in New Issue
Block a user