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 => {
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 = dims.sidewallHeight/2 //this will start the cables at the top of the sidewall
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 bottomY = getBottomY(orbit.radius, dims);
if (orbit.orbit === 0 && count > 0) {

View file

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

View file

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

View file

@ -133,13 +133,23 @@ export default function TempHeatMap({ binDimensions, targetTemp, nodes, flatMaxY
const binRadius = binDimensions.diameter / 2;
const sidewallHeight = binDimensions.sidewallHeight;
const hopperHeight = binDimensions.hopperHeight ?? 0;
const roofHeight = binDimensions.roofHeight ?? 0;
const upperThreshold = targetTemp;
const sidewallBaseY = -sidewallHeight / 2;
const sidewallTopY = sidewallHeight / 2;
const hopperTipY = sidewallBaseY - hopperHeight;
const roofTipY = sidewallTopY + roofHeight;
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;
// Inside the hopper cone — tapers from binRadius at sidewallBaseY to 0 at hopperTipY
if (hopperHeight <= 0 || y <= hopperTipY) return 0;
return binRadius * ((y - hopperTipY) / hopperHeight);
};
@ -192,7 +202,7 @@ export default function TempHeatMap({ binDimensions, targetTemp, nodes, flatMaxY
const getSurfaceY = (x: number, z: number): number => {
if (surfaceAnchors.length > 0) {
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;
};
@ -416,6 +426,8 @@ export default function TempHeatMap({ binDimensions, targetTemp, nodes, flatMaxY
binRadius,
upperThreshold,
hopperHeight,
roofHeight,
roofTipY,
flatMaxY,
flatMinY,
]);
@ -455,5 +467,4 @@ export default function TempHeatMap({ binDimensions, targetTemp, nodes, flatMaxY
</mesh>
</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.
*/
@ -6,28 +8,40 @@ export interface GrainYBounds {
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(
diameter: number,
sidewallHeight: number,
hopperHeight: number,
roofHeight: number,
fillPercent: number
): GrainYBounds {
const radius = diameter / 2;
const radius = diameter / 2;
const sidewallBaseY = -sidewallHeight / 2;
const hopperTipY = sidewallBaseY - hopperHeight;
const hopperTipY = sidewallBaseY - hopperHeight;
const roofBaseY = sidewallHeight / 2;
if (fillPercent <= 0) {
return { minY: sidewallBaseY, maxY: sidewallBaseY };
}
const cylinderVolume = Math.PI * radius * radius * sidewallHeight;
const hopperVolume =
hopperHeight > 0 ? (1 / 3) * Math.PI * radius * radius * hopperHeight : 0;
const totalVolume = cylinderVolume + hopperVolume;
const cylinderVolume = CylinderVolume(radius, sidewallHeight);
const hopperVolume = hopperHeight > 0 ? ConeVolume(radius, hopperHeight) : 0;
const roofVolume = roofHeight > 0 ? ConeVolume(radius, roofHeight) : 0;
// 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;
// Fill the hopper first
if (hopperHeight > 0 && filledVolume <= hopperVolume) {
const ratio = filledVolume / hopperVolume;
const ratio = filledVolume / hopperVolume;
const hopperFillHeight = hopperHeight * Math.pow(ratio, 1 / 3);
return {
minY: hopperTipY,
@ -35,15 +49,23 @@ export function grainYBoundsFromFillPercent(
};
}
const hopperFillHeight = hopperHeight;
const remainingVolume = filledVolume - hopperVolume;
const cylinderFillHeight = Math.max(
0,
remainingVolume / (Math.PI * radius * radius)
);
// Then fill the cylinder
const volumeAfterHopper = filledVolume - hopperVolume;
if (volumeAfterHopper <= cylinderVolume) {
const cylinderFillHeight = volumeAfterHopper / (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 {
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>
<LinearProgress value={displayProgress} variant="determinate" />
<Box display="flex" flexDirection="row" justifyContent="space-between">
<Typography>{startDate.format("MMM D, YYYY")}</Typography>
<Typography>{endDate.format("MMM D, YYYY")}</Typography>
<Typography variant="caption">{startDate.format("MMM D, YYYY")}</Typography>
<Typography variant="caption">{endDate.format("MMM D, YYYY")}</Typography>
</Box>
</Box>
{/* date range selector */}

View file

@ -81,8 +81,8 @@ export default function BinTableExpanded(props: Props) {
const inGrain = nodeIndex < cable.topNode
if (inGrain) {
if (raw > targetTemp + 5) color = warning
else if (raw > targetTemp) color = caution
if (raw > targetTemp + 10) color = warning
else if (raw > targetTemp + 5) color = caution
else color = inBounds
}
@ -102,7 +102,7 @@ export default function BinTableExpanded(props: Props) {
const inGrain = nodeIndex < cable.topNode
if (inGrain) {
if (raw > targetMoisture + 1.5) color = warning
else if (raw > targetMoisture) color = caution
else if (raw > targetMoisture + 0.5) color = caution
else color = inBounds
}
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" gap={1}>
<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 display="flex" alignItems="center" gap={1}>
<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 display="flex" alignItems="center" gap={1}>
<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 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}/>
{loading ? <Skeleton variant="rectangular" height={100} sx={{marginBottom: 2}} /> : isMobile ? inventorySummaryMobile() : inventorySummaryDesktop()}
<Grid2 container spacing={2}>
{!isMobile &&
<Grid2 size={isMobile ? 12 : 7}>
{loading ?
<Skeleton variant="rectangular" height={600}/>
@ -406,8 +407,7 @@ export default function BinSummary(props: Props){
binPrefs={binPrefs}
updateBinCallback={updateBinCallback}
/>
{/* only show the player on desktop */}
{setBin && !isMobile &&
{setBin &&
<Box paddingX={2} position="absolute" bottom={10} width="100%">
<BinPlayer bin={bin} componentDevices={componentDevices} setBin={setBin}/>
</Box>
@ -415,6 +415,7 @@ export default function BinSummary(props: Props){
</Card>
}
</Grid2>
}
<Grid2 size={isMobile ? 12 : 5}>
{loading ?
<Skeleton variant="rectangular" height={600}/>
@ -428,7 +429,12 @@ export default function BinSummary(props: Props){
linkedComponents={componentMap}
permissions={permissions}
cables={cables}
plenums={plenums}/>
plenums={plenums}
fans={fans}
heaters={heaters}
binPrefs={binPrefs}
componentMap={componentMap}
setBin={setBin}/>
</Card>
}
</Grid2>

View file

@ -11,6 +11,9 @@ import BinAnalysisGraphs from "./binAnalysisGraphs";
import { GrainCable } from "models/GrainCable";
import { Plenum } from "models/Plenum";
import { Controller } from "models/Controller";
import { useMobile } from "hooks";
import Bin3dVisualizer from "bin/bin3dVisualizer";
import BinPlayer from "bin/BinPlayer";
interface Props {
bin: Bin
@ -21,6 +24,12 @@ interface Props {
permissions: pond.Permission[]
cables: GrainCable[]
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 {
@ -51,8 +60,9 @@ function TabPanelMine(props: TabPanelProps) {
}
export default function BinDetails (props: Props) {
const {bin, updateBinCallback, linkedComponents, componentDevices, devices, permissions, cables, plenums} = props
const [currentTab, setCurrentTab] = useState(0)
const {bin, updateBinCallback, linkedComponents, componentDevices, devices, permissions, cables, plenums, showBinTab, fans, heaters, componentMap, binPrefs, setBin} = props
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
@ -65,22 +75,48 @@ export default function BinDetails (props: Props) {
textColor="primary"
variant="fullWidth"
sx={{ flexShrink: 0 }}>
<Tab label={"Table View"} value={0} />
<Tab label={"Alerts"} value={1} />
<Tab label={"Controls"} value={2} />
<Tab label={"Analysis"} value={3} />
{isMobile && <Tab label={"Bin"} value={"bin"} />}
<Tab label={"Table"} value={"table"} />
<Tab label={"Alerts"} value={"alerts"} />
<Tab label={"Controls"} value={"controls"} />
<Tab label={"Analysis"} value={"analysis"} />
</Tabs>
<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}/>
{setBin && isMobile &&
<Box padding={1}>
<BinPlayer bin={bin} componentDevices={componentDevices} setBin={setBin}/>
</Box>
}
</TabPanelMine>
<TabPanelMine value={currentTab} index={1}>
<TabPanelMine value={currentTab} index={"alerts"}>
<BinAlerts componentDevices={componentDevices} linkedComponents={linkedComponents} permissions={permissions}/>
</TabPanelMine>
<TabPanelMine value={currentTab} index={2}>
<TabPanelMine value={currentTab} index={"controls"}>
<BinControls componentDevices={componentDevices} devices={devices} linkedComponents={linkedComponents} permissions={permissions}/>
</TabPanelMine>
<TabPanelMine value={currentTab} index={3}>
<TabPanelMine value={currentTab} index={"analysis"}>
<BinAnalysisGraphs bin={bin} cables={cables} plenums={plenums} componentDevices={componentDevices}/>
</TabPanelMine>
</Box>

View file

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