finished hooking up the inventory adjustment slider when using manual control on the bin page

This commit is contained in:
csawatzky 2026-06-03 12:36:08 -06:00
parent b8a49197cb
commit 13c15ec195
6 changed files with 169 additions and 26 deletions

View file

@ -207,16 +207,16 @@ export default function BinTableExpanded(props: Props) {
<Typography sx={{fontWeight: 600, fontSize: 10}}>{user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "+20 °F" : "+10 °C"} above target</Typography> <Typography sx={{fontWeight: 600, fontSize: 10}}>{user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "+20 °F" : "+10 °C"} above target</Typography>
</Box> </Box>
</Box> </Box>
<Box sx={{ display: "flex", flexDirection: "row", overflowX: "auto", gap: 2}}> <Box sx={{ display: "flex", flexDirection: "row", overflowX: "auto", gap: 2, paddingBottom: 2}}>
{cables.map((cable, i) => ( {cables.map((cable, i) => (
<Box key={i} sx={{ flexShrink: 0 }}> <Box key={i} sx={{ flexShrink: 0 }}>
<Typography className={classes.headerCellStyle} sx={{ textAlign: "center" }}> <Typography className={classes.headerCellStyle} sx={{ textAlign: "center" }}>
{cable.name} {cable.name}
</Typography> </Typography>
{cableTable(cable)} {cableTable(cable)}
</Box>
))}
</Box> </Box>
))}
</Box>
</Box> </Box>
) )
} }

View file

@ -1,4 +1,4 @@
import { Box, Card, DialogContent, DialogTitle, Grid2, IconButton, LinearProgress, Switch, Typography } from "@mui/material"; import { Box, Card, DialogContent, DialogTitle, Grid2, IconButton, LinearProgress, Slider, Switch, Tooltip, Typography } from "@mui/material";
import { useMobile } from "hooks"; import { useMobile } from "hooks";
import { Bin, Component, Device } from "models"; import { Bin, Component, Device } from "models";
import Bin3dVisualizer from "./components/bin3dVisualizer"; import Bin3dVisualizer from "./components/bin3dVisualizer";
@ -13,9 +13,12 @@ import BinDetails from "./components/binDetails";
import { Plenum } from "models/Plenum"; import { Plenum } from "models/Plenum";
import BinSensorsDisplay from "../binSensorsDisplay"; import BinSensorsDisplay from "../binSensorsDisplay";
import ResponsiveDialog from "common/ResponsiveDialog"; import ResponsiveDialog from "common/ResponsiveDialog";
import { useState } from "react"; import { useEffect, useRef, useState } from "react";
import SearchSelect from "common/SearchSelect"; import SearchSelect from "common/SearchSelect";
import ChangeGrainDialog from "grain/ChangeGrainDialog"; import ChangeGrainDialog from "grain/ChangeGrainDialog";
import { useGlobalState } from "providers";
import GrainTransaction from "grain/GrainTransaction";
import { cloneDeep } from "lodash";
interface Props { interface Props {
bin: Bin bin: Bin
@ -32,13 +35,33 @@ interface Props {
React.SetStateAction<Map<string, pond.BinComponentPreferences> | undefined> React.SetStateAction<Map<string, pond.BinComponentPreferences> | undefined>
>; >;
updateBinCallback?: (bin: Bin) => void updateBinCallback?: (bin: Bin) => void
setBin?: React.Dispatch<React.SetStateAction<Bin>>;
} }
export default function BinSummary(props: Props){ export default function BinSummary(props: Props){
const {bin, devices, fans, heaters, permissions, componentDevices, componentMap, binPrefs, updateBinCallback, cables = [], plenums = [], setPreferences} = props const {bin, devices, fans, heaters, permissions, componentDevices, componentMap, binPrefs, updateBinCallback, cables = [], plenums = [], setPreferences, setBin} = props
//const [currentDevice, setCurrentDevice] = useState<Device | undefined>(undefined) //const [currentDevice, setCurrentDevice] = useState<Device | undefined>(undefined)
const isMobile = useMobile() const isMobile = useMobile()
const [openGrainDialog, setOpenGrainDialog] = useState(false) const [openGrainDialog, setOpenGrainDialog] = useState(false)
const [manualFillPercent, setManualFillPercent] = useState(0)
const [manualBushels, setManualBushels] = useState(0)
const [{user}] = useGlobalState()
const fillCapRef = useRef<HTMLSpanElement>(null);
const [fillCapWidth, setFillCapWidth] = useState<number | undefined>(undefined);
const [openNewTransaction, setOpenNewTransaction] = useState(false)
useEffect(() => {
if (fillCapRef.current) {
// measure the max possible string width once
fillCapRef.current.textContent = `${bin.grainCapacity(user)}/${bin.grainCapacity(user)}`;
setFillCapWidth(fillCapRef.current.offsetWidth);
}
}, [bin, user]);
useEffect(()=>{
setManualFillPercent(bin.fillPercent())
setManualBushels(bin.bushels())
},[bin])
// useEffect(() => { // useEffect(() => {
// if(devices.length > 0){ // if(devices.length > 0){
@ -76,6 +99,18 @@ export default function BinSummary(props: Props){
) )
} }
const currentFill = () => {
if(bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_MANUAL){
if(user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE && bin.bushelsPerTonne() > 1){
return Math.round((manualBushels / bin.bushelsPerTonne())*10)/10
}else if (user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TON && bin.bushelsPerTonne() > 1){
return Math.round((manualBushels / (bin.bushelsPerTonne() * 0.907))*10)/10
}
return Math.round(manualBushels)
}
return bin.grainInventory(user)
}
const inventorySummary = () => { const inventorySummary = () => {
return ( return (
<Card raised sx={{ marginBottom: 2 }}> <Card raised sx={{ marginBottom: 2 }}>
@ -111,14 +146,58 @@ export default function BinSummary(props: Props){
</Box> </Box>
{/* Bin Fill */} {/* Bin Fill */}
<Box sx={{ flexGrow: 1, maxWidth: 400, mx: 4 }}> <Box sx={{ flexGrow: 1, maxWidth: 450, mx: 4 }}>
<Typography variant="caption" sx={{ color: grey[500] }}> <Typography variant="caption" sx={{ color: grey[500] }}>
Bin Fill Bin Fill
</Typography> </Typography>
<Box display="flex" alignItems="center" gap={1} sx={{ mt: 0.5 }}> <Box display="flex" alignItems="center" gap={1} sx={{ mt: 0.5 }}>
<Typography variant="body2" noWrap> {/* hidden measuring span, renders off-screen */}
{bin.binFillCap()} <span
ref={fillCapRef}
style={{
position: "absolute",
visibility: "hidden",
whiteSpace: "nowrap",
fontSize: "0.875rem", // match body2
}}
/>
<Typography variant="body2" noWrap sx={{ width: fillCapWidth, flexShrink: 0 }}>
{currentFill()}/{bin.grainCapacity(user)}
</Typography> </Typography>
{bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_MANUAL ?
<Slider
value={manualFillPercent}
onChangeCommitted={() => {
setOpenNewTransaction(true)
}}
onChange={(_, val) => {
let fillPercent = val as number
setManualFillPercent(fillPercent)
let newBushels = (fillPercent/100) * bin.bushelCapacity()
setManualBushels(newBushels)
//this does casue the 3d bin to fill as you drag it but it throws off the transaction and the reset if you cancel
// let clone = cloneDeep(bin)
// let inventory = clone.settings.inventory ?? pond.BinInventory.create()
// inventory.grainBushels = newBushels
// if(setBin){
// setBin(clone)
// }
}}
min={0}
max={100}
sx={{
flexGrow: 1,
color: "#4caf50",
"& .MuiSlider-thumb": {
width: 16,
height: 16,
},
"& .MuiSlider-rail": {
backgroundColor: "rgba(255,255,255,0.1)",
},
}}
/>
:
<LinearProgress <LinearProgress
variant="determinate" variant="determinate"
value={bin.fillPercent()} value={bin.fillPercent()}
@ -133,6 +212,10 @@ export default function BinSummary(props: Props){
}, },
}} }}
/> />
}
<Typography variant="body2" noWrap sx={{ width: 100 }}>
{bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_MANUAL ? manualFillPercent : bin.fillPercent()}%
</Typography>
</Box> </Box>
</Box> </Box>
@ -141,15 +224,17 @@ export default function BinSummary(props: Props){
<Typography variant="caption" sx={{ color: grey[500] }}> <Typography variant="caption" sx={{ color: grey[500] }}>
Last Updated Last Updated
</Typography> </Typography>
<Box display="flex" alignItems="center" gap={2} sx={{ mt: 0.5 }}> <Tooltip title="Bin Status will update whenever its settings change or a device with a connected sensor checks in">
<Box display="flex" alignItems="center" gap={1}> <Box display="flex" alignItems="center" gap={2} sx={{ mt: 0.5 }}>
<AccessTime sx={{ fontSize: 16, color: grey[400] }} /> <Box display="flex" alignItems="center" gap={1}>
<Typography variant="body2" sx={{ color: grey[300] }}> <AccessTime sx={{ fontSize: 16, color: grey[400] }} />
{moment(bin.status.timestamp).fromNow()} <Typography variant="body2" sx={{ color: grey[300] }}>
</Typography> {moment(bin.status.timestamp).fromNow()}
</Typography>
</Box>
{activity(bin.status.timestamp)}
</Box> </Box>
{activity(bin.status.timestamp)} </Tooltip>
</Box>
</Box> </Box>
</Box> </Box>
</Card> </Card>
@ -158,6 +243,27 @@ export default function BinSummary(props: Props){
return ( return (
<Box padding={2}> <Box padding={2}>
<GrainTransaction
open={openNewTransaction}
mainObject={bin}
grainAdjustment={manualBushels - bin.bushels()}
close={(confirmed) => {
//this resets all of the state variables used
setOpenNewTransaction(false);
if(!confirmed){
setManualBushels(bin.bushels())
setManualFillPercent(bin.fillPercent())
}else{
let clone = cloneDeep(bin)
let inventory = clone.settings.inventory ?? pond.BinInventory.create()
inventory.grainBushels = manualBushels
if(setBin){
setBin(clone)
}
}
}}
/>
<ChangeGrainDialog bin={bin} open={openGrainDialog} closeDialog={()=>setOpenGrainDialog(false)} permissions={permissions} updateBin={updateBinCallback}/> <ChangeGrainDialog bin={bin} open={openGrainDialog} closeDialog={()=>setOpenGrainDialog(false)} permissions={permissions} updateBin={updateBinCallback}/>
{inventorySummary()} {inventorySummary()}
<Grid2 container spacing={2}> <Grid2 container spacing={2}>

View file

@ -300,7 +300,8 @@ export default function BinTableView(props: Props) {
}}> }}>
Show All Cables Show All Cables
</Button> </Button>
<Tooltip title="testing"> <Tooltip title="This table shows the bins levels using nodes that are in the grain averaged together. Data comes from the bins status.
Use the button to see all of the cables individually.">
<Info /> <Info />
</Tooltip> </Tooltip>
</Box> </Box>

View file

@ -37,7 +37,7 @@ interface Props {
asDestination?: boolean; asDestination?: boolean;
restrictMatching?: boolean; restrictMatching?: boolean;
open: boolean; open: boolean;
close: () => void; close: (confirmed?: boolean) => void;
callback?: (newTransaction?: pond.Transaction) => void; callback?: (newTransaction?: pond.Transaction) => void;
allowAttachmentUploads?: boolean; allowAttachmentUploads?: boolean;
} }
@ -82,7 +82,7 @@ export default function GrainTransaction(props: Props) {
const [filePermission, setFilePermission] = useState(false); const [filePermission, setFilePermission] = useState(false);
const closeDialogs = (confirmed: boolean, newTransaction?: pond.Transaction) => { const closeDialogs = (confirmed: boolean, newTransaction?: pond.Transaction) => {
close(); close(confirmed);
setGrainChangeVal("0"); setGrainChangeVal("0");
setGrainEntry("0"); setGrainEntry("0");
setIsObject(""); setIsObject("");

View file

@ -162,6 +162,11 @@ export class Bin {
return colour; return colour;
} }
/**
* this function returns the number of bushels in the bin, if using the automatic lidar or libracart to control inventory it will use the bushels in status
* otherwise will use the bushels in the inventory found in settings
* @returns (number) the current bushels in the bin
*/
public bushels(): number { public bushels(): number {
let control = this.settings.inventory?.inventoryControl; let control = this.settings.inventory?.inventoryControl;
let bushels = this.settings.inventory?.grainBushels || 0 let bushels = this.settings.inventory?.grainBushels || 0
@ -172,6 +177,14 @@ export class Bin {
return bushels return bushels
} }
/**
* this function returns the bushelCapacity in the bins specs as it is stored or 0 if the specs are undefined
* @returns (number) the capacity of the bin in bushels
*/
public bushelCapacity(): number {
return this.settings.specs?.bushelCapacity ?? 0
}
public fillPercent(): number { public fillPercent(): number {
let fill = 0; let fill = 0;
if (this.settings.inventory && this.settings.specs) { if (this.settings.inventory && this.settings.specs) {
@ -281,6 +294,12 @@ export class Bin {
return bpt; return bpt;
} }
/**
* this function returns a bins stored inventory in the unit of the passed in user,
* it can return it in bushels, US tons, or metric Tonnes
* @param user the user object
* @returns (number) bins current stored inventory
*/
public grainInventory(user: User): number { public grainInventory(user: User): number {
let grain = this.bushels() let grain = this.bushels()
if(user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE && this.bushelsPerTonne() > 1){ if(user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE && this.bushelsPerTonne() > 1){
@ -291,6 +310,22 @@ export class Bin {
return Math.round(grain*100)/100 return Math.round(grain*100)/100
} }
/**
* this function returns a bins capacity in the unit of the passed in user,
* it can return it in bushels, US tons, or metric Tonnes
* @param user the user object
* @returns (number) bins max capacity
*/
public grainCapacity(user: User): number {
let capacity = this.bushelCapacity()
if(user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE && this.bushelsPerTonne() > 1){
capacity = capacity / this.bushelsPerTonne()
}else if (user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TON && this.bushelsPerTonne() > 1){
capacity = capacity / (this.bushelsPerTonne() * 0.907)
}
return Math.round(capacity*100)/100
}
/** /**
* gets the enum value for the inventory control in the bins inventory * gets the enum value for the inventory control in the bins inventory
*/ */

View file

@ -553,6 +553,7 @@ export default function BinV2(){
openSnack("Bin has Been Updated") openSnack("Bin has Been Updated")
}) })
}} }}
setBin={setBin}
/> />
{/* render drawers */} {/* render drawers */}
{noteDrawer()} {noteDrawer()}