Added better UI and implmented missing features

This commit is contained in:
Tom Butcher 2025-06-02 00:32:22 +01:00
parent ce8f4cb038
commit b39e761546
4 changed files with 293 additions and 76 deletions

View File

@ -174,7 +174,7 @@ const AppContent = () => {
path='*'
element={
<AppError
message='404! Page not found.'
message='Error 404. Page not found.'
showRefresh={false}
/>
}

View File

@ -11,7 +11,11 @@ import {
Modal,
message,
Dropdown,
Typography
Typography,
Popover,
Checkbox,
Input,
Spin
} from 'antd'
import { createStyles } from 'antd-style'
import { LoadingOutlined } from '@ant-design/icons'
@ -27,6 +31,9 @@ import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import FilamentStockState from '../common/FilamentStockState'
import TimeDisplay from '../common/TimeDisplay'
import XMarkIcon from '../../Icons/XMarkIcon'
import CheckIcon from '../../Icons/CheckIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import config from '../../../config'
@ -57,38 +64,105 @@ const FilamentStocks = () => {
const { socket } = useContext(SocketContext)
const [filamentStocksData, setFilamentStocksData] = useState([])
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [loading, setLoading] = useState(true)
const [lazyLoading, setLazyLoading] = useState(false)
const [filters, setFilters] = useState({})
const [sorter, setSorter] = useState({
field: 'createdAt',
order: 'descend'
})
const [newFilamentStockOpen, setNewFilamentStockOpen] = useState(false)
const [loading, setLoading] = useState(true)
const [initialized, setInitialized] = useState(false)
const { authenticated } = useContext(AuthContext)
const fetchFilamentStocksData = useCallback(async () => {
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={<XMarkIcon />}
/>
<Button
type='primary'
onClick={() => confirm()}
icon={<CheckIcon />}
/>
</Space.Compact>
</div>
)
}
const fetchFilamentStocksData = useCallback(
async (pageNum = 1, append = false) => {
try {
const response = await axios.get(`${config.backendUrl}/filamentstocks`, {
const response = await axios.get(
`${config.backendUrl}/filamentstocks`,
{
params: {
page: pageNum,
limit: 25,
...filters,
sort: sorter.field,
order: sorter.order
},
headers: {
Accept: 'application/json'
},
withCredentials: true // Important for including cookies
})
setFilamentStocksData(response.data)
setLoading(false)
} catch (err) {
messageApi.info(err)
withCredentials: true
}
}, [messageApi])
)
const newData = response.data
setHasMore(newData.length === 25)
if (append) {
setFilamentStocksData((prev) => [...prev, ...newData])
} else {
setFilamentStocksData(newData)
}
setLoading(false)
setLazyLoading(false)
} catch (err) {
messageApi.error('Error fetching filament stocks:', err)
setLoading(false)
setLazyLoading(false)
}
},
[messageApi, filters, sorter]
)
useEffect(() => {
// Fetch initial data
if (authenticated) {
fetchFilamentStocksData()
}
}, [authenticated, fetchFilamentStocksData])
useEffect(() => {
// Add WebSocket event listener for real-time updates
if (socket && !initialized) {
setInitialized(true)
socket.on('notify_filamentstock_update', (updateData) => {
@ -134,6 +208,43 @@ const FilamentStocks = () => {
}
}
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)
fetchFilamentStocksData(nextPage, true)
}
},
[page, lazyLoading, hasMore, fetchFilamentStocksData]
)
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
})
fetchFilamentStocksData(1)
}
// Column definitions
const columns = [
{
@ -150,6 +261,20 @@ const FilamentStocks = () => {
key: 'name',
width: 200,
fixed: 'left',
sorter: true,
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName: 'filament name'
}),
render: (filament) => <Text ellipsis>{filament.name}</Text>
},
{
@ -171,7 +296,8 @@ const FilamentStocks = () => {
title: 'Current (g)',
dataIndex: 'currentNetWeight',
key: 'currentNetWeight',
width: 120,
width: 140,
sorter: true,
render: (currentNetWeight) => (
<Text ellipsis>{currentNetWeight.toFixed(2) + 'g'}</Text>
)
@ -180,7 +306,8 @@ const FilamentStocks = () => {
title: 'Starting (g)',
dataIndex: 'startingNetWeight',
key: 'startingNetWeight',
width: 120,
width: 140,
sorter: true,
render: (startingNetWeight) => (
<Text ellipsis>{startingNetWeight.toFixed(2) + 'g'}</Text>
)
@ -190,6 +317,8 @@ const FilamentStocks = () => {
dataIndex: 'createdAt',
key: 'createdAt',
width: 180,
sorter: true,
defaultSortOrder: 'descend',
render: (createdAt) => {
if (createdAt) {
return <TimeDisplay dateTime={createdAt} />
@ -203,6 +332,7 @@ const FilamentStocks = () => {
dataIndex: 'updatedAt',
key: 'updatedAt',
width: 180,
sorter: true,
render: (updatedAt) => {
if (updatedAt) {
return <TimeDisplay dateTime={updatedAt} />
@ -236,6 +366,39 @@ const FilamentStocks = () => {
}
]
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 [columnVisibility, updateColumnVisibility] = useColumnVisibility(
'FilamentStocks',
columns
)
const visibleColumns = columns.filter(
(col) => !col.key || columnVisibility[col.key]
)
const actionItems = {
items: [
{
@ -252,7 +415,8 @@ const FilamentStocks = () => {
],
onClick: ({ key }) => {
if (key === 'reloadList') {
fetchFilamentStocksData()
setPage(1)
fetchFilamentStocksData(1)
} else if (key === 'newFilamentStock') {
setNewFilamentStockOpen(true)
}
@ -263,19 +427,32 @@ const FilamentStocks = () => {
<>
<Flex vertical={'true'} gap='large'>
{contextHolder}
<Space>
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<Popover
content={getViewDropdownItems()}
placement='bottomLeft'
arrow={false}
>
<Button>View</Button>
</Popover>
</Space>
{lazyLoading && <Spin indicator={<LoadingOutlined />} />}
</Flex>
<Table
dataSource={filamentStocksData}
className={styles.customTable}
columns={columns}
columns={visibleColumns}
pagination={false}
rowKey='_id'
loading={{ spinning: loading, indicator: <LoadingOutlined spin /> }}
scroll={{ y: 'calc(100vh - 270px)' }}
onScroll={handleScroll}
onChange={handleTableChange}
showSorterTooltip={false}
/>
</Flex>
<Modal

View File

@ -11,11 +11,11 @@ import {
Checkbox,
Dropdown,
Table,
Typography
Typography,
Input
} from 'antd'
import { createStyles } from 'antd-style'
import { LoadingOutlined, AuditOutlined } from '@ant-design/icons'
import moment from 'moment'
import { AuthContext } from '../context/AuthContext'
import { SocketContext } from '../context/SocketContext'
@ -25,6 +25,9 @@ import ReloadIcon from '../../Icons/ReloadIcon'
import PlusMinusIcon from '../../Icons/PlusMinusIcon'
import SubJobIcon from '../../Icons/SubJobIcon'
import PlayCircleIcon from '../../Icons/PlayCircleIcon'
import XMarkIcon from '../../Icons/XMarkIcon'
import CheckIcon from '../../Icons/CheckIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import config from '../../../config'
@ -54,6 +57,16 @@ const StockEvents = () => {
const { socket } = useContext(SocketContext)
const [initialized, setInitialized] = useState(false)
// Helper function to convert text to camelCase
const toCamelCase = (text) => {
return text
.toLowerCase()
.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => {
return index === 0 ? word.toLowerCase() : word.toUpperCase()
})
.replace(/\s+/g, '')
}
const [stockEventsData, setStockEventsData] = useState([])
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
@ -61,7 +74,10 @@ const StockEvents = () => {
const [lazyLoading, setLazyLoading] = useState(false)
const [filters, setFilters] = useState({})
const [sorter, setSorter] = useState({})
const [sorter, setSorter] = useState({
field: 'createdAt',
order: 'descend'
})
// Column definitions for visibility
const columns = [
@ -87,40 +103,27 @@ const StockEvents = () => {
dataIndex: 'type',
key: 'type',
width: 200,
sorter: (a, b) => a.type.localeCompare(b.type),
filters: [
{ text: 'Sub Job', value: 'Sub Job' },
{ text: 'Audit Adjustment', value: 'Audit Adjustment' },
{ text: 'Initial', value: 'Initial' }
],
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)
)
},
render: (type) => {
switch (type.toLowerCase()) {
case 'subjob':
return 'Sub Job'
case 'audit':
return 'Audit Adjustment'
default:
return type.charAt(0).toUpperCase() + type.slice(1)
}
}
sorter: true,
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
propertyName: 'type'
})
},
{
title: <PlusMinusIcon />,
dataIndex: 'value',
key: 'value',
width: 100,
sorter: (a, b) => a.value - b.value,
sorter: true,
render: (value, record) => {
const formattedValue = value.toFixed(2) + record.unit
return (
@ -180,8 +183,8 @@ const StockEvents = () => {
dataIndex: 'createdAt',
key: 'createdAt',
width: 180,
sorter: true,
defaultSortOrder: 'descend',
sorter: (a, b) => moment(a.createdAt).unix() - moment(b.createdAt).unix(),
render: (createdAt) => {
if (createdAt) {
return <TimeDisplay dateTime={createdAt} />
@ -195,7 +198,7 @@ const StockEvents = () => {
dataIndex: 'updatedAt',
key: 'updatedAt',
width: 180,
sorter: (a, b) => moment(a.updatedAt).unix() - moment(b.updatedAt).unix(),
sorter: true,
render: (updatedAt) => {
if (updatedAt) {
return <TimeDisplay dateTime={updatedAt} />
@ -206,15 +209,47 @@ const StockEvents = () => {
}
]
const [columnVisibility, setColumnVisibility] = useState(
columns.reduce((acc, col) => {
if (col.key) {
acc[col.key] = true
}
return acc
}, {})
const [columnVisibility, updateColumnVisibility] = useColumnVisibility(
'StockEvents',
columns
)
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={<XMarkIcon />}
/>
<Button
type='primary'
onClick={() => confirm()}
icon={<CheckIcon />}
/>
</Space.Compact>
</div>
)
}
const { authenticated } = useContext(AuthContext)
const fetchStockEventsData = useCallback(
@ -224,6 +259,7 @@ const StockEvents = () => {
params: {
page: pageNum,
limit: 25,
type: filters.type,
...filters,
sort: sorter.field,
order: sorter.order
@ -341,8 +377,13 @@ const StockEvents = () => {
const newFilters = {}
Object.entries(filters).forEach(([key, value]) => {
if (value && value.length > 0) {
// Convert type filter to camelCase
if (key === 'type') {
newFilters[key] = toCamelCase(value[0])
} else {
newFilters[key] = value[0]
}
}
})
setPage(1)
setFilters(newFilters)
@ -350,6 +391,8 @@ const StockEvents = () => {
field: sorter.field,
order: sorter.order
})
// Trigger a new fetch with the updated filters
fetchStockEventsData(1)
}
const getViewDropdownItems = () => {
@ -360,10 +403,7 @@ const StockEvents = () => {
checked={columnVisibility[col.key]}
key={col.key}
onChange={(e) => {
setColumnVisibility((prev) => ({
...prev,
[col.key]: e.target.checked
}))
updateColumnVisibility(col.key, e.target.checked)
}}
>
{col.title}

View File

@ -91,7 +91,7 @@ const Settings = () => {
<Select
value={getCurrentThemeValue()}
onChange={handleThemeChange}
style={{ width: '200px' }}
style={{ width: '100%' }}
>
<Option value='light'>Light</Option>
<Option value='dark'>Dark</Option>
@ -102,7 +102,7 @@ const Settings = () => {
<Select
value={isCompact ? 'compact' : 'comfortable'}
onChange={handleCompactChange}
style={{ width: '200px' }}
style={{ width: '100%' }}
>
<Option value='comfortable'>Comfortable</Option>
<Option value='compact'>Compact</Option>