finished work on the table, made the legend, and hooked up the edits for the temp/moisture targets

This commit is contained in:
csawatzky 2026-05-15 15:49:53 -06:00
parent 0f4d706e76
commit a65dbe8820
7 changed files with 282 additions and 35 deletions

View file

@ -43,7 +43,7 @@ export default function GrainCable(props: Props) {
<React.Fragment>
<Cylinder geometry={geometry} position={calculateCenter()} opacity={0.99} renderOrder={renderOrder} depthWrite={false} depthTest={false}/>
{nodes.map((node, i) => (
<CableNode displayTemp={displayTemp} displayMoisture={displayMoisture} onNodeClick={onNodeClick} key={"node " + i} renderOrder={renderOrder} node={node} targetMoisture={bin.targetMoisture()} upperThreshold={bin.upperTempThreshold()}/>
<CableNode displayTemp={displayTemp} displayMoisture={displayMoisture} onNodeClick={onNodeClick} key={"node " + i} renderOrder={renderOrder} node={node} targetMoisture={bin.targetMoisture()} upperThreshold={bin.targetTemp()}/>
))}
</React.Fragment>
)

View file

@ -73,7 +73,7 @@ export default function NodePointCloud(props: Props) {
.filter(n => n.inGrain && !n.excluded)
.map(n => {
const lower = bin.lowerTempThreshold();
const upper = bin.upperTempThreshold();
const upper = bin.targetTemp();
if (n.celcius >= lower && n.celcius <= upper) return null;

View file

@ -129,7 +129,7 @@ export default function TempHeatMap({ bin, nodes, flatMaxY }: Props) {
const binRadius = bin.diameter() / 2;
const sidewallHeight = bin.sidewallHeight();
const hopperHeight = bin.hopperHeight() ?? 0;
const upperThreshold = bin.upperTempThreshold();
const upperThreshold = bin.targetTemp();
const sidewallBaseY = -sidewallHeight / 2;
const hopperTipY = sidewallBaseY - hopperHeight;

View file

@ -11,6 +11,7 @@ import { useEffect, useState } from "react";
import { Controller } from "models/Controller";
import { GrainCable } from "models/GrainCable";
import { pond } from "protobuf-ts/pond";
import BinDetails from "./components/binDetails";
interface Props {
bin: Bin
@ -156,7 +157,7 @@ export default function BinSummary(props: Props){
</Card>
<Grid2 container spacing={2}>
<Grid2 size={isMobile ? 12 : 7}>
<Card raised>
<Card raised sx={{height: 600}}>
<Bin3dVisualizer
bin={bin}
fans={fans}
@ -171,8 +172,8 @@ export default function BinSummary(props: Props){
</Card>
</Grid2>
<Grid2 size={isMobile ? 12 : 5}>
<Card raised sx={{height: "100%"}}>
<BinTableView bin={bin}/>
<Card raised sx={{height: 600}}>
<BinDetails bin={bin} updateBinCallback={updateBinCallback}/>
</Card>
</Grid2>
<Grid2 size={12}>

View file

@ -1,17 +1,65 @@
import { Box } from "@mui/material";
import { Box, Tab, Tabs } from "@mui/material";
import React from "react";
import { useState } from "react";
import BinTableView from "./binTableView";
import { Bin } from "models";
interface Props {
bin: Bin
updateBinCallback?: (bin: Bin) => void
}
export default function BinDetails (props: Props) {
interface TabPanelProps {
children?: React.ReactNode;
index: any;
value: any;
}
function TabPanelMine(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
aria-labelledby={`simple-tab-${index}`}
{...other}>
{value === index && <React.Fragment>{children}</React.Fragment>}
</div>
);
}
export default function BinDetails (props: Props) {
const {bin, updateBinCallback} = props
const [currentTab, setCurrentTab] = useState(0)
//this is where we will have the tabs abd tab panels for the table view, alerts, interactions, and analysis
return (
<Box>
<Tabs
value={currentTab}
onChange={(_, value) => setCurrentTab(value)}
indicatorColor="primary"
textColor="primary"
variant="fullWidth">
<Tab label={"Table View"} value={0} />
<Tab label={"Alerts"} value={1} />
<Tab label={"Interactions"} value={2} />
<Tab label={"Analysis"} value={3} />
</Tabs>
<TabPanelMine value={currentTab} index={0}>
<BinTableView bin={bin} updateBinCallback={updateBinCallback}/>
</TabPanelMine>
<TabPanelMine value={currentTab} index={1}>
</TabPanelMine>
<TabPanelMine value={currentTab} index={2}>
</TabPanelMine>
<TabPanelMine value={currentTab} index={3}>
</TabPanelMine>
</Box>
)
}

View file

@ -1,14 +1,47 @@
import { Box, Table, TableBody, TableCell, TableHead, TableRow } from "@mui/material";
import { green, grey, orange, red } from "@mui/material/colors";
import { Edit, GppGood, Save } from "@mui/icons-material";
import { Box, darken, IconButton, Table, TableBody, TableCell, TableHead, TableRow, TextField, Theme, Typography } from "@mui/material";
import { green, grey, orange, red, yellow } from "@mui/material/colors";
import { makeStyles } from "@mui/styles";
import HumidityIcon from "component/HumidityIcon";
import TemperatureIcon from "component/TemperatureIcon";
import { cloneDeep } from "lodash";
import { Bin } from "models";
import { pond } from "protobuf-ts/pond";
import { useGlobalState } from "providers";
import React from "react";
import React, { useEffect, useState } from "react";
import { useMemo } from "react";
import { avg, celsiusToFahrenheit } from "utils";
const useStyles = makeStyles((theme: Theme) => ({
tableStyle: {
borderCollapse: "collapse",
},
headerCellStyle: {
fontWeight: 650,
backgroundColor: darken(theme.palette.background.paper, 0.2),
fontSize: 15,
textAlign: "center",
border: "1px solid black"
},
cellStyle: {
padding: 2,
fontWeight: 650,
fontSize: 17,
textAlign: "center",
border: "1px solid black",
},
thresholdCell: {
fontWeight: 650,
fontSize: 15,
textAlign: "center",
}
})
);
interface Props {
bin: Bin
updateBinCallback?: (bin: Bin) => void
}
interface BinLevel {
@ -20,8 +53,29 @@ interface BinLevel {
export default function BinTableView(props: Props) {
const {bin} = props
const classes = useStyles()
const {bin, updateBinCallback} = props
const [{user}] = useGlobalState()
const [enterTemp, setEnterTemp] = useState(false)
const [enterMoisture, setEnterMoisture] = useState(false)
const [tempTarget, setTempTarget] = useState(0)
const [tempEntry, setTempEntry] = useState("")
const [moistureEntry, setMoistureEntry] = useState("")
const [moistureTarget, setMoistureTarget] = useState(0)
const inBounds = green[600]
const caution = yellow[800]
const warning = red[700]
useEffect(()=>{
let val = bin.targetTemp()
if(user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT){
val = bin.targetTemp() * (9/5) + 32
}
setTempTarget(val)
setTempEntry(val.toFixed(2))
setMoistureTarget(bin.targetMoisture())
setMoistureEntry(bin.targetMoisture().toFixed(2))
},[bin])
const binLevels = useMemo(() => {
const cables = bin.status.grainCables
@ -89,21 +143,20 @@ export default function BinTableView(props: Props) {
let moistureDisplay = "--"
let lvlTemp
let lvlMoisture
let tempColour: string = grey[600]
let moistureColour: string = grey[600]
let tempColour: string = ""
let moistureColour: string = ""
//determine the temp
if(level.grainTemps.length > 0){
console.log(level.grainTemps)
lvlTemp = avg(level.grainTemps)
//if the average temp is 5 degrees above the bins threshold
if(lvlTemp > (bin.upperTempThreshold() + 5)){
tempColour = red[700]
}else if(lvlTemp > bin.upperTempThreshold()){
if(lvlTemp > (bin.targetTemp() + 5)){
tempColour = warning
}else if(lvlTemp > bin.targetTemp()){
//if the average temp is greater than 5 degrees over
tempColour = orange[600]
tempColour = caution
}else {
tempColour = green[300]
tempColour = inBounds
}
}else if (level.airTemps.length > 0){
lvlTemp = avg(level.airTemps)
@ -112,19 +165,19 @@ 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) + (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? " °F" : " °C")
}
//determine the moisture to show
if(level.grainMoistures.length > 0){
lvlMoisture = avg(level.grainMoistures)
if(lvlMoisture > (bin.targetMoisture() + 1.5)){
moistureColour = red[700]
moistureColour = warning
}else if(lvlMoisture > bin.targetMoisture()){
//if the average temp is greater than 5 degrees over
moistureColour = orange[600]
moistureColour = caution
}else {
moistureColour = green[300]
moistureColour = inBounds
}
}else if (level.airMoistures.length > 0){
lvlMoisture = avg(level.airMoistures)
@ -134,33 +187,177 @@ export default function BinTableView(props: Props) {
}
return (
<React.Fragment>
<TableCell sx={{color: "black", fontWeight: 650, fontSize: 17, textAlign: "center", backgroundColor: tempColour}}>{tempDisplay}</TableCell>
<TableCell sx={{color: "black", fontWeight: 650, fontSize: 17, textAlign: "center", backgroundColor: moistureColour}}>{moistureDisplay}</TableCell>
<TableCell padding="none" className={classes.cellStyle} sx={{backgroundColor: tempColour}}>{tempDisplay}</TableCell>
<TableCell padding="none" className={classes.cellStyle} sx={{backgroundColor: moistureColour}}>{moistureDisplay}</TableCell>
</React.Fragment>
)
}
const update = () => {
let temp = tempTarget
if(user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT){
//convert to c
temp = (temp - 32) * (5 / 9);
}
let clone = cloneDeep(bin)
if(clone.settings.inventory && updateBinCallback){
clone.settings.inventory.targetTemperature = temp
clone.settings.inventory.targetMoisture = moistureTarget
updateBinCallback(clone)
}
}
const tempThreshDisplay = () => {
if(user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT){
return tempTarget.toFixed(2) + " °F"
}
return tempTarget.toFixed(2) + " °C"
}
const LegendSwatch = ({ color }: { color: string }) => (
<Box sx={{ width: 40, height: 20, backgroundColor: color, borderRadius: 1, flexShrink: 0 }} />
)
const updateTempEntry = () => {
return (
<TextField
value={tempEntry}
onChange={e => {
let val = parseFloat(e.target.value)
if(!isNaN(val)){
setTempTarget(val)
}
setTempEntry(e.target.value)
}}
/>
)
}
const updateMoistureEntry = () => {
return (
<TextField
value={moistureEntry}
onChange={e => {
let val = parseFloat(e.target.value)
if(!isNaN(val)){
setMoistureTarget(val)
}
setMoistureEntry(e.target.value)
}}
/>
)
}
return (
<Box padding={2}>
<Table>
{/* Level Table */}
<Table className={classes.tableStyle}>
<TableHead>
<TableRow>
<TableCell sx={{ fontWeight: 650, fontSize: 20, textAlign: "center"}}>Level</TableCell>
<TableCell sx={{ fontWeight: 650, fontSize: 20, textAlign: "center"}}>Temperature</TableCell>
<TableCell sx={{ fontWeight: 650, fontSize: 20, textAlign: "center"}}>Moisture</TableCell>
<TableCell className={classes.thresholdCell}>
<Box justifyContent={"center"} sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<GppGood />
<Typography fontWeight={650}>
Targets
</Typography>
</Box>
</TableCell>
<TableCell className={classes.thresholdCell}>
<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.thresholdCell}>
<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>
<TableCell className={classes.headerCellStyle}>Level</TableCell>
<TableCell className={classes.headerCellStyle}>Temperature</TableCell>
<TableCell className={classes.headerCellStyle}>Moisture</TableCell>
</TableRow>
</TableHead>
<TableBody>
{binLevels.map((level, i) => (
<TableRow>
<TableCell sx={{color: "gray", fontWeight: 650, fontSize: 17, textAlign: "center"}}>{binLevels.length - i}</TableCell>
<TableCell padding="none" className={classes.cellStyle}>{binLevels.length - i}</TableCell>
{levelDisplay(level)}
</TableRow>
))}
</TableBody>
</Table>
{/* Table Legend */}
<Box display="flex" alignItems="center" justifyContent={"space-between"} marginTop={2}>
<Box display="flex" alignItems="center" gap={1}>
<LegendSwatch color={inBounds} />
<Typography sx={{fontWeight: 600, fontSize: 10}}>On or below 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>
</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>
</Box>
</Box>
</Box>
)
}