763 lines
23 KiB
TypeScript
763 lines
23 KiB
TypeScript
import {
|
|
Box,
|
|
Button,
|
|
Card,
|
|
darken,
|
|
Grid2 as Grid,
|
|
InputAdornment,
|
|
lighten,
|
|
Slider,
|
|
TextField,
|
|
Theme,
|
|
Tooltip,
|
|
Typography
|
|
} from "@mui/material";
|
|
import { CheckCircleOutline, ImportExport } from "@mui/icons-material";
|
|
import Warning from "@mui/icons-material/Warning";
|
|
import HumidityIcon from "component/HumidityIcon";
|
|
import TemperatureIcon from "component/TemperatureIcon";
|
|
import { cloneDeep } from "lodash";
|
|
import { Bin, Component } from "models";
|
|
import { GrainCable } from "models/GrainCable";
|
|
import { UnitMeasurement } from "models/UnitMeasurement";
|
|
import Co2Icon from "products/CommonIcons/co2Icon";
|
|
import { pond } from "protobuf-ts/pond";
|
|
import { useBinAPI, useGlobalState, useSnackbar } from "providers";
|
|
import React, { useEffect, useState } from "react";
|
|
import { avg, getTemperatureUnit } from "utils";
|
|
import { makeStyles } from "@mui/styles";
|
|
import { Mark } from "@mui/material/Slider/useSlider.types";
|
|
import { CO2 } from "models/CO2";
|
|
|
|
const useStyles = makeStyles((theme: Theme) => {
|
|
return ({
|
|
card: {
|
|
padding: 10,
|
|
paddingLeft: 15,
|
|
paddingRight: 15
|
|
},
|
|
displayBG: {
|
|
marginTop: 10,
|
|
background: darken(theme.palette.background.default, 0.05),
|
|
borderRadius: 5,
|
|
padding: 10
|
|
},
|
|
slider: {
|
|
zIndex: 1,
|
|
"&:hover": {
|
|
zIndex: 3
|
|
}
|
|
},
|
|
markContainer: {
|
|
zIndex: 2
|
|
},
|
|
arrowDown: {
|
|
width: 0,
|
|
height: 0,
|
|
borderLeft: "10px solid transparent",
|
|
borderRight: "10px solid transparent",
|
|
borderTop: "10px solid yellow"
|
|
},
|
|
co2Box: {
|
|
background: lighten(theme.palette.background.default, 0.2),
|
|
borderRadius: 10,
|
|
padding: 10,
|
|
margin: 10,
|
|
marginLeft: 0,
|
|
marginRight: 0
|
|
},
|
|
low: {
|
|
fontWeight: 650,
|
|
color: "#0575E6"
|
|
},
|
|
target: {
|
|
fontWeight: 650,
|
|
color: "green"
|
|
},
|
|
high: {
|
|
fontWeight: 650,
|
|
color: "#c42605"
|
|
},
|
|
sliderRoot: {},
|
|
sliderThumb: {
|
|
height: 15,
|
|
width: 15,
|
|
marginTop: 0,
|
|
marginLeft: 0,
|
|
backgroundColor: "yellow"
|
|
},
|
|
sliderTrack: {
|
|
height: 3,
|
|
backgroundColor: "white"
|
|
},
|
|
sliderRail: {
|
|
height: 3,
|
|
backgroundColor: "white"
|
|
},
|
|
sliderValLabel: {
|
|
left: -20,
|
|
top: 40,
|
|
background: "transparent",
|
|
"& *": {
|
|
color: "#fff"
|
|
}
|
|
},
|
|
sliderMark: {
|
|
visibility: "hidden"
|
|
},
|
|
sliderMarked: {
|
|
marginTop: 25,
|
|
marginBottom: 0
|
|
},
|
|
sliderMarkLabel: {
|
|
top: -25,
|
|
left: -10
|
|
}
|
|
});
|
|
});
|
|
|
|
interface Props {
|
|
bin: Bin;
|
|
headspaceCO2: CO2[];
|
|
cables: GrainCable[];
|
|
}
|
|
|
|
interface NodeCounts {
|
|
below: number;
|
|
onTarget: number;
|
|
above: number;
|
|
total: number;
|
|
}
|
|
|
|
export default function BinStorageConditions(props: Props) {
|
|
const classes = useStyles();
|
|
const binAPI = useBinAPI();
|
|
const { openSnack } = useSnackbar();
|
|
const { bin, headspaceCO2, cables } = props;
|
|
const [{as}] = useGlobalState()
|
|
const [sliderTemps, setSliderTemps] = useState<number[]>([-40, 40]);
|
|
//boolean that if a cable is missing its filled to display an icon or something to let the user know not all of the cables have their fill set
|
|
const [missingTopNodeWarning, setMissingTopNodeWarning] = useState(false);
|
|
const [targetTemp, setTargetTemp] = useState("");
|
|
const [targetEMC, setTargetEMC] = useState("");
|
|
const [emcDeviation, setEmcDeviation] = useState("");
|
|
const [averageTemp, setAverageTemp] = useState<number | undefined>(10);
|
|
const [averageEMC, setAverageEMC] = useState<number | undefined>(10);
|
|
|
|
//the number of nodes in the grain that fall into each category
|
|
const [tempTargets, setTempTargets] = useState<NodeCounts>({
|
|
below: 0,
|
|
onTarget: 0,
|
|
above: 0,
|
|
total: 0
|
|
});
|
|
const [emcTargets, setEmcTargets] = useState<NodeCounts>({
|
|
below: 0,
|
|
onTarget: 0,
|
|
above: 0,
|
|
total: 0
|
|
});
|
|
|
|
useEffect(() => {
|
|
setSliderTemps([bin.settings.lowTemp, bin.settings.highTemp]);
|
|
let cableTempAvgs: number[] = [];
|
|
let tempCounts: NodeCounts = {
|
|
below: 0,
|
|
onTarget: 0,
|
|
above: 0,
|
|
total: 0
|
|
};
|
|
let emcCounts: NodeCounts = {
|
|
below: 0,
|
|
onTarget: 0,
|
|
above: 0,
|
|
total: 0
|
|
};
|
|
let cableEMCAvgs: number[] = [];
|
|
cables.forEach(cable => {
|
|
//if a cable is missing the top node being set
|
|
if (cable.topNode === 0) {
|
|
setMissingTopNodeWarning(true);
|
|
} else {
|
|
//with the way the measurements are stored the node closest to the device (highest on the cable) is the first position of the array
|
|
//so we will need to flip the array so that node 1 is the beginning of the array
|
|
let temps = cloneDeep(cable.temperatures).reverse();
|
|
let emcs = cloneDeep(cable.grainMoistures).reverse();
|
|
|
|
//only use the nodes up to the top node
|
|
let nodeTemps: number[] = [];
|
|
temps.forEach((temp, i) => {
|
|
if (i < cable.topNode) {
|
|
nodeTemps.push(temp);
|
|
tempCounts.total++;
|
|
if (temp > bin.settings.highTemp) {
|
|
tempCounts.above++;
|
|
} else if (temp < bin.settings.lowTemp) {
|
|
tempCounts.below++;
|
|
} else {
|
|
tempCounts.onTarget++;
|
|
}
|
|
}
|
|
});
|
|
|
|
let nodeEMCs: number[] = [];
|
|
|
|
emcs.forEach((emc, i) => {
|
|
if (bin.settings.inventory?.targetMoisture) {
|
|
if (i < cable.topNode) {
|
|
nodeEMCs.push(emc);
|
|
emcCounts.total++;
|
|
if (
|
|
emc >
|
|
bin.settings.inventory.targetMoisture +
|
|
bin.settings.inventory.moistureTargetDeviation
|
|
) {
|
|
emcCounts.above++;
|
|
} else if (
|
|
emc <
|
|
bin.settings.inventory.targetMoisture -
|
|
bin.settings.inventory.moistureTargetDeviation
|
|
) {
|
|
emcCounts.below++;
|
|
} else {
|
|
emcCounts.onTarget++;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
cableTempAvgs.push(avg(nodeTemps));
|
|
cableEMCAvgs.push(avg(nodeEMCs));
|
|
}
|
|
});
|
|
if (cableTempAvgs.length > 0) {
|
|
setAverageTemp(avg(cableTempAvgs));
|
|
}
|
|
setTempTargets(tempCounts);
|
|
let tempVal = bin.settings.inventory?.targetTemperature ?? 0;
|
|
if (getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
|
tempVal = tempVal * 1.8 + 32;
|
|
}
|
|
setTargetTemp(tempVal.toFixed(1));
|
|
|
|
if (cableEMCAvgs.length > 0) {
|
|
setAverageEMC(avg(cableEMCAvgs));
|
|
}
|
|
setEmcTargets(emcCounts);
|
|
setTargetEMC(bin.settings.inventory?.targetMoisture.toFixed(1) ?? "");
|
|
setEmcDeviation(bin.settings.inventory?.moistureTargetDeviation.toFixed(1) ?? "");
|
|
}, [bin, cables]);
|
|
|
|
//useEffect that watches for changes in the target emc and deviation to update the targets
|
|
useEffect(() => {
|
|
let target = parseFloat(targetEMC);
|
|
let deviation = parseFloat(emcDeviation);
|
|
if (!isNaN(target) && !isNaN(deviation)) {
|
|
let emcCounts: NodeCounts = {
|
|
below: 0,
|
|
onTarget: 0,
|
|
above: 0,
|
|
total: 0
|
|
};
|
|
|
|
cables.forEach(cable => {
|
|
let emcs = cloneDeep(cable.grainMoistures).reverse();
|
|
//let nodeEMCs: number[] = [];
|
|
|
|
emcs.forEach((emc, i) => {
|
|
if (bin.settings.inventory?.targetMoisture) {
|
|
if (i < cable.topNode) {
|
|
//nodeEMCs.push(emc);
|
|
emcCounts.total++;
|
|
if (emc > target + deviation) {
|
|
emcCounts.above++;
|
|
} else if (emc < target - deviation) {
|
|
emcCounts.below++;
|
|
} else {
|
|
emcCounts.onTarget++;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
setEmcTargets(emcCounts);
|
|
}
|
|
}, [targetEMC, emcDeviation, bin, cables]);
|
|
|
|
const isErrors = () => {
|
|
let errors = false;
|
|
if (
|
|
isNaN(parseFloat(targetTemp)) ||
|
|
isNaN(parseFloat(targetEMC)) ||
|
|
isNaN(parseFloat(emcDeviation))
|
|
) {
|
|
errors = true;
|
|
}
|
|
return errors;
|
|
};
|
|
|
|
const updateBinThresholds = () => {
|
|
//update the bin settings with the new thresholds
|
|
let settings = bin.settings;
|
|
settings.lowTemp = sliderTemps[0];
|
|
settings.highTemp = sliderTemps[1];
|
|
if (settings.inventory) {
|
|
let tempVal = parseFloat(targetTemp);
|
|
if (getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
|
tempVal = Math.fround(((tempVal - 32) * 5) / 9);
|
|
}
|
|
settings.inventory.targetTemperature = tempVal;
|
|
settings.inventory.targetMoisture = parseFloat(targetEMC);
|
|
settings.inventory.moistureTargetDeviation = parseFloat(emcDeviation);
|
|
}
|
|
|
|
binAPI
|
|
.updateBin(bin.key(), settings, as)
|
|
.then(_resp => {
|
|
openSnack("Updated bin thresholds");
|
|
})
|
|
.catch(_err => {
|
|
openSnack("Failed to update bin thresholds");
|
|
});
|
|
};
|
|
|
|
const updateSliderTemps = (newVals: number[]) => {
|
|
//the slider always returns the array with the lower value first
|
|
setSliderTemps(newVals);
|
|
|
|
//re-calc the node counts
|
|
let tempCounts: NodeCounts = {
|
|
below: 0,
|
|
onTarget: 0,
|
|
above: 0,
|
|
total: 0
|
|
};
|
|
cables.forEach(cable => {
|
|
let temps = cloneDeep(cable.temperatures).reverse();
|
|
let nodeTemps: number[] = [];
|
|
temps.forEach((temp, i) => {
|
|
if (i < cable.topNode) {
|
|
nodeTemps.push(temp);
|
|
tempCounts.total++;
|
|
if (temp > newVals[1]) {
|
|
tempCounts.above++;
|
|
} else if (temp < newVals[0]) {
|
|
tempCounts.below++;
|
|
} else {
|
|
tempCounts.onTarget++;
|
|
}
|
|
}
|
|
});
|
|
});
|
|
setTempTargets(tempCounts);
|
|
};
|
|
|
|
const customMark = (val: string, sub: string) => {
|
|
return (
|
|
<Box className={classes.markContainer}>
|
|
<Grid container direction="column" alignContent="center" alignItems="center">
|
|
<Grid>
|
|
<Typography style={{ fontSize: 15, fontWeight: 650, lineHeight: 1 }}>{val}</Typography>
|
|
</Grid>
|
|
<Grid>
|
|
<Typography style={{ fontSize: 8 }}>{sub}</Typography>
|
|
</Grid>
|
|
<Grid>
|
|
<Box className={classes.arrowDown} />
|
|
</Grid>
|
|
</Grid>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
//this will display at all times
|
|
const tempDisplay = () => {
|
|
// console.log(tempTargets)
|
|
let sliderEdge = 40;
|
|
|
|
let mark: Mark[] = [];
|
|
if (averageTemp) {
|
|
let temp = averageTemp;
|
|
if (getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
|
temp = temp * 1.8 + 32;
|
|
}
|
|
mark = [
|
|
{
|
|
label: customMark(
|
|
temp.toFixed(1) +
|
|
(getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT
|
|
? "°F"
|
|
: "°C"),
|
|
"AVG"
|
|
),
|
|
value: averageTemp
|
|
}
|
|
];
|
|
}
|
|
|
|
return (
|
|
<Box className={classes.displayBG}>
|
|
<Grid
|
|
container
|
|
direction="row"
|
|
alignContent="center"
|
|
alignItems="center"
|
|
justifyContent="space-between">
|
|
<Grid size={{ xs: 8 }}>
|
|
<Box display="flex" alignContent="center" alignItems="center">
|
|
<TemperatureIcon />
|
|
<Typography noWrap style={{ fontWeight: 650 }}>
|
|
Target Temp =
|
|
</Typography>
|
|
</Box>
|
|
</Grid>
|
|
<Grid size={{ xs: 4 }}>
|
|
<TextField
|
|
variant="outlined"
|
|
margin="dense"
|
|
value={targetTemp}
|
|
error={isNaN(parseFloat(targetTemp))}
|
|
helperText={isNaN(parseFloat(targetTemp)) && "Must be a valid number"}
|
|
onChange={e => {
|
|
setTargetTemp(e.target.value);
|
|
}}
|
|
InputProps={{
|
|
endAdornment: (
|
|
<InputAdornment position="end">
|
|
{getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT
|
|
? "°F"
|
|
: "°C"}
|
|
</InputAdornment>
|
|
)
|
|
}}
|
|
/>
|
|
</Grid>
|
|
</Grid>
|
|
<Slider
|
|
className={classes.slider}
|
|
classes={{
|
|
root: classes.sliderRoot,
|
|
rail: classes.sliderRail,
|
|
track: classes.sliderTrack,
|
|
thumb: classes.sliderThumb,
|
|
valueLabel: classes.sliderValLabel,
|
|
marked: classes.sliderMarked,
|
|
mark: classes.sliderMark,
|
|
markLabel: classes.sliderMarkLabel
|
|
}}
|
|
|
|
marks={mark}
|
|
step={0.1}
|
|
value={sliderTemps}
|
|
min={-sliderEdge}
|
|
max={sliderEdge}
|
|
valueLabelDisplay="on"
|
|
valueLabelFormat={value => {
|
|
if (getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
|
return ((value * 9) / 5 + 32).toFixed(1) + "°F";
|
|
}
|
|
return value.toFixed(1) + "°C";
|
|
}}
|
|
onChange={(_, newVals) => {
|
|
updateSliderTemps(newVals as number[]);
|
|
}}
|
|
/>
|
|
{tempTargets.total > 0 && (
|
|
<Box style={{ marginLeft: 10, marginRight: 10, marginTop: 5 }}>
|
|
<Grid container direction="row" justifyContent="space-between">
|
|
<Grid>
|
|
<Typography className={classes.low} align="center">
|
|
{((tempTargets.below / tempTargets.total) * 100).toFixed(2)}%
|
|
</Typography>
|
|
<Typography align="center">Below</Typography>
|
|
</Grid>
|
|
<Grid>
|
|
<Typography className={classes.target} align="center">
|
|
{((tempTargets.onTarget / tempTargets.total) * 100).toFixed(2)}%
|
|
</Typography>
|
|
<Typography align="center">On Target</Typography>
|
|
</Grid>
|
|
<Grid>
|
|
<Typography className={classes.high} align="center">
|
|
{((tempTargets.above / tempTargets.total) * 100).toFixed(2)}%
|
|
</Typography>
|
|
<Typography align="center">Above</Typography>
|
|
</Grid>
|
|
</Grid>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
//this will only display if the bin has the moisture target set
|
|
const moistureDisplay = () => {
|
|
const targetVal = parseFloat(targetEMC);
|
|
const devVal = parseFloat(emcDeviation);
|
|
let low = targetVal - devVal;
|
|
let high = targetVal + devVal;
|
|
const min = 0;
|
|
const max = 25;
|
|
if (isNaN(low) || isNaN(high)) {
|
|
low = 0;
|
|
high = 0;
|
|
}
|
|
|
|
let mark: Mark[] = [];
|
|
if (averageEMC) {
|
|
mark = [
|
|
{
|
|
label: customMark(averageEMC.toFixed(1) + "%", "AVG"),
|
|
value: averageEMC
|
|
}
|
|
];
|
|
}
|
|
return (
|
|
<Box className={classes.displayBG}>
|
|
<Grid
|
|
container
|
|
direction="row"
|
|
alignContent="center"
|
|
alignItems="center"
|
|
justifyContent="space-between">
|
|
<Grid size={{ xs: 6 }}>
|
|
<Box display="flex" alignContent="center" alignItems="center">
|
|
<Box style={{ width: 24, paddingLeft: 3 }}>
|
|
<HumidityIcon height={24} width={15} />
|
|
</Box>
|
|
<Typography noWrap style={{ fontWeight: 650 }}>
|
|
Target EMC =
|
|
</Typography>
|
|
</Box>
|
|
</Grid>
|
|
<Grid size={{ xs: 4 }}>
|
|
<TextField
|
|
variant="outlined"
|
|
margin="dense"
|
|
value={targetEMC}
|
|
error={isNaN(parseFloat(targetEMC))}
|
|
helperText={isNaN(parseFloat(targetEMC)) && "Must be a valid number"}
|
|
onChange={e => {
|
|
setTargetEMC(e.target.value);
|
|
}}
|
|
InputProps={{
|
|
endAdornment: <InputAdornment position="end">%</InputAdornment>
|
|
}}
|
|
/>
|
|
</Grid>
|
|
</Grid>
|
|
<Grid
|
|
container
|
|
direction="row"
|
|
alignContent="center"
|
|
alignItems="center"
|
|
justifyContent="space-between">
|
|
<Grid size={{ xs: 6 }}>
|
|
<Box display="flex" alignContent="center" alignItems="center">
|
|
<ImportExport />
|
|
<Typography noWrap style={{ fontWeight: 650 }}>
|
|
Deviation =
|
|
</Typography>
|
|
</Box>
|
|
</Grid>
|
|
<Grid size={{ xs: 4 }}>
|
|
<TextField
|
|
variant="outlined"
|
|
margin="dense"
|
|
value={emcDeviation}
|
|
error={isNaN(parseFloat(emcDeviation))}
|
|
helperText={isNaN(parseFloat(emcDeviation)) && "Must be a valid number"}
|
|
onChange={e => {
|
|
setEmcDeviation(e.target.value);
|
|
}}
|
|
InputProps={{
|
|
endAdornment: <InputAdornment position="end">%</InputAdornment>
|
|
}}
|
|
/>
|
|
</Grid>
|
|
</Grid>
|
|
<Slider
|
|
style={{ cursor: "default" }}
|
|
marks={mark}
|
|
classes={{
|
|
root: classes.sliderRoot,
|
|
rail: classes.sliderRail,
|
|
track: classes.sliderTrack,
|
|
thumb: classes.sliderThumb,
|
|
valueLabel: classes.sliderValLabel,
|
|
marked: classes.sliderMarked,
|
|
mark: classes.sliderMark,
|
|
markLabel: classes.sliderMarkLabel
|
|
}}
|
|
valueLabelDisplay="on"
|
|
valueLabelFormat={value => {
|
|
return value.toFixed(1) + "%";
|
|
}}
|
|
step={0.5}
|
|
min={min}
|
|
max={max}
|
|
value={[low, high]}
|
|
/>
|
|
{emcTargets.total > 0 && (
|
|
<Box style={{ marginLeft: 10, marginRight: 10, marginTop: 5 }}>
|
|
<Grid container direction="row" justifyContent="space-between">
|
|
<Grid>
|
|
<Typography className={classes.low} align="center">
|
|
{((emcTargets.below / emcTargets.total) * 100).toFixed(2)}%
|
|
</Typography>
|
|
<Typography align="center">Below</Typography>
|
|
</Grid>
|
|
<Grid>
|
|
<Typography className={classes.target} align="center">
|
|
{((emcTargets.onTarget / emcTargets.total) * 100).toFixed(2)}%
|
|
</Typography>
|
|
<Typography align="center">On Target</Typography>
|
|
</Grid>
|
|
<Grid>
|
|
<Typography className={classes.high} align="center">
|
|
{((emcTargets.above / emcTargets.total) * 100).toFixed(2)}%
|
|
</Typography>
|
|
<Typography align="center">Above</Typography>
|
|
</Grid>
|
|
</Grid>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
//this will only appear when there is a CO2 sensor
|
|
const co2Display = () => {
|
|
if (headspaceCO2.length > 0) {
|
|
return (
|
|
<React.Fragment>
|
|
<Box className={classes.displayBG}>
|
|
<Box display="flex">
|
|
<Co2Icon size={24} />
|
|
<Typography style={{ fontWeight: 650, marginLeft: 5 }}>
|
|
CO2 Spoilage Detection
|
|
</Typography>
|
|
</Box>
|
|
{headspaceCO2.map(co2 => {
|
|
let measurement = UnitMeasurement.any(co2.status.lastGoodMeasurement[0] ?? UnitMeasurement.create());
|
|
let reading: number = 0;
|
|
if (measurement.values[0] && measurement.values[0].values[0]) {
|
|
reading = measurement.values[0].values[0];
|
|
}
|
|
let mark: Mark[] = [];
|
|
if (reading > 0) {
|
|
mark = [
|
|
{
|
|
label: customMark(reading + "ppm", "last"),
|
|
value: reading
|
|
}
|
|
];
|
|
}
|
|
return (
|
|
<Grid
|
|
key={co2.key()}
|
|
container
|
|
direction="row"
|
|
alignContent="center"
|
|
alignItems="center">
|
|
<Grid size={{ xs: 4 }}>
|
|
<Box className={classes.co2Box}>
|
|
<Grid container direction="column" alignContent="center" alignItems="center">
|
|
<Grid >
|
|
<Typography style={{ fontSize: 15, textAlign: "center" }}>
|
|
Current CO2
|
|
</Typography>
|
|
</Grid>
|
|
<Grid >
|
|
<Typography
|
|
style={{
|
|
fontSize: 15,
|
|
fontWeight: 650,
|
|
textAlign: "center",
|
|
color: measurement.colour
|
|
}}>
|
|
{reading} ppm
|
|
</Typography>
|
|
</Grid>
|
|
<Grid >
|
|
<CheckCircleOutline style={{ color: reading < 1100 ? "green" : "red" }} />
|
|
</Grid>
|
|
</Grid>
|
|
</Box>
|
|
</Grid>
|
|
<Grid size={{ xs: 8 }}>
|
|
<Box marginLeft={2}>
|
|
<Box>
|
|
<Slider
|
|
style={{ cursor: "default" }}
|
|
min={200}
|
|
max={1300}
|
|
value={[400, 1100]}
|
|
marks={mark}
|
|
classes={{
|
|
root: classes.sliderRoot,
|
|
rail: classes.sliderRail,
|
|
track: classes.sliderTrack,
|
|
thumb: classes.sliderThumb,
|
|
valueLabel: classes.sliderValLabel,
|
|
marked: classes.sliderMarked,
|
|
mark: classes.sliderMark,
|
|
markLabel: classes.sliderMarkLabel
|
|
}}
|
|
valueLabelDisplay="on"
|
|
valueLabelFormat={value => {
|
|
return value + "ppm";
|
|
}}
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
</Grid>
|
|
</Grid>
|
|
);
|
|
})}
|
|
</Box>
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
return;
|
|
};
|
|
|
|
//made up of the co2, moisture and temp displays
|
|
const storageConditions = () => {
|
|
return (
|
|
<Box>
|
|
<Box display="flex" justifyContent={"space-between"}>
|
|
<Typography style={{ fontWeight: 650 }}>Storage Conditions</Typography>
|
|
{missingTopNodeWarning && (
|
|
<Tooltip
|
|
title={`
|
|
One or more cables do not have their fill level set.
|
|
Check to make sure all top nodes are set for your bin cables
|
|
`}>
|
|
<Warning />
|
|
</Tooltip>
|
|
)}
|
|
</Box>
|
|
{tempDisplay()}
|
|
{moistureDisplay()}
|
|
{co2Display()}
|
|
<Box display="flex" marginTop={1} justifyContent="center">
|
|
<Button
|
|
variant="contained"
|
|
color="primary"
|
|
disabled={isErrors()}
|
|
onClick={() => {
|
|
updateBinThresholds();
|
|
}}>
|
|
Update Thresholds
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<Card raised className={classes.card}>
|
|
{storageConditions()}
|
|
</Card>
|
|
);
|
|
}
|