From b39e76154689ab593fd69a72774d44b363f41f98 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Mon, 2 Jun 2025 00:32:22 +0100 Subject: [PATCH] Added better UI and implmented missing features --- src/App.jsx | 2 +- .../Dashboard/Inventory/FilamentStocks.jsx | 233 +++++++++++++++--- .../Dashboard/Inventory/StockEvents.jsx | 130 ++++++---- .../Dashboard/Management/Settings.jsx | 4 +- 4 files changed, 293 insertions(+), 76 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 549170d..46e1028 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -174,7 +174,7 @@ const AppContent = () => { path='*' element={ } diff --git a/src/components/Dashboard/Inventory/FilamentStocks.jsx b/src/components/Dashboard/Inventory/FilamentStocks.jsx index 600ea1f..68baf6e 100644 --- a/src/components/Dashboard/Inventory/FilamentStocks.jsx +++ b/src/components/Dashboard/Inventory/FilamentStocks.jsx @@ -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 ( +
+ + + setSelectedKeys(e.target.value ? [e.target.value] : []) + } + onPressEnter={() => confirm()} + style={{ width: 200, display: 'block' }} + /> +
+ ) + } + + 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) => {filament.name} }, { @@ -171,7 +296,8 @@ const FilamentStocks = () => { title: 'Current (g)', dataIndex: 'currentNetWeight', key: 'currentNetWeight', - width: 120, + width: 140, + sorter: true, render: (currentNetWeight) => ( {currentNetWeight.toFixed(2) + 'g'} ) @@ -180,7 +306,8 @@ const FilamentStocks = () => { title: 'Starting (g)', dataIndex: 'startingNetWeight', key: 'startingNetWeight', - width: 120, + width: 140, + sorter: true, render: (startingNetWeight) => ( {startingNetWeight.toFixed(2) + 'g'} ) @@ -190,6 +317,8 @@ const FilamentStocks = () => { dataIndex: 'createdAt', key: 'createdAt', width: 180, + sorter: true, + defaultSortOrder: 'descend', render: (createdAt) => { if (createdAt) { return @@ -203,6 +332,7 @@ const FilamentStocks = () => { dataIndex: 'updatedAt', key: 'updatedAt', width: 180, + sorter: true, render: (updatedAt) => { if (updatedAt) { return @@ -236,6 +366,39 @@ const FilamentStocks = () => { } ] + const getViewDropdownItems = () => { + const columnItems = columns + .filter((col) => col.key && col.title !== '') + .map((col) => ( + { + updateColumnVisibility(col.key, e.target.checked) + }} + > + {col.title} + + )) + + return ( + + + {columnItems} + + + ) + } + + 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 = () => { <> {contextHolder} - - - - - + + + + + + + + + + {lazyLoading && } />} + }} scroll={{ y: 'calc(100vh - 270px)' }} + onScroll={handleScroll} + onChange={handleTableChange} + showSorterTooltip={false} /> { 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: , 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 @@ -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 @@ -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 ( +
+ + + setSelectedKeys(e.target.value ? [e.target.value] : []) + } + onPressEnter={() => confirm()} + style={{ width: 200, display: 'block' }} + /> +
+ ) + } + 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} diff --git a/src/components/Dashboard/Management/Settings.jsx b/src/components/Dashboard/Management/Settings.jsx index 4990b2c..ad0dfb0 100644 --- a/src/components/Dashboard/Management/Settings.jsx +++ b/src/components/Dashboard/Management/Settings.jsx @@ -91,7 +91,7 @@ const Settings = () => {