Enhance Inventory Overview and Model History Display Components
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- Updated InventoryOverview to include additional statistics for part stock, filament stock, product stock, and stock events, improving data visibility.
- Refactored InfoCollapse components to support new statistics and ensure proper state management for collapsible sections.
- Enhanced ModelHistoryDisplay to support both line and bar chart types, providing more flexibility in data visualization.
- Added new stats definitions for FilamentStock, PartStock, ProductStock, and StockEvent models to support the expanded inventory features.
This commit is contained in:
Tom Butcher 2026-08-24 22:54:06 +01:00
parent e0a5ff3cf8
commit 1edd48c1d7
7 changed files with 363 additions and 60 deletions

View File

@ -1,15 +1,29 @@
import { Flex } from 'antd' import { Flex } from 'antd'
import useCollapseState from '../hooks/useCollapseState' import useCollapseState from '../hooks/useCollapseState.jsx'
import StatsDisplay from '../common/StatsDisplay' import StatsDisplay from '../common/StatsDisplay'
import ModelHistoryDisplay from '../common/ModelHistoryDisplay'
import InfoCollapse from '../common/InfoCollapse' import InfoCollapse from '../common/InfoCollapse'
import ScrollBox from '../common/ScrollBox' import ScrollBox from '../common/ScrollBox'
import ObjectTable from '../common/ObjectTable'
const InventoryOverview = () => { const InventoryOverview = () => {
const [collapseState, updateCollapseState] = useCollapseState( const [collapseState, updateCollapseState] = useCollapseState(
'InventoryOverview', 'InventoryOverview',
{ {
inventoryStats: true, partStockStats: true,
purchaseOrderStats: true filamentStockStats: true,
productStockStats: true,
purchaseOrderStats: true,
stockEventStats: true,
partStockHistory: true,
newPartStocks: true,
filamentStockHistory: true,
unconsumedFilamentStocks: true,
productStockHistory: true,
draftProductStocks: true,
purchaseOrderHistory: true,
draftPurchaseOrders: true,
stockEventHistory: true
} }
) )
@ -25,15 +39,15 @@ const InventoryOverview = () => {
<ScrollBox> <ScrollBox>
<Flex vertical gap={'large'}> <Flex vertical gap={'large'}>
<InfoCollapse <InfoCollapse
title='Inventory Statistics' title='Part Stock Statistics'
icon={null} icon={null}
active={collapseState.inventoryStats} canCollapse={false}
active={collapseState.partStockStats}
onToggle={(isActive) => onToggle={(isActive) =>
updateCollapseState('inventoryStats', isActive) updateCollapseState('partStockStats', isActive)
} }
className='no-t-padding-collapse' className='no-t-padding-collapse'
collapseKey='inventoryStats' collapseKey='partStockStats'
canCollapse={false}
> >
<Flex <Flex
justify='flex-start' justify='flex-start'
@ -41,10 +55,49 @@ const InventoryOverview = () => {
wrap='wrap' wrap='wrap'
align='flex-start' align='flex-start'
> >
<Flex gap='middle' wrap='wrap'> <StatsDisplay objectType='partStock' />
<StatsDisplay objectType='partStock' /> </Flex>
<StatsDisplay objectType='filamentStock' /> </InfoCollapse>
</Flex>
<InfoCollapse
title='Filament Stock Statistics'
icon={null}
canCollapse={false}
active={collapseState.filamentStockStats}
onToggle={(isActive) =>
updateCollapseState('filamentStockStats', isActive)
}
className='no-t-padding-collapse'
collapseKey='filamentStockStats'
>
<Flex
justify='flex-start'
gap='middle'
wrap='wrap'
align='flex-start'
>
<StatsDisplay objectType='filamentStock' />
</Flex>
</InfoCollapse>
<InfoCollapse
title='Product Stock Statistics'
icon={null}
canCollapse={false}
active={collapseState.productStockStats}
onToggle={(isActive) =>
updateCollapseState('productStockStats', isActive)
}
className='no-t-padding-collapse'
collapseKey='productStockStats'
>
<Flex
justify='flex-start'
gap='middle'
wrap='wrap'
align='flex-start'
>
<StatsDisplay objectType='productStock' />
</Flex> </Flex>
</InfoCollapse> </InfoCollapse>
@ -69,8 +122,29 @@ const InventoryOverview = () => {
</Flex> </Flex>
</InfoCollapse> </InfoCollapse>
<InfoCollapse
title='Stock Event Statistics'
icon={null}
canCollapse={false}
active={collapseState.stockEventStats}
onToggle={(isActive) =>
updateCollapseState('stockEventStats', isActive)
}
className='no-t-padding-collapse'
collapseKey='stockEventStats'
>
<Flex
justify='flex-start'
gap='middle'
wrap='wrap'
align='flex-start'
>
<StatsDisplay objectType='stockEvent' />
</Flex>
</InfoCollapse>
<Flex gap='large' wrap='wrap'> <Flex gap='large' wrap='wrap'>
<Flex flex={1} vertical style={{ minWidth: '300px' }}> <Flex flex={1} vertical style={{ minWidth: '300px' }} gap='large'>
<InfoCollapse <InfoCollapse
title='Part Stock History' title='Part Stock History'
icon={null} icon={null}
@ -80,9 +154,23 @@ const InventoryOverview = () => {
} }
collapseKey='partStockHistory' collapseKey='partStockHistory'
canCollapse={false} canCollapse={false}
></InfoCollapse> >
<ModelHistoryDisplay objectType='partStock' chartType='line' />
</InfoCollapse>
<InfoCollapse
title='New Part Stocks'
icon={null}
active={collapseState.newPartStocks}
onToggle={(isActive) =>
updateCollapseState('newPartStocks', isActive)
}
collapseKey='newPartStocks'
canCollapse={false}
>
<ObjectTable type='partStock' masterFilter={{ state: 'new' }} />
</InfoCollapse>
</Flex> </Flex>
<Flex flex={1} vertical style={{ minWidth: '300px' }}> <Flex flex={1} vertical style={{ minWidth: '300px' }} gap='large'>
<InfoCollapse <InfoCollapse
title='Filament Stock History' title='Filament Stock History'
icon={null} icon={null}
@ -92,8 +180,95 @@ const InventoryOverview = () => {
} }
canCollapse={false} canCollapse={false}
collapseKey='filamentStockHistory' collapseKey='filamentStockHistory'
></InfoCollapse> >
<ModelHistoryDisplay
objectType='filamentStock'
chartType='line'
/>
</InfoCollapse>
<InfoCollapse
title='Unconsumed Filament Stocks'
icon={null}
active={collapseState.unconsumedFilamentStocks}
onToggle={(isActive) =>
updateCollapseState('unconsumedFilamentStocks', isActive)
}
canCollapse={false}
collapseKey='unconsumedFilamentStocks'
>
<ObjectTable
type='filamentStock'
masterFilter={{ state: 'unconsumed' }}
/>
</InfoCollapse>
</Flex> </Flex>
</Flex>
<Flex gap='large' wrap='wrap'>
<Flex flex={1} vertical style={{ minWidth: '300px' }} gap='large'>
<InfoCollapse
title='Product Stock History'
icon={null}
active={collapseState.productStockHistory}
onToggle={(isActive) =>
updateCollapseState('productStockHistory', isActive)
}
collapseKey='productStockHistory'
canCollapse={false}
>
<ModelHistoryDisplay
objectType='productStock'
chartType='line'
/>
</InfoCollapse>
<InfoCollapse
title='Draft Product Stocks'
icon={null}
active={collapseState.draftProductStocks}
onToggle={(isActive) =>
updateCollapseState('draftProductStocks', isActive)
}
collapseKey='draftProductStocks'
canCollapse={false}
>
<ObjectTable
type='productStock'
masterFilter={{ state: 'draft' }}
/>
</InfoCollapse>
</Flex>
<Flex flex={1} vertical style={{ minWidth: '300px' }} gap='large'>
<InfoCollapse
title='Purchase Order History'
icon={null}
active={collapseState.purchaseOrderHistory}
onToggle={(isActive) =>
updateCollapseState('purchaseOrderHistory', isActive)
}
canCollapse={false}
collapseKey='purchaseOrderHistory'
>
<ModelHistoryDisplay objectType='purchaseOrder' />
</InfoCollapse>
<InfoCollapse
title='Draft Purchase Orders'
icon={null}
active={collapseState.draftPurchaseOrders}
onToggle={(isActive) =>
updateCollapseState('draftPurchaseOrders', isActive)
}
canCollapse={false}
collapseKey='draftPurchaseOrders'
>
<ObjectTable
type='purchaseOrder'
masterFilter={{ state: 'draft' }}
/>
</InfoCollapse>
</Flex>
</Flex>
<Flex gap='large' wrap='wrap'>
<Flex flex={1} vertical style={{ minWidth: '300px' }}> <Flex flex={1} vertical style={{ minWidth: '300px' }}>
<InfoCollapse <InfoCollapse
title='Stock Event History' title='Stock Event History'
@ -104,7 +279,9 @@ const InventoryOverview = () => {
} }
canCollapse={false} canCollapse={false}
collapseKey='stockEventHistory' collapseKey='stockEventHistory'
></InfoCollapse> >
<ModelHistoryDisplay objectType='stockEvent' chartType='line' />
</InfoCollapse>
</Flex> </Flex>
</Flex> </Flex>
</Flex> </Flex>

View File

@ -4,6 +4,8 @@ import {
ResponsiveContainer, ResponsiveContainer,
BarChart, BarChart,
Bar, Bar,
LineChart,
Line,
CartesianGrid, CartesianGrid,
XAxis, XAxis,
YAxis, YAxis,
@ -26,7 +28,8 @@ const ModelHistoryDisplay = ({
startDate, startDate,
endDate, endDate,
styles, styles,
height = 400 height = 400,
chartType = 'bar'
}) => { }) => {
const { getModelHistory, connected } = useContext(ApiServerContext) const { getModelHistory, connected } = useContext(ApiServerContext)
const { token } = useContext(AuthContext) const { token } = useContext(AuthContext)
@ -120,7 +123,9 @@ const ModelHistoryDisplay = ({
return null return null
} }
const modelStats = model.stats || [] const modelStats = (model.stats || []).filter(
(statDef) => statDef.history !== false
)
if (modelStats.length === 0) { if (modelStats.length === 0) {
return null return null
@ -323,6 +328,70 @@ const ModelHistoryDisplay = ({
</Space.Compact> </Space.Compact>
) )
const axisTickStyle = {
fill: isDarkMode ? '#d9d9d9' : '#595959',
fontSize: 12
}
const tooltipStyle = {
backgroundColor: isDarkMode ? '#1f1f1f' : '#ffffff',
border: `1px solid ${isDarkMode ? '#303030' : '#f0f0f0'}`,
borderRadius: 8
}
const renderChart = () => {
const commonElements = (
<>
<CartesianGrid
strokeDasharray='3 3'
stroke={isDarkMode ? '#303030' : '#f0f0f0'}
/>
<XAxis dataKey='dateFormatted' tick={axisTickStyle} />
<YAxis tick={axisTickStyle} />
<Tooltip contentStyle={tooltipStyle} />
<Legend />
</>
)
if (chartType === 'line') {
return (
<LineChart data={chartRows}>
{commonElements}
{modelStats.map((statDef, index) => {
const category = statDef.label || statDef.name
return (
<Line
key={category}
type='monotone'
dataKey={category}
stroke={colorRange[index] || colors.default}
strokeWidth={2}
dot={false}
/>
)
})}
</LineChart>
)
}
return (
<BarChart data={chartRows}>
{commonElements}
{modelStats.map((statDef, index) => {
const category = statDef.label || statDef.name
return (
<Bar
key={category}
dataKey={category}
stackId='history'
fill={colorRange[index] || colors.default}
/>
)
})}
</BarChart>
)
}
return ( return (
<Card <Card
style={{ width: '100%' }} style={{ width: '100%' }}
@ -370,44 +439,7 @@ const ModelHistoryDisplay = ({
{chartRows.length > 0 && ( {chartRows.length > 0 && (
<div style={{ width: '100%', height: `${height}px` }}> <div style={{ width: '100%', height: `${height}px` }}>
<ResponsiveContainer width='100%' height='100%'> <ResponsiveContainer width='100%' height='100%'>
<BarChart data={chartRows}> {renderChart()}
<CartesianGrid
strokeDasharray='3 3'
stroke={isDarkMode ? '#303030' : '#f0f0f0'}
/>
<XAxis
dataKey='dateFormatted'
tick={{
fill: isDarkMode ? '#d9d9d9' : '#595959',
fontSize: 12
}}
/>
<YAxis
tick={{
fill: isDarkMode ? '#d9d9d9' : '#595959',
fontSize: 12
}}
/>
<Tooltip
contentStyle={{
backgroundColor: isDarkMode ? '#1f1f1f' : '#ffffff',
border: `1px solid ${isDarkMode ? '#303030' : '#f0f0f0'}`,
borderRadius: 8
}}
/>
<Legend />
{modelStats.map((statDef, index) => {
const category = statDef.label || statDef.name
return (
<Bar
key={category}
dataKey={category}
stackId='history'
fill={colorRange[index] || colors.default}
/>
)
})}
</BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
)} )}
@ -433,7 +465,8 @@ ModelHistoryDisplay.propTypes = {
]), ]),
styles: PropTypes.object, styles: PropTypes.object,
endDate: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]), endDate: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]),
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]) height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
chartType: PropTypes.oneOf(['line', 'bar'])
} }
export default ModelHistoryDisplay export default ModelHistoryDisplay

View File

@ -188,6 +188,27 @@ export const FilamentStock = {
} }
], ],
stats: [ stats: [
{
name: 'unconsumed.count',
label: 'Unconsumed',
type: 'number',
color: 'success',
history: false
},
{
name: 'used.count',
label: 'Used',
type: 'number',
color: 'warning',
history: false
},
{
name: 'consumed.count',
label: 'Consumed',
type: 'number',
color: 'default',
history: false
},
{ {
name: 'totalCurrentWeight.sum', name: 'totalCurrentWeight.sum',
label: 'Total Current Weight', label: 'Total Current Weight',

View File

@ -199,6 +199,27 @@ export const PartStock = {
} }
], ],
stats: [ stats: [
{
name: 'new.count',
label: 'New',
type: 'number',
color: 'success',
history: false
},
{
name: 'used.count',
label: 'Used',
type: 'number',
color: 'warning',
history: false
},
{
name: 'consumed.count',
label: 'Consumed',
type: 'number',
color: 'default',
history: false
},
{ {
name: 'totalCurrentQuantity.sum', name: 'totalCurrentQuantity.sum',
label: 'Total Current Quantity', label: 'Total Current Quantity',

View File

@ -419,13 +419,29 @@ export const ProductStock = {
name: 'draft.count', name: 'draft.count',
label: 'Draft', label: 'Draft',
type: 'number', type: 'number',
color: 'default' color: 'default',
history: false
}, },
{ {
name: 'new.count', name: 'new.count',
label: 'New', label: 'New',
type: 'number', type: 'number',
color: 'success' color: 'success',
history: false
},
{
name: 'used.count',
label: 'Used',
type: 'number',
color: 'warning',
history: false
},
{
name: 'consumed.count',
label: 'Consumed',
type: 'number',
color: 'default',
history: false
}, },
{ {
name: 'totalCurrentQuantity.sum', name: 'totalCurrentQuantity.sum',

View File

@ -437,6 +437,18 @@ export const PurchaseOrder = {
label: 'Received', label: 'Received',
type: 'number', type: 'number',
color: 'success' color: 'success'
},
{
name: 'cancelled.count',
label: 'Cancelled',
type: 'number',
color: 'error'
},
{
name: 'completed.count',
label: 'Completed',
type: 'number',
color: 'success'
} }
] ]
} }

View File

@ -93,5 +93,28 @@ export const StockEvent = {
return objectData.unit return objectData.unit
} }
} }
],
stats: [
{
name: 'partStock.count',
label: 'Part Stock Events',
type: 'number',
color: 'processing',
cardWidth: 200
},
{
name: 'filamentStock.count',
label: 'Filament Stock Events',
type: 'number',
color: 'warning',
cardWidth: 220
},
{
name: 'productStock.count',
label: 'Product Stock Events',
type: 'number',
color: 'success',
cardWidth: 220
}
] ]
} }