import { Edit, GppGood, Info, Save } from "@mui/icons-material"; import { Box, Button, darken, DialogActions, IconButton, Table, TableBody, TableCell, TableHead, TableRow, TextField, Theme, Tooltip, Typography, useTheme } from "@mui/material"; import { green, grey, orange, red, yellow } from "@mui/material/colors"; import { makeStyles } from "@mui/styles"; import ResponsiveDialog from "common/ResponsiveDialog"; 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, { useEffect, useRef, useState } from "react"; import { useMemo } from "react"; import { avg, celsiusToFahrenheit } from "utils"; import BinTableExpanded from "./BinTableExpanded"; import { useMobile } from "hooks"; const useStyles = makeStyles((theme: Theme) => ({ tableStyle: { borderCollapse: "separate", borderSpacing: 0, // removes the default gap that "separate" introduces }, headerCellStyle: { fontWeight: 650, backgroundColor: darken(theme.palette.background.paper, 0.3), textAlign: "center", padding: 10 }, mobileHeaderCellStyle: { fontWeight: 650, backgroundColor: darken(theme.palette.background.paper, 0.3), textAlign: "center", padding: 2 }, targetCellStyle: { textAlign: "center", backgroundColor: theme.palette.background.paper }, mobileTargetCellStyle: { textAlign: "center", backgroundColor: theme.palette.background.paper, padding: 2 }, defaultCellStyle: { padding: 2, fontSize: 15, textAlign: "center", backgroundColor: darken(theme.palette.background.paper, 0.1), borderTop: "1px solid " + darken(theme.palette.background.paper, 0.4), borderBottom: "none" }, colouredCellStyle: { padding: 2, borderTop: "1px solid " + darken(theme.palette.background.paper, 0.4), // fontWeight: 650, fontSize: 15, textAlign: "center", borderBottom: "none" } }) ); interface Props { bin: Bin updateBinCallback?: (bin: Bin) => void } interface BinLevel { grainTemps: number[] airTemps: number[] grainMoistures: number[] airMoistures: number[] } export default function BinTableView(props: Props) { const classes = useStyles() const {bin, updateBinCallback} = props const [{user}] = useGlobalState() const theme = useTheme() 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 [openExpandedCables, setOpenExpandedCables] = useState(false) const isMobile = useMobile() const inBounds = green[600] const caution = yellow[800] const warning = red[700] const headerRow1Ref = useRef(null) const [firstRowHeight, setFirstRowHeight] = useState(0) useEffect(() => { if (headerRow1Ref.current) { setFirstRowHeight(headerRow1Ref.current.getBoundingClientRect().height) } }, [enterTemp, enterMoisture]) // re-measure when the row height might change due to TextField appearing 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 let levels: BinLevel[] = [] //note for future self, the first item in the measurement arrays is the lowest node on the cable //the zones we are creating here will match with the node number because most cables will either have the same number of nodes or only have a difference of 1 or 2 //example: cable 1 has 5 nodes cable 2 has 7 nodes, the first five nodes of the cables will match up and average together for each zone //and then zones 6 and 7 will be made using only cable 2 because cable 1 doesn't have them cables.forEach(cable => { cable.celcius.forEach((c, index) => { //remember index will be 0 based but the top node starts at 1 for the lowest cable let inGrain = index < cable.topNode if (levels[index]){ if(cable.excludedNodes.includes(index+1)) return if(inGrain){ levels[index].grainTemps.push(c) //need to check that the cable not only has the moisture mutation on it, but also that it reads humidity //cables that dont read humidity return an array of 0 values so if the average of all of the values is 0 then it is a temp only cable if(cable.moisture.length > 0 && avg(cable.moisture) > 0){ //based on all the other checks and the fact that the indexes between the array should match this should never be 0 but put a guard here just in case //also note that we are only doing moisture and not humidity, it seems like users dont care about humidity levels[index].grainMoistures.push(cable.moisture[index] ?? 0) } }else{ levels[index].airTemps.push(c) //need to check that the cable not only has the moisture mutation on it, but also that it reads humidity //cables that dont read humidity return an array of 0 values so if the average of all of the values is 0 then it is a temp only cable if(cable.moisture.length > 0 && avg(cable.moisture) > 0){ //based on all the other checks and the fact that the indexes between the array should match this should never be 0 but put a guard here just in case levels[index].airMoistures.push(cable.moisture[index] ?? 0) } } }else{ let newlevel: BinLevel = { grainTemps: [], grainMoistures: [], airTemps: [], airMoistures: [] } if(!cable.excludedNodes.includes(index)){ if(inGrain){ newlevel.grainTemps.push(c) if(cable.moisture.length > 0 && avg(cable.moisture) > 0){ newlevel.grainMoistures.push(cable.moisture[index] ?? 0) } }else{ newlevel.airTemps.push(c) if(cable.moisture.length > 0 && avg(cable.moisture) > 0){ newlevel.airMoistures.push(cable.moisture[index] ?? 0) } } } levels.push(newlevel) } }) }) //flip the levels so that the highest nodes are on the top levels.reverse() return levels },[bin]) const levelDisplay = (level: BinLevel, lastRow?: boolean) => { let tempDisplay = "--" let moistureDisplay = "--" let lvlTemp let lvlMoisture let tempColour: string = darken(theme.palette.background.paper, 0.1) let moistureColour: string = darken(theme.palette.background.paper, 0.1) //determine the temp if(level.grainTemps.length > 0){ lvlTemp = avg(level.grainTemps) //if the average temp is 5 degrees above the bins threshold if(lvlTemp > (bin.targetTemp() + 10)){ tempColour = warning }else if(lvlTemp > bin.targetTemp() + 5){ //if the average temp is greater than 5 degrees over tempColour = caution }else { tempColour = inBounds } }else if (level.airTemps.length > 0){ lvlTemp = avg(level.airTemps) } if (lvlTemp){ if(user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT){ lvlTemp = celsiusToFahrenheit(lvlTemp) } tempDisplay = lvlTemp.toFixed(2) } //determine the moisture to show if(level.grainMoistures.length > 0){ lvlMoisture = avg(level.grainMoistures) if(lvlMoisture > (bin.targetMoisture() + 1.5)){ moistureColour = warning }else if(lvlMoisture > bin.targetMoisture() + 0.5){ moistureColour = caution }else { moistureColour = inBounds } }else if (level.airMoistures.length > 0){ lvlMoisture = avg(level.airMoistures) } if(lvlMoisture){ moistureDisplay = lvlMoisture.toFixed(2) } return ( {tempDisplay} {moistureDisplay} ) } 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 = (hideUnit?: boolean) => { if(user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT){ return tempTarget.toFixed(2) + (hideUnit ? "" : " °F") } return tempTarget.toFixed(2) + (hideUnit ? "" : " °C") } const LegendSwatch = ({ color }: { color: string }) => ( ) const updateTempEntry = () => { return ( { let val = parseFloat(e.target.value) if(!isNaN(val)){ setTempTarget(val) } setTempEntry(e.target.value) }} /> ) } const updateMoistureEntry = () => { return ( { let val = parseFloat(e.target.value) if(!isNaN(val)){ setMoistureTarget(val) } setMoistureEntry(e.target.value) }} /> ) } const expandedTableDialog = () => { return ( {setOpenExpandedCables(false)}}> ) } return ( {expandedTableDialog()} {/* Level Table */} Grain Table {/* this box is to make the whole table scrollable */} {/* "Targets" cell — hide on mobile when either field is being edited */} {!(isMobile && (enterTemp || enterMoisture)) && ( {!isMobile && Targets } )} {/* Temperature cell */} {!(isMobile && enterMoisture) && ( {enterTemp ? updateTempEntry() : ( {tempThreshDisplay(isMobile)} )} {enterTemp ? ( { setEnterTemp(false); update(); }}> ) : ( setEnterTemp(true)}> )} )} {/* Moisture cell — hide on mobile when temperature is being edited */} {!(isMobile && enterTemp) && ( {enterMoisture ? updateMoistureEntry() : ( {bin.targetMoisture()} )} {enterMoisture ? ( { setEnterMoisture(false); update(); }}> ) : ( setEnterMoisture(true)}> )} )} Level {isMobile ? "T" : "Temperature"}{(user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? " (°F)" : " (°C)")} EMC (%) {binLevels.map((level, i) => ( {binLevels.length - i} {levelDisplay(level, i === binLevels.length-1)} ))}
{/* Table Legend */} On target {user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "~10 °F" : "+5 °C"} (0.5%) above {user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "~20 °F" : "+10 °C"} (1.5%) above
) }