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 useCollapseState from '../hooks/useCollapseState'
import useCollapseState from '../hooks/useCollapseState.jsx'
import StatsDisplay from '../common/StatsDisplay'
import ModelHistoryDisplay from '../common/ModelHistoryDisplay'
import InfoCollapse from '../common/InfoCollapse'
import ScrollBox from '../common/ScrollBox'
import ObjectTable from '../common/ObjectTable'
const InventoryOverview = () => {
const [collapseState, updateCollapseState] = useCollapseState(
'InventoryOverview',
{
inventoryStats: true,
purchaseOrderStats: true
partStockStats: 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>
<Flex vertical gap={'large'}>
<InfoCollapse
title='Inventory Statistics'
title='Part Stock Statistics'
icon={null}
active={collapseState.inventoryStats}
canCollapse={false}
active={collapseState.partStockStats}
onToggle={(isActive) =>
updateCollapseState('inventoryStats', isActive)
updateCollapseState('partStockStats', isActive)
}
className='no-t-padding-collapse'
collapseKey='inventoryStats'
canCollapse={false}
collapseKey='partStockStats'
>
<Flex
justify='flex-start'
@ -41,10 +55,49 @@ const InventoryOverview = () => {
wrap='wrap'
align='flex-start'
>
<Flex gap='middle' wrap='wrap'>
<StatsDisplay objectType='partStock' />
<StatsDisplay objectType='filamentStock' />
</Flex>
<StatsDisplay objectType='partStock' />
</Flex>
</InfoCollapse>
<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>
</InfoCollapse>
@ -69,8 +122,29 @@ const InventoryOverview = () => {
</Flex>
</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 flex={1} vertical style={{ minWidth: '300px' }}>
<Flex flex={1} vertical style={{ minWidth: '300px' }} gap='large'>
<InfoCollapse
title='Part Stock History'
icon={null}
@ -80,9 +154,23 @@ const InventoryOverview = () => {
}
collapseKey='partStockHistory'
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={1} vertical style={{ minWidth: '300px' }}>
<Flex flex={1} vertical style={{ minWidth: '300px' }} gap='large'>
<InfoCollapse
title='Filament Stock History'
icon={null}
@ -92,8 +180,95 @@ const InventoryOverview = () => {
}
canCollapse={false}
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 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' }}>
<InfoCollapse
title='Stock Event History'
@ -104,7 +279,9 @@ const InventoryOverview = () => {
}
canCollapse={false}
collapseKey='stockEventHistory'
></InfoCollapse>
>
<ModelHistoryDisplay objectType='stockEvent' chartType='line' />
</InfoCollapse>
</Flex>
</Flex>
</Flex>

View File

@ -4,6 +4,8 @@ import {
ResponsiveContainer,
BarChart,
Bar,
LineChart,
Line,
CartesianGrid,
XAxis,
YAxis,
@ -26,7 +28,8 @@ const ModelHistoryDisplay = ({
startDate,
endDate,
styles,
height = 400
height = 400,
chartType = 'bar'
}) => {
const { getModelHistory, connected } = useContext(ApiServerContext)
const { token } = useContext(AuthContext)
@ -120,7 +123,9 @@ const ModelHistoryDisplay = ({
return null
}
const modelStats = model.stats || []
const modelStats = (model.stats || []).filter(
(statDef) => statDef.history !== false
)
if (modelStats.length === 0) {
return null
@ -323,6 +328,70 @@ const ModelHistoryDisplay = ({
</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 (
<Card
style={{ width: '100%' }}
@ -370,44 +439,7 @@ const ModelHistoryDisplay = ({
{chartRows.length > 0 && (
<div style={{ width: '100%', height: `${height}px` }}>
<ResponsiveContainer width='100%' height='100%'>
<BarChart data={chartRows}>
<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>
{renderChart()}
</ResponsiveContainer>
</div>
)}
@ -433,7 +465,8 @@ ModelHistoryDisplay.propTypes = {
]),
styles: PropTypes.object,
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

View File

@ -188,6 +188,27 @@ export const FilamentStock = {
}
],
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',
label: 'Total Current Weight',

View File

@ -199,6 +199,27 @@ export const PartStock = {
}
],
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',
label: 'Total Current Quantity',

View File

@ -419,13 +419,29 @@ export const ProductStock = {
name: 'draft.count',
label: 'Draft',
type: 'number',
color: 'default'
color: 'default',
history: false
},
{
name: 'new.count',
label: 'New',
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',

View File

@ -437,6 +437,18 @@ export const PurchaseOrder = {
label: 'Received',
type: 'number',
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
}
}
],
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
}
]
}