finished some tweaks for the new bin page

This commit is contained in:
csawatzky 2026-06-10 16:06:07 -06:00
parent 87382dd5d8
commit 2cffc20daa
10 changed files with 225 additions and 130 deletions

View file

@ -172,8 +172,9 @@ export function BuildCableData(bin: Bin, dims: BinDims): CableData[] {
distributed.forEach(orbit => { distributed.forEach(orbit => {
const count = orbit.assignedCount ?? 0; const count = orbit.assignedCount ?? 0;
// const topY = getTopY(orbit.radius, dims); //this is if we want the cables to start at the roof const topY = getTopY(orbit.radius, dims); //this is if we want the cables to start at the roof
const topY = dims.sidewallHeight/2 //this will start the cables at the top of the sidewall //const topY = dims.sidewallHeight/2 //this will start the cables at the top of the sidewall
const bottomY = getBottomY(orbit.radius, dims); const bottomY = getBottomY(orbit.radius, dims);
if (orbit.orbit === 0 && count > 0) { if (orbit.orbit === 0 && count > 0) {

View file

@ -92,6 +92,7 @@ export default function Bin3dView(props: Props) {
dims.diameter, dims.diameter,
dims.sidewallHeight, dims.sidewallHeight,
dims.hopperHeight, dims.hopperHeight,
dims.roofHeight,
fillPercent fillPercent
); );
}, [isCableInventory, fillPercent, dims]); }, [isCableInventory, fillPercent, dims]);

View file

@ -133,11 +133,18 @@ export default function MoistureHeatMap({ binDimensions, targetMoisture, nodes,
const binRadius = binDimensions.diameter / 2; const binRadius = binDimensions.diameter / 2;
const sidewallHeight = binDimensions.sidewallHeight; const sidewallHeight = binDimensions.sidewallHeight;
const hopperHeight = binDimensions.hopperHeight ?? 0; const hopperHeight = binDimensions.hopperHeight ?? 0;
const roofHeight = binDimensions.roofHeight ?? 0;
const sidewallBaseY = -sidewallHeight / 2; const sidewallBaseY = -sidewallHeight / 2;
const sidewallTopY = sidewallHeight / 2;
const hopperTipY = sidewallBaseY - hopperHeight; const hopperTipY = sidewallBaseY - hopperHeight;
const roofTipY = sidewallTopY + roofHeight;
const maxRadiusAtY = (y: number) => { const maxRadiusAtY = (y: number) => {
if (y > sidewallTopY) {
if (roofHeight <= 0 || y >= roofTipY) return 0;
return binRadius * (1 - (y - sidewallTopY) / roofHeight);
}
if (y >= sidewallBaseY) return binRadius; if (y >= sidewallBaseY) return binRadius;
if (hopperHeight <= 0 || y <= hopperTipY) return 0; if (hopperHeight <= 0 || y <= hopperTipY) return 0;
return binRadius * ((y - hopperTipY) / hopperHeight); return binRadius * ((y - hopperTipY) / hopperHeight);
@ -191,7 +198,7 @@ export default function MoistureHeatMap({ binDimensions, targetMoisture, nodes,
const getSurfaceY = (x: number, z: number): number => { const getSurfaceY = (x: number, z: number): number => {
if (surfaceAnchors.length > 0) { if (surfaceAnchors.length > 0) {
const raw = idwSurfaceY(x, z, surfaceAnchors, SURFACE_IDW_POWER); const raw = idwSurfaceY(x, z, surfaceAnchors, SURFACE_IDW_POWER);
return Math.max(grainFloorY, Math.min(sidewallHeight / 2, raw)); return Math.max(grainFloorY, Math.min(roofTipY, raw));
} }
return flatMaxY ?? sidewallBaseY; return flatMaxY ?? sidewallBaseY;
}; };
@ -415,6 +422,8 @@ export default function MoistureHeatMap({ binDimensions, targetMoisture, nodes,
binRadius, binRadius,
targetMoisture, targetMoisture,
hopperHeight, hopperHeight,
roofHeight,
roofTipY,
flatMaxY, flatMaxY,
flatMinY, flatMinY,
]); ]);

View file

@ -133,13 +133,23 @@ export default function TempHeatMap({ binDimensions, targetTemp, nodes, flatMaxY
const binRadius = binDimensions.diameter / 2; const binRadius = binDimensions.diameter / 2;
const sidewallHeight = binDimensions.sidewallHeight; const sidewallHeight = binDimensions.sidewallHeight;
const hopperHeight = binDimensions.hopperHeight ?? 0; const hopperHeight = binDimensions.hopperHeight ?? 0;
const roofHeight = binDimensions.roofHeight ?? 0;
const upperThreshold = targetTemp; const upperThreshold = targetTemp;
const sidewallBaseY = -sidewallHeight / 2; const sidewallBaseY = -sidewallHeight / 2;
const sidewallTopY = sidewallHeight / 2;
const hopperTipY = sidewallBaseY - hopperHeight; const hopperTipY = sidewallBaseY - hopperHeight;
const roofTipY = sidewallTopY + roofHeight;
const maxRadiusAtY = (y: number) => { const maxRadiusAtY = (y: number) => {
// Inside the roof cone — tapers from binRadius at sidewallTopY to 0 at roofTipY
if (y > sidewallTopY) {
if (roofHeight <= 0 || y >= roofTipY) return 0;
return binRadius * (1 - (y - sidewallTopY) / roofHeight);
}
// Straight sidewall
if (y >= sidewallBaseY) return binRadius; if (y >= sidewallBaseY) return binRadius;
// Inside the hopper cone — tapers from binRadius at sidewallBaseY to 0 at hopperTipY
if (hopperHeight <= 0 || y <= hopperTipY) return 0; if (hopperHeight <= 0 || y <= hopperTipY) return 0;
return binRadius * ((y - hopperTipY) / hopperHeight); return binRadius * ((y - hopperTipY) / hopperHeight);
}; };
@ -192,7 +202,7 @@ export default function TempHeatMap({ binDimensions, targetTemp, nodes, flatMaxY
const getSurfaceY = (x: number, z: number): number => { const getSurfaceY = (x: number, z: number): number => {
if (surfaceAnchors.length > 0) { if (surfaceAnchors.length > 0) {
const raw = idwSurfaceY(x, z, surfaceAnchors, SURFACE_IDW_POWER); const raw = idwSurfaceY(x, z, surfaceAnchors, SURFACE_IDW_POWER);
return Math.max(grainFloorY, Math.min(sidewallHeight / 2, raw)); return Math.max(grainFloorY, Math.min(roofTipY, raw));
} }
return flatMaxY ?? sidewallBaseY; return flatMaxY ?? sidewallBaseY;
}; };
@ -416,6 +426,8 @@ export default function TempHeatMap({ binDimensions, targetTemp, nodes, flatMaxY
binRadius, binRadius,
upperThreshold, upperThreshold,
hopperHeight, hopperHeight,
roofHeight,
roofTipY,
flatMaxY, flatMaxY,
flatMinY, flatMinY,
]); ]);
@ -456,4 +468,3 @@ export default function TempHeatMap({ binDimensions, targetTemp, nodes, flatMaxY
</React.Fragment> </React.Fragment>
); );
} }

View file

@ -1,3 +1,5 @@
import { ConeVolume, CylinderVolume } from "common/TrigFunctions";
/** /**
* Y bounds of filled grain volume matches GrainFillFlat / GrainCableFill fallback volume math. * Y bounds of filled grain volume matches GrainFillFlat / GrainCableFill fallback volume math.
*/ */
@ -6,28 +8,40 @@ export interface GrainYBounds {
maxY: number; maxY: number;
} }
// Grain never physically fills all the way to the roof tip — cap roof fill
// at this fraction of roofHeight so there is always visible breathing room.
const MAX_ROOF_FILL_FRACTION = 0.75;
export function grainYBoundsFromFillPercent( export function grainYBoundsFromFillPercent(
diameter: number, diameter: number,
sidewallHeight: number, sidewallHeight: number,
hopperHeight: number, hopperHeight: number,
roofHeight: number,
fillPercent: number fillPercent: number
): GrainYBounds { ): GrainYBounds {
const radius = diameter / 2; const radius = diameter / 2;
const sidewallBaseY = -sidewallHeight / 2; const sidewallBaseY = -sidewallHeight / 2;
const hopperTipY = sidewallBaseY - hopperHeight; const hopperTipY = sidewallBaseY - hopperHeight;
const roofBaseY = sidewallHeight / 2;
if (fillPercent <= 0) { if (fillPercent <= 0) {
return { minY: sidewallBaseY, maxY: sidewallBaseY }; return { minY: sidewallBaseY, maxY: sidewallBaseY };
} }
const cylinderVolume = Math.PI * radius * radius * sidewallHeight; const cylinderVolume = CylinderVolume(radius, sidewallHeight);
const hopperVolume = const hopperVolume = hopperHeight > 0 ? ConeVolume(radius, hopperHeight) : 0;
hopperHeight > 0 ? (1 / 3) * Math.PI * radius * radius * hopperHeight : 0; const roofVolume = roofHeight > 0 ? ConeVolume(radius, roofHeight) : 0;
const totalVolume = cylinderVolume + hopperVolume;
// Volume of the usable roof portion (up to MAX_ROOF_FILL_FRACTION of height).
// A cone fills as h³ so the capped volume fraction = MAX_ROOF_FILL_FRACTION³.
const roofVolumeCapped = roofVolume * Math.pow(MAX_ROOF_FILL_FRACTION, 3);
const totalVolume = cylinderVolume + hopperVolume + roofVolumeCapped;
const filledVolume = totalVolume * fillPercent; const filledVolume = totalVolume * fillPercent;
// Fill the hopper first
if (hopperHeight > 0 && filledVolume <= hopperVolume) { if (hopperHeight > 0 && filledVolume <= hopperVolume) {
const ratio = filledVolume / hopperVolume; const ratio = filledVolume / hopperVolume;
const hopperFillHeight = hopperHeight * Math.pow(ratio, 1 / 3); const hopperFillHeight = hopperHeight * Math.pow(ratio, 1 / 3);
return { return {
minY: hopperTipY, minY: hopperTipY,
@ -35,15 +49,23 @@ export function grainYBoundsFromFillPercent(
}; };
} }
const hopperFillHeight = hopperHeight; // Then fill the cylinder
const remainingVolume = filledVolume - hopperVolume; const volumeAfterHopper = filledVolume - hopperVolume;
const cylinderFillHeight = Math.max( if (volumeAfterHopper <= cylinderVolume) {
0, const cylinderFillHeight = volumeAfterHopper / (Math.PI * radius * radius);
remainingVolume / (Math.PI * radius * radius) return {
); minY: hopperHeight > 0 ? hopperTipY : sidewallBaseY,
maxY: sidewallBaseY + cylinderFillHeight,
};
}
// Finally fill into the roof cone, capped at MAX_ROOF_FILL_FRACTION
const roofFilledVolume = volumeAfterHopper - cylinderVolume;
const roofRatio = Math.min(1, roofFilledVolume / roofVolumeCapped);
const roofFillHeight = roofHeight * MAX_ROOF_FILL_FRACTION * Math.pow(roofRatio, 1 / 3);
return { return {
minY: hopperHeight > 0 ? hopperTipY : sidewallBaseY, minY: hopperHeight > 0 ? hopperTipY : sidewallBaseY,
maxY: sidewallBaseY + cylinderFillHeight, maxY: roofBaseY + roofFillHeight,
}; };
} }

View file

@ -513,8 +513,8 @@ const dateSelector = () => {
<Typography textAlign={"center"}>{currentCheckpointTime ? currentCheckpointTime.format("MMM D, YYYY h:mm A") : moment().format("MMM D, YYYY h:mm A")}</Typography> <Typography textAlign={"center"}>{currentCheckpointTime ? currentCheckpointTime.format("MMM D, YYYY h:mm A") : moment().format("MMM D, YYYY h:mm A")}</Typography>
<LinearProgress value={displayProgress} variant="determinate" /> <LinearProgress value={displayProgress} variant="determinate" />
<Box display="flex" flexDirection="row" justifyContent="space-between"> <Box display="flex" flexDirection="row" justifyContent="space-between">
<Typography>{startDate.format("MMM D, YYYY")}</Typography> <Typography variant="caption">{startDate.format("MMM D, YYYY")}</Typography>
<Typography>{endDate.format("MMM D, YYYY")}</Typography> <Typography variant="caption">{endDate.format("MMM D, YYYY")}</Typography>
</Box> </Box>
</Box> </Box>
{/* date range selector */} {/* date range selector */}

View file

@ -81,8 +81,8 @@ export default function BinTableExpanded(props: Props) {
const inGrain = nodeIndex < cable.topNode const inGrain = nodeIndex < cable.topNode
if (inGrain) { if (inGrain) {
if (raw > targetTemp + 5) color = warning if (raw > targetTemp + 10) color = warning
else if (raw > targetTemp) color = caution else if (raw > targetTemp + 5) color = caution
else color = inBounds else color = inBounds
} }
@ -102,7 +102,7 @@ export default function BinTableExpanded(props: Props) {
const inGrain = nodeIndex < cable.topNode const inGrain = nodeIndex < cable.topNode
if (inGrain) { if (inGrain) {
if (raw > targetMoisture + 1.5) color = warning if (raw > targetMoisture + 1.5) color = warning
else if (raw > targetMoisture) color = caution else if (raw > targetMoisture + 0.5) color = caution
else color = inBounds else color = inBounds
} }
return { display: raw.toFixed(2) + "%", color } return { display: raw.toFixed(2) + "%", color }
@ -196,15 +196,15 @@ export default function BinTableExpanded(props: Props) {
<Box display="flex" alignItems="center" justifyContent={"space-between"} marginY={2}> <Box display="flex" alignItems="center" justifyContent={"space-between"} marginY={2}>
<Box display="flex" alignItems="center" gap={1}> <Box display="flex" alignItems="center" gap={1}>
<Box sx={{ width: 40, height: 20, backgroundColor: inBounds, borderRadius: 1, flexShrink: 0 }} /> <Box sx={{ width: 40, height: 20, backgroundColor: inBounds, borderRadius: 1, flexShrink: 0 }} />
<Typography sx={{fontWeight: 600, fontSize: 10}}>On or below target</Typography> <Typography sx={{fontWeight: 600, fontSize: 10}}>On target</Typography>
</Box> </Box>
<Box display="flex" alignItems="center" gap={1}> <Box display="flex" alignItems="center" gap={1}>
<Box sx={{ width: 40, height: 20, backgroundColor: caution, borderRadius: 1, flexShrink: 0 }} /> <Box sx={{ width: 40, height: 20, backgroundColor: caution, borderRadius: 1, flexShrink: 0 }} />
<Typography sx={{fontWeight: 600, fontSize: 10}}>{user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "+10 °F" : "+5 °C"} above target</Typography> <Typography sx={{fontWeight: 600, fontSize: 10}}>{user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "~10 °F" : "+5 °C"} above</Typography>
</Box> </Box>
<Box display="flex" alignItems="center" gap={1}> <Box display="flex" alignItems="center" gap={1}>
<Box sx={{ width: 40, height: 20, backgroundColor: warning, borderRadius: 1, flexShrink: 0 }} /> <Box sx={{ width: 40, height: 20, backgroundColor: warning, borderRadius: 1, flexShrink: 0 }} />
<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</Typography>
</Box> </Box>
</Box> </Box>
<Box sx={{ display: "flex", flexDirection: "row", overflowX: "auto", gap: 2, paddingBottom: 2}}> <Box sx={{ display: "flex", flexDirection: "row", overflowX: "auto", gap: 2, paddingBottom: 2}}>

View file

@ -390,6 +390,7 @@ export default function BinSummary(props: Props){
<ChangeGrainDialog bin={bin} open={openGrainDialog} closeDialog={()=>setOpenGrainDialog(false)} permissions={permissions} updateBin={updateBinCallback}/> <ChangeGrainDialog bin={bin} open={openGrainDialog} closeDialog={()=>setOpenGrainDialog(false)} permissions={permissions} updateBin={updateBinCallback}/>
{loading ? <Skeleton variant="rectangular" height={100} sx={{marginBottom: 2}} /> : isMobile ? inventorySummaryMobile() : inventorySummaryDesktop()} {loading ? <Skeleton variant="rectangular" height={100} sx={{marginBottom: 2}} /> : isMobile ? inventorySummaryMobile() : inventorySummaryDesktop()}
<Grid2 container spacing={2}> <Grid2 container spacing={2}>
{!isMobile &&
<Grid2 size={isMobile ? 12 : 7}> <Grid2 size={isMobile ? 12 : 7}>
{loading ? {loading ?
<Skeleton variant="rectangular" height={600}/> <Skeleton variant="rectangular" height={600}/>
@ -406,8 +407,7 @@ export default function BinSummary(props: Props){
binPrefs={binPrefs} binPrefs={binPrefs}
updateBinCallback={updateBinCallback} updateBinCallback={updateBinCallback}
/> />
{/* only show the player on desktop */} {setBin &&
{setBin && !isMobile &&
<Box paddingX={2} position="absolute" bottom={10} width="100%"> <Box paddingX={2} position="absolute" bottom={10} width="100%">
<BinPlayer bin={bin} componentDevices={componentDevices} setBin={setBin}/> <BinPlayer bin={bin} componentDevices={componentDevices} setBin={setBin}/>
</Box> </Box>
@ -415,6 +415,7 @@ export default function BinSummary(props: Props){
</Card> </Card>
} }
</Grid2> </Grid2>
}
<Grid2 size={isMobile ? 12 : 5}> <Grid2 size={isMobile ? 12 : 5}>
{loading ? {loading ?
<Skeleton variant="rectangular" height={600}/> <Skeleton variant="rectangular" height={600}/>
@ -428,7 +429,12 @@ export default function BinSummary(props: Props){
linkedComponents={componentMap} linkedComponents={componentMap}
permissions={permissions} permissions={permissions}
cables={cables} cables={cables}
plenums={plenums}/> plenums={plenums}
fans={fans}
heaters={heaters}
binPrefs={binPrefs}
componentMap={componentMap}
setBin={setBin}/>
</Card> </Card>
} }
</Grid2> </Grid2>

View file

@ -11,6 +11,9 @@ import BinAnalysisGraphs from "./binAnalysisGraphs";
import { GrainCable } from "models/GrainCable"; import { GrainCable } from "models/GrainCable";
import { Plenum } from "models/Plenum"; import { Plenum } from "models/Plenum";
import { Controller } from "models/Controller"; import { Controller } from "models/Controller";
import { useMobile } from "hooks";
import Bin3dVisualizer from "bin/bin3dVisualizer";
import BinPlayer from "bin/BinPlayer";
interface Props { interface Props {
bin: Bin bin: Bin
@ -21,6 +24,12 @@ interface Props {
permissions: pond.Permission[] permissions: pond.Permission[]
cables: GrainCable[] cables: GrainCable[]
plenums: Plenum[] plenums: Plenum[]
showBinTab?: boolean
fans?: Controller[]
heaters?: Controller[]
componentMap: Map<string, Component>
binPrefs?: Map<string, pond.BinComponentPreferences>
setBin?: React.Dispatch<React.SetStateAction<Bin>>;
} }
interface TabPanelProps { interface TabPanelProps {
@ -51,8 +60,9 @@ function TabPanelMine(props: TabPanelProps) {
} }
export default function BinDetails (props: Props) { export default function BinDetails (props: Props) {
const {bin, updateBinCallback, linkedComponents, componentDevices, devices, permissions, cables, plenums} = props const {bin, updateBinCallback, linkedComponents, componentDevices, devices, permissions, cables, plenums, showBinTab, fans, heaters, componentMap, binPrefs, setBin} = props
const [currentTab, setCurrentTab] = useState(0) const isMobile = useMobile()
const [currentTab, setCurrentTab] = useState((isMobile || showBinTab) ? "bin" : "table")
//this is where we will have the tabs and tab panels for the table view, alerts, controls, and analysis //this is where we will have the tabs and tab panels for the table view, alerts, controls, and analysis
@ -65,22 +75,48 @@ export default function BinDetails (props: Props) {
textColor="primary" textColor="primary"
variant="fullWidth" variant="fullWidth"
sx={{ flexShrink: 0 }}> sx={{ flexShrink: 0 }}>
<Tab label={"Table View"} value={0} /> {isMobile && <Tab label={"Bin"} value={"bin"} />}
<Tab label={"Alerts"} value={1} /> <Tab label={"Table"} value={"table"} />
<Tab label={"Controls"} value={2} /> <Tab label={"Alerts"} value={"alerts"} />
<Tab label={"Analysis"} value={3} /> <Tab label={"Controls"} value={"controls"} />
<Tab label={"Analysis"} value={"analysis"} />
</Tabs> </Tabs>
<Box sx={{ display: "flex", flexDirection: "column", flex: 1, minHeight: 0, overflow: "hidden" }}> <Box sx={{ display: "flex", flexDirection: "column", flex: 1, minHeight: 0, overflow: "hidden" }}>
<TabPanelMine value={currentTab} index={0}> {(isMobile || showBinTab) &&
<TabPanelMine value={currentTab} index={"bin"}>
<Bin3dVisualizer
bin={bin}
fans={fans}
heaters={heaters}
permissions={permissions}
devices={devices}
componentDevices={componentDevices}
componentMap={componentMap}
binPrefs={binPrefs}
updateBinCallback={updateBinCallback}
/>
{setBin &&
<Box padding={1}>
<BinPlayer bin={bin} componentDevices={componentDevices} setBin={setBin}/>
</Box>
}
</TabPanelMine>
}
<TabPanelMine value={currentTab} index={"table"}>
<BinTableView bin={bin} updateBinCallback={updateBinCallback}/> <BinTableView bin={bin} updateBinCallback={updateBinCallback}/>
{setBin && isMobile &&
<Box padding={1}>
<BinPlayer bin={bin} componentDevices={componentDevices} setBin={setBin}/>
</Box>
}
</TabPanelMine> </TabPanelMine>
<TabPanelMine value={currentTab} index={1}> <TabPanelMine value={currentTab} index={"alerts"}>
<BinAlerts componentDevices={componentDevices} linkedComponents={linkedComponents} permissions={permissions}/> <BinAlerts componentDevices={componentDevices} linkedComponents={linkedComponents} permissions={permissions}/>
</TabPanelMine> </TabPanelMine>
<TabPanelMine value={currentTab} index={2}> <TabPanelMine value={currentTab} index={"controls"}>
<BinControls componentDevices={componentDevices} devices={devices} linkedComponents={linkedComponents} permissions={permissions}/> <BinControls componentDevices={componentDevices} devices={devices} linkedComponents={linkedComponents} permissions={permissions}/>
</TabPanelMine> </TabPanelMine>
<TabPanelMine value={currentTab} index={3}> <TabPanelMine value={currentTab} index={"analysis"}>
<BinAnalysisGraphs bin={bin} cables={cables} plenums={plenums} componentDevices={componentDevices}/> <BinAnalysisGraphs bin={bin} cables={cables} plenums={plenums} componentDevices={componentDevices}/>
</TabPanelMine> </TabPanelMine>
</Box> </Box>

View file

@ -23,7 +23,6 @@ const useStyles = makeStyles((theme: Theme) => ({
headerCellStyle: { headerCellStyle: {
fontWeight: 650, fontWeight: 650,
backgroundColor: darken(theme.palette.background.paper, 0.2), backgroundColor: darken(theme.palette.background.paper, 0.2),
fontSize: 15,
textAlign: "center", textAlign: "center",
borderTop: "1px solid black", borderTop: "1px solid black",
borderLeft: "1px solid black", borderLeft: "1px solid black",
@ -32,6 +31,18 @@ const useStyles = makeStyles((theme: Theme) => ({
borderRight: "1px solid black", borderRight: "1px solid black",
} }
}, },
mobileHeaderCellStyle: {
fontWeight: 650,
backgroundColor: darken(theme.palette.background.paper, 0.2),
textAlign: "center",
borderTop: "1px solid black",
borderLeft: "1px solid black",
borderBottom: "1px solid black",
"&:last-child": {
borderRight: "1px solid black",
},
padding: 2
},
cellStyle: { cellStyle: {
padding: 2, padding: 2,
fontWeight: 650, fontWeight: 650,
@ -164,9 +175,9 @@ export default function BinTableView(props: Props) {
if(level.grainTemps.length > 0){ if(level.grainTemps.length > 0){
lvlTemp = avg(level.grainTemps) lvlTemp = avg(level.grainTemps)
//if the average temp is 5 degrees above the bins threshold //if the average temp is 5 degrees above the bins threshold
if(lvlTemp > (bin.targetTemp() + 5)){ if(lvlTemp > (bin.targetTemp() + 10)){
tempColour = warning tempColour = warning
}else if(lvlTemp > bin.targetTemp()){ }else if(lvlTemp > bin.targetTemp() + 5){
//if the average temp is greater than 5 degrees over //if the average temp is greater than 5 degrees over
tempColour = caution tempColour = caution
}else { }else {
@ -179,7 +190,7 @@ export default function BinTableView(props: Props) {
if(user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT){ if(user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT){
lvlTemp = celsiusToFahrenheit(lvlTemp) lvlTemp = celsiusToFahrenheit(lvlTemp)
} }
tempDisplay = lvlTemp.toFixed(2) + (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? " °F" : " °C") tempDisplay = lvlTemp.toFixed(2)
} }
//determine the moisture to show //determine the moisture to show
@ -187,8 +198,7 @@ export default function BinTableView(props: Props) {
lvlMoisture = avg(level.grainMoistures) lvlMoisture = avg(level.grainMoistures)
if(lvlMoisture > (bin.targetMoisture() + 1.5)){ if(lvlMoisture > (bin.targetMoisture() + 1.5)){
moistureColour = warning moistureColour = warning
}else if(lvlMoisture > bin.targetMoisture()){ }else if(lvlMoisture > bin.targetMoisture() + 0.5){
//if the average temp is greater than 5 degrees over
moistureColour = caution moistureColour = caution
}else { }else {
moistureColour = inBounds moistureColour = inBounds
@ -222,11 +232,11 @@ export default function BinTableView(props: Props) {
} }
} }
const tempThreshDisplay = () => { const tempThreshDisplay = (hideUnit?: boolean) => {
if(user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT){ if(user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT){
return tempTarget.toFixed(2) + " °F" return tempTarget.toFixed(2) + (hideUnit ? "" : " °F")
} }
return tempTarget.toFixed(2) + " °C" return tempTarget.toFixed(2) + (hideUnit ? "" : " °C")
} }
const LegendSwatch = ({ color }: { color: string }) => ( const LegendSwatch = ({ color }: { color: string }) => (
@ -287,7 +297,7 @@ export default function BinTableView(props: Props) {
return ( return (
<Box padding={2} sx={{ display: "flex", flexDirection: "column", height: "100%" }}> <Box padding={2} sx={{ display: "flex", flexDirection: "column", height: isMobile ? "88%" : "100%" }}>
{expandedTableDialog()} {expandedTableDialog()}
{/* Level Table */} {/* Level Table */}
<Box display="flex" justifyContent="space-between" alignItems="center"> <Box display="flex" justifyContent="space-between" alignItems="center">
@ -311,80 +321,80 @@ export default function BinTableView(props: Props) {
<Box sx={{ overflowY: "auto", flex: 1, minHeight: 0 }}> <Box sx={{ overflowY: "auto", flex: 1, minHeight: 0 }}>
<Table className={classes.tableStyle}> <Table className={classes.tableStyle}>
<TableHead> <TableHead>
<TableRow ref={headerRow1Ref}> <TableRow ref={headerRow1Ref}>
<TableCell className={classes.headerCellStyle} sx={{ top: 0, position: "sticky", zIndex: 3 }}> {/* "Targets" cell — hide on mobile when either field is being edited */}
<Box justifyContent={"center"} sx={{ display: "flex", alignItems: "center", gap: 1 }}> {!(isMobile && (enterTemp || enterMoisture)) && (
<GppGood /> <TableCell className={isMobile ? classes.mobileHeaderCellStyle : classes.headerCellStyle} sx={{top: 0, position: "sticky", zIndex: 3 }}>
<Typography fontWeight={650}> <Box justifyContent={"center"} sx={{ display: "flex", alignItems: "center", gap: 1 }}>
Targets <GppGood />
</Typography> {!isMobile &&
</Box> <Typography fontSize={16} fontWeight={650}>
</TableCell> Targets
<TableCell className={classes.headerCellStyle} sx={{ top: 0, position: "sticky", zIndex: 3 }}> </Typography>
<Box justifyContent={"center"} sx={{ display: "flex", alignItems: "center", gap: 1 }}> }
<TemperatureIcon heightWidth={30}/> </Box>
{enterTemp ? </TableCell>
updateTempEntry() )}
:
<Typography fontWeight={650}> {/* Temperature cell */}
{tempThreshDisplay()} {!(isMobile && enterMoisture) && (
</Typography> <TableCell
} className={isMobile ? classes.mobileHeaderCellStyle : classes.headerCellStyle}
{enterTemp ? colSpan={isMobile && enterTemp ? 3 : 1}
<IconButton sx={{ top: 0, position: "sticky", zIndex: 3 }}
onClick={() => { >
setEnterTemp(false) <Box justifyContent={"center"} sx={{ display: "flex", alignItems: "center", gap: 1 }}>
//and update the bin with the new temp target <TemperatureIcon heightWidth={isMobile ? 25 : 30} />
update() {enterTemp ? updateTempEntry() : (
}} <Typography fontSize={isMobile ? 12 : 16} fontWeight={650}>
> {tempThreshDisplay(isMobile)}
<Save /> </Typography>
</IconButton> )}
: {enterTemp ? (
<IconButton <IconButton onClick={() => { setEnterTemp(false); update(); }}>
onClick={() => { <Save />
setEnterTemp(true) </IconButton>
}}> ) : (
<Edit /> <IconButton onClick={() => setEnterTemp(true)}>
</IconButton> <Edit />
} </IconButton>
</Box> )}
</TableCell> </Box>
<TableCell className={classes.headerCellStyle} sx={{ top: 0, position: "sticky", zIndex: 3 }}> </TableCell>
<Box justifyContent={"center"} sx={{ display: "flex", alignItems: "center", gap: 1 }}> )}
<HumidityIcon height={30} width={20}/> {/* Moisture cell — hide on mobile when temperature is being edited */}
{enterMoisture ? {!(isMobile && enterTemp) && (
updateMoistureEntry() <TableCell
: className={isMobile ? classes.mobileHeaderCellStyle : classes.headerCellStyle}
<Typography fontWeight={650} sx={{marginLeft: "10px"}}> colSpan={isMobile && enterMoisture ? 3 : 1}
{bin.targetMoisture()}% sx={{ top: 0, position: "sticky", zIndex: 3 }}
</Typography> >
} <Box justifyContent={"center"} sx={{ display: "flex", alignItems: "center", gap: 1 }}>
{enterMoisture ? <HumidityIcon height={isMobile ? 25 : 30} width={20} />
<IconButton {enterMoisture ? updateMoistureEntry() : (
onClick={() => { <Typography fontSize={isMobile ? 12 : 16} fontWeight={650} sx={{ marginLeft: "10px" }}>
setEnterMoisture(false) {bin.targetMoisture()}%
//and update the bin with the new temp target </Typography>
update() )}
}} {enterMoisture ? (
> <IconButton onClick={() => { setEnterMoisture(false); update(); }}>
<Save /> <Save />
</IconButton> </IconButton>
: ) : (
<IconButton <IconButton onClick={() => setEnterMoisture(true)}>
onClick={() => { <Edit />
setEnterMoisture(true) </IconButton>
}}> )}
<Edit /> </Box>
</IconButton> </TableCell>
} )}
</Box> </TableRow>
</TableCell>
</TableRow>
<TableRow> <TableRow>
<TableCell className={classes.headerCellStyle} sx={{ top: firstRowHeight, position: "sticky", zIndex: 3 }}>Level</TableCell> <TableCell className={isMobile ? classes.mobileHeaderCellStyle : classes.headerCellStyle} sx={{ top: firstRowHeight, position: "sticky", zIndex: 3 }}>Level</TableCell>
<TableCell className={classes.headerCellStyle} sx={{ top: firstRowHeight, position: "sticky", zIndex: 3 }}>Temperature</TableCell> <TableCell className={isMobile ? classes.mobileHeaderCellStyle : classes.headerCellStyle} sx={{ top: firstRowHeight, position: "sticky", zIndex: 3 }}>
<TableCell className={classes.headerCellStyle} sx={{ top: firstRowHeight, position: "sticky", zIndex: 3 }}>Moisture</TableCell> {isMobile ? "T" : "Temperature"}{(user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? " (°F)" : " (°C)")}
</TableCell>
<TableCell className={isMobile ? classes.mobileHeaderCellStyle : classes.headerCellStyle} sx={{ top: firstRowHeight, position: "sticky", zIndex: 3 }}>EMC (%)</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
@ -402,18 +412,17 @@ export default function BinTableView(props: Props) {
<Box display="flex" alignItems="center" justifyContent="space-between" marginTop={2} sx={{ flexShrink: 0 }}> <Box display="flex" alignItems="center" justifyContent="space-between" marginTop={2} sx={{ flexShrink: 0 }}>
<Box display="flex" alignItems="center" gap={1}> <Box display="flex" alignItems="center" gap={1}>
<LegendSwatch color={inBounds} /> <LegendSwatch color={inBounds} />
<Typography sx={{fontWeight: 600, fontSize: 10}}>On or below target</Typography> <Typography sx={{fontWeight: 600, fontSize: 10}}>On target</Typography>
</Box> </Box>
<Box display="flex" alignItems="center" gap={1}> <Box display="flex" alignItems="center" gap={1}>
<LegendSwatch color={caution} /> <LegendSwatch color={caution} />
<Typography sx={{fontWeight: 600, fontSize: 10}}>{user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "+10 °F" : "+5 °C"} above target</Typography> <Typography sx={{fontWeight: 600, fontSize: 10}}>{user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "~10 °F" : "+5 °C"} (0.5%) above</Typography>
</Box> </Box>
<Box display="flex" alignItems="center" gap={1}> <Box display="flex" alignItems="center" gap={1}>
<LegendSwatch color={warning} /> <LegendSwatch color={warning} />
<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"} (1.5%) above</Typography>
</Box> </Box>
</Box> </Box>
</Box> </Box>
) )
} }