Added better UI and implmented missing features
This commit is contained in:
parent
ce8f4cb038
commit
b39e761546
@ -174,7 +174,7 @@ const AppContent = () => {
|
||||
path='*'
|
||||
element={
|
||||
<AppError
|
||||
message='404! Page not found.'
|
||||
message='Error 404. Page not found.'
|
||||
showRefresh={false}
|
||||
/>
|
||||
}
|
||||
|
||||
@ -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 () => {
|
||||
try {
|
||||
const response = await axios.get(`${config.backendUrl}/filamentstocks`, {
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
withCredentials: true // Important for including cookies
|
||||
})
|
||||
setFilamentStocksData(response.data)
|
||||
setLoading(false)
|
||||
} catch (err) {
|
||||
messageApi.info(err)
|
||||
}
|
||||
}, [messageApi])
|
||||
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`,
|
||||
{
|
||||
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) {
|
||||
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>
|
||||
<Dropdown menu={actionItems}>
|
||||
<Button>Actions</Button>
|
||||
</Dropdown>
|
||||
</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
|
||||
|
||||
@ -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,7 +377,12 @@ const StockEvents = () => {
|
||||
const newFilters = {}
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (value && value.length > 0) {
|
||||
newFilters[key] = value[0]
|
||||
// Convert type filter to camelCase
|
||||
if (key === 'type') {
|
||||
newFilters[key] = toCamelCase(value[0])
|
||||
} else {
|
||||
newFilters[key] = value[0]
|
||||
}
|
||||
}
|
||||
})
|
||||
setPage(1)
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user