2098 lines
71 KiB
TypeScript
2098 lines
71 KiB
TypeScript
import {
|
|
Avatar,
|
|
Box,
|
|
Button,
|
|
Card,
|
|
CircularProgress,
|
|
darken,
|
|
DialogActions,
|
|
DialogContent,
|
|
DialogTitle,
|
|
Grid2 as Grid,
|
|
IconButton,
|
|
lighten,
|
|
Link,
|
|
Skeleton,
|
|
Slider,
|
|
Switch,
|
|
TextField,
|
|
Theme,
|
|
Typography,
|
|
useTheme,
|
|
} from "@mui/material";
|
|
import FullscreenIcon from "@mui/icons-material/Fullscreen";
|
|
import FullscreenExitIcon from "@mui/icons-material/FullscreenExit";
|
|
import RefreshIcon from "@mui/icons-material/Refresh";
|
|
import HumidityIcon from "component/HumidityIcon";
|
|
import TemperatureIcon from "component/TemperatureIcon";
|
|
import GrainDescriber, { GrainOptions, ToGrainOption } from "grain/GrainDescriber";
|
|
import useViewport from "hooks/useViewport";
|
|
import { round } from "lodash";
|
|
import { Bin, Component, Device } from "models";
|
|
import { GetComponentIcon } from "pbHelpers/ComponentType";
|
|
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
|
|
import { useComponentAPI, useMobile } from "hooks";
|
|
import { pond } from "protobuf-ts/pond";
|
|
import { quack } from "protobuf-ts/quack";
|
|
import { useGlobalState, useSnackbar } from "providers";
|
|
import React, { useCallback, useEffect, useState } from "react";
|
|
import moment, { Moment } from "moment";
|
|
import ResponsiveDialog from "common/ResponsiveDialog";
|
|
import { getThemeType } from "theme";
|
|
import { or } from "utils";
|
|
import { useBinAPI } from "providers/pond/binAPI";
|
|
import BindaptIcon from "products/Bindapt/BindaptIcon";
|
|
import {
|
|
AccessTime,
|
|
ArrowForwardIos,
|
|
CheckCircleOutline,
|
|
Error,
|
|
InfoOutlined,
|
|
TrendingDown,
|
|
TrendingFlat,
|
|
TrendingUp,
|
|
Warning
|
|
} from "@mui/icons-material";
|
|
import { Plenum } from "models/Plenum";
|
|
import { Pressure } from "models/Pressure";
|
|
import { GrainCable } from "models/GrainCable";
|
|
import GrainNodeInteractions from "./GrainNodeInteractions";
|
|
import GrainTransaction from "grain/GrainTransaction";
|
|
import { DevicePreset } from "models/DevicePreset";
|
|
import BinSVGV2 from "./BinSVGV2";
|
|
import AerationFanIcon from "products/AgIcons/AerationFanIcon";
|
|
import ObjectHeaterIcon from "products/Construction/ObjectHeaterIcon";
|
|
import { Ambient } from "models/Ambient";
|
|
import { ExtractMoisture } from "grain";
|
|
import SearchSelect, { Option } from "common/SearchSelect";
|
|
import Edit from "@mui/icons-material/Edit";
|
|
import { makeStyles } from "@mui/styles";
|
|
import ButtonGroup from "common/ButtonGroup";
|
|
import ModeChangeDialog from "./conditioning/modeChangeDialog";
|
|
import CustomGrainSelector from "grain/CustomGrainSelector";
|
|
import BinControllerDisplay from "./BinControllerDisplay";
|
|
import { useFullScreen } from "hooks/FullScreenHandle";
|
|
|
|
const useStyles = makeStyles((theme: Theme) => {
|
|
return ({
|
|
cardContent: {
|
|
padding: theme.spacing(1),
|
|
paddingRight: theme.spacing(0)
|
|
},
|
|
bgItem: {
|
|
background: darken(theme.palette.background.paper, getThemeType() === "light" ? 0.05 : 0.25),
|
|
borderRadius: theme.shape.borderRadius,
|
|
width: "auto",
|
|
marginRight: theme.spacing(1),
|
|
padding: 5
|
|
},
|
|
displayBoxBinLeft: {
|
|
background: darken(theme.palette.background.default, 0.05),
|
|
borderRadius: 5,
|
|
marginRight: -70, //negative margin to extend the box behind the bin svg
|
|
paddingRight: 80, //padding to offset the text and ignore the extension
|
|
paddingTop: 5,
|
|
paddingBottom: 5,
|
|
paddingLeft: 5
|
|
},
|
|
displayBox: {
|
|
background: darken(theme.palette.background.default, 0.05),
|
|
borderRadius: 5,
|
|
padding: 5
|
|
},
|
|
controllerDisplay: {
|
|
display: "flex",
|
|
justifyContent: "space-between",
|
|
paddingLeft: 5,
|
|
paddingRight: 5
|
|
},
|
|
days: {
|
|
margin: theme.spacing(0.5),
|
|
fontSize: "16px"
|
|
},
|
|
avatarIcon: {
|
|
background: "transparent",
|
|
width: theme.spacing(3),
|
|
height: theme.spacing(3),
|
|
margin: "auto",
|
|
marginBottom: "auto"
|
|
},
|
|
selected: {
|
|
backgroundColor: "gold",
|
|
"&.selected": {
|
|
backgroundColor: "gold"
|
|
}
|
|
},
|
|
extraDetails: {
|
|
padding: 1,
|
|
marginTop: theme.spacing(1),
|
|
display: "flex",
|
|
width: "100%",
|
|
flexGrow: 1,
|
|
height: "100%"
|
|
},
|
|
grainOVerlay: {
|
|
position: "absolute",
|
|
height: "10%",
|
|
width: "100%",
|
|
textAlign: "center",
|
|
textShadow: "4px 4px 10px black"
|
|
},
|
|
grainDiff: {
|
|
position: "absolute",
|
|
top: "10%"
|
|
},
|
|
lightBox: {
|
|
background: lighten(theme.palette.background.default, 0.2),
|
|
borderRadius: 10,
|
|
padding: 10,
|
|
margin: 10
|
|
},
|
|
bottomSpacing: {
|
|
marginBottom: theme.spacing(1)
|
|
},
|
|
sliderValLabel: {
|
|
height: 30,
|
|
width: 30,
|
|
left: -5,
|
|
top: -20,
|
|
background: "transparent"
|
|
},
|
|
});
|
|
});
|
|
|
|
const average = (arr: any) => arr.reduce((p: any, c: any) => p + c, 0) / arr.length;
|
|
|
|
interface GrainConditions {
|
|
tempC: number;
|
|
humidity: number;
|
|
emc: number;
|
|
tempCTrend: number;
|
|
humidityTrend: number;
|
|
emcTrend: number;
|
|
}
|
|
|
|
interface CombinedPlenum {
|
|
tempHumidity?: Plenum,
|
|
pressure?: Pressure
|
|
}
|
|
|
|
interface Props {
|
|
bin: Bin;
|
|
loading: boolean;
|
|
components: Map<string, Component>;
|
|
componentDevices: Map<string, number>;
|
|
devices: Device[];
|
|
plenums?: Plenum[];
|
|
ambient?: Ambient;
|
|
cables?: GrainCable[];
|
|
pressures?: Pressure[];
|
|
refresh: (showSnack?: boolean) => void;
|
|
afterUpdate?(): void;
|
|
preferences?: Map<string, pond.BinComponentPreferences>;
|
|
permissions?: pond.Permission[];
|
|
updateComponentCallback: (componentKey: string) => void;
|
|
binPresets: DevicePreset[];
|
|
}
|
|
|
|
export default function BinVisualizer(props: Props) {
|
|
const {
|
|
bin,
|
|
plenums,
|
|
ambient,
|
|
pressures,
|
|
loading,
|
|
cables,
|
|
components,
|
|
devices,
|
|
preferences,
|
|
componentDevices,
|
|
permissions,
|
|
refresh,
|
|
updateComponentCallback,
|
|
binPresets
|
|
} = props;
|
|
const binAPI = useBinAPI();
|
|
const [{as}] = useGlobalState()
|
|
const isMobile = useMobile();
|
|
const classes = useStyles();
|
|
const theme = useTheme();
|
|
const {fullScreenHandler, FullScreenWrapper} = useFullScreen()
|
|
const viewport = useViewport();
|
|
const { openSnack } = useSnackbar();
|
|
const [fillPercentage, setFillPercentage] = useState<number | null>(0);
|
|
const [lidarBushels, setLidarBushels] = useState<number | undefined>();
|
|
const [lidarPercentage, setLidarPercentage] = useState<number | undefined>();
|
|
const [pendingGrainAmount, setPendingGrainAmount] = useState<number | undefined>();
|
|
const tempColour = describeMeasurement(
|
|
quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE
|
|
).colour();
|
|
const humidColour = describeMeasurement(
|
|
quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
|
|
quack.ComponentType.COMPONENT_TYPE_DHT
|
|
).colour();
|
|
const pressColour = describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE).colour();
|
|
const emcColour = describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_GRAIN_EMC).colour();
|
|
// const [showInputMoisture, setShowInputMoisture] = useState<boolean>(false);
|
|
const [targetMoisture, setTargetMoisture] = useState(0);
|
|
const [outdoorTemp, setOutdoorTemp] = useState(0);
|
|
const [outdoorHumidity, setOutdoorHumidity] = useState(0);
|
|
const [modeTime, setModeTime] = useState<Moment>(moment());
|
|
const [grainUpdate, setGrainUpdate] = useState<boolean>(false);
|
|
const [openStorageTime, setOpenStorageTime] = useState(false);
|
|
const [sliderColour, setSliderCoulour] = useState("gold");
|
|
const [grainDiff, setGrainDiff] = useState<number>(0);
|
|
|
|
const [grainCables, setGrainCables] = useState<GrainCable[]>([]);
|
|
const iOS = typeof window !== "undefined" && /iPad|iPhone|iPod/.test(navigator.userAgent);
|
|
const [nodeDetails, setNodeDetails] = useState(false);
|
|
const [cfmLowOpen, setCFMLowOpen] = useState(false);
|
|
const [cfmHighOpen, setCFMHighOpen] = useState(false);
|
|
const [{ user }] = useGlobalState();
|
|
// const [newPreset, setNewPreset] = useState<pond.BinMode>(pond.BinMode.BIN_MODE_NONE);
|
|
const [newBinMode, setNewBinMode] = useState<pond.BinMode>(pond.BinMode.BIN_MODE_NONE);
|
|
const [selectedCable, setSelectedCable] = useState<GrainCable>();
|
|
const [openNodeDialog, setOpenNodeDialog] = useState(false);
|
|
const [cableDevice, setCableDevice] = useState<Device | undefined>();
|
|
const [selectedNode, setSelectedNode] = useState(0);
|
|
const [devMap, setDevMap] = useState<Map<number, Device>>(new Map<number, Device>());
|
|
//the cables with the highest and lowest temp nodes
|
|
//const [highTempCable, setHighTempCable] = useState<GrainCable>();
|
|
//const [lowTempCable, setLowTempCable] = useState<GrainCable>();
|
|
//the switch value to determine what to display in the grain condition box
|
|
const [valueDisplay, setValueDisplay] = useState<"low" | "avg" | "high">("high");
|
|
//the variables to be able to change the grain type through a unique dialog outside of the settings
|
|
const [grainChangeDialog, setGrainChangeDialog] = useState(false);
|
|
const grainOptions = GrainOptions();
|
|
const [isCustomInventory, setIsCustomInventory] = useState(false);
|
|
const [grainOption, setGrainOption] = useState<Option>();
|
|
const [customTypeName, setCustomTypeName] = useState("");
|
|
const [grainType, setGrainType] = useState<pond.Grain>(pond.Grain.GRAIN_NONE);
|
|
const [storageType, setStorageType] = useState<pond.BinStorage>(
|
|
pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN
|
|
);
|
|
//const [bushPerTonne, setBushPerTonne] = useState("0");
|
|
const [grainSubtype, setGrainSubtype] = useState("");
|
|
|
|
const [loadingTrend, setLoadingTrend] = useState(false);
|
|
const [averageConditions, setAverageConditions] = useState<GrainConditions>();
|
|
const [highNodeConditions, setHighNodeConditions] = useState<GrainConditions>();
|
|
const [lowNodeConditions, setLowNodeConditions] = useState<GrainConditions>();
|
|
|
|
const [storageDate, setStorageDate] = useState(moment().format("YYYY-MM-DD"));
|
|
const [storageTime, setStorageTime] = useState(moment().format("HH:mm"));
|
|
|
|
const [activePlenum, setActivePlenum] = useState<CombinedPlenum | undefined>()
|
|
const [activePlenumIndex, setActivePlenumIndex] = useState(0)
|
|
const [combinedPlenums, setCombinedPlenums] = useState<CombinedPlenum[]>([])
|
|
|
|
const [openModeChange, setOpenModeChange] = useState(false)
|
|
const [modeChangeInProgress, setModeChangeInProgress] = useState(false)
|
|
const [newGrainSettings, setNewGrainSettings] = useState<pond.GrainSettings>()
|
|
|
|
const [filteredComponents, setFilteredComponents] = useState<Component[]>([])
|
|
|
|
useEffect(() => {
|
|
setModeTime(moment(bin.status.lastModeChange));
|
|
}, [bin.status.lastModeChange]);
|
|
|
|
useEffect(()=>{
|
|
//match the plenums and pressures based on their location (address)
|
|
let combinedPlenums: CombinedPlenum[] = []
|
|
if (plenums) {
|
|
plenums.forEach(plenum => {
|
|
let newCombinedPlenum: CombinedPlenum = {
|
|
tempHumidity: plenum
|
|
}
|
|
if (pressures) {
|
|
pressures.forEach(pressure => {
|
|
if(plenum.location().address === pressure.location().address){
|
|
newCombinedPlenum.pressure = pressure
|
|
}
|
|
})
|
|
}
|
|
combinedPlenums.push(newCombinedPlenum)
|
|
})
|
|
}
|
|
if (combinedPlenums.length > 0){
|
|
setActivePlenum(combinedPlenums[0])
|
|
}
|
|
setCombinedPlenums(combinedPlenums)
|
|
},[plenums, pressures])
|
|
|
|
useEffect(() => {
|
|
setIsCustomInventory(bin.storage() !== pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN);
|
|
setStorageType(bin.storage());
|
|
// setNewPreset(pond.BinMode.BIN_MODE_NONE);
|
|
if (bin.settings.inventory) {
|
|
setTargetMoisture(bin.settings.inventory.initialMoisture);
|
|
setCustomTypeName(bin.settings.inventory.customTypeName);
|
|
setGrainType(bin.settings.inventory.grainType);
|
|
setGrainOption(ToGrainOption(bin.settings.inventory.grainType));
|
|
//setBushPerTonne(bin.settings.inventory.bushelsPerTonne.toFixed(2));
|
|
setGrainSubtype(bin.subtype());
|
|
setNewBinMode(bin.settings.mode);
|
|
setNewGrainSettings(bin.customGrain());
|
|
}
|
|
if (bin.settings) {
|
|
let t = 0
|
|
let h = 0
|
|
if(ambient){
|
|
t = ambient.temperature
|
|
h = ambient.humidity
|
|
}else{
|
|
t = bin.settings.outdoorTemp;//use what is already set in the bin settings
|
|
h = bin.settings.outdoorHumidity
|
|
}
|
|
|
|
if (user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
|
t = CtoF(t);
|
|
}
|
|
setOutdoorTemp(t);
|
|
setOutdoorHumidity(h)
|
|
}
|
|
}, [bin, user, ambient]);
|
|
|
|
useEffect(() => {
|
|
//if(loadingTrend) return
|
|
if (cables && cables.length > 0) {
|
|
setGrainCables(cables);
|
|
let trendCables = cables;
|
|
if (bin.key() !== "" && !loadingTrend) {
|
|
setLoadingTrend(true);
|
|
binAPI
|
|
.getBinsTrendData([bin.key()], 7)
|
|
.then(resp => {
|
|
let binTrend = resp.data.trendData[bin.key()];
|
|
//the variables to use to build the average conditions
|
|
let temps: number[] = [];
|
|
let humids: number[] = [];
|
|
let emcs: number[] = [];
|
|
let tempTrends: number[] = [];
|
|
let humidTrends: number[] = [];
|
|
let emcTrends: number[] = [];
|
|
|
|
let lowNodeConditions: GrainConditions | undefined = undefined;
|
|
let highNodeConditions: GrainConditions | undefined = undefined;
|
|
let avgConditions: GrainConditions | undefined = undefined;
|
|
trendCables.forEach(cable => {
|
|
//only use the values below the top node
|
|
//because splice mutates the original array make a clone in the cables so the svg still has all of them for display
|
|
//also reverse it so that the node 1 (end of the cable) is in the first position
|
|
let filteredNodes = cable.filteredNodes(true)
|
|
temps.push(...filteredNodes.temps)
|
|
//only push to the humidity values if the cable reads humidities
|
|
if(cable.subType() !== quack.GrainCableSubtype.GRAIN_CABLE_SUBTYPE_OPI_TEMP){
|
|
humids.push(...filteredNodes.humids)
|
|
emcs.push(...filteredNodes.moistures)
|
|
}
|
|
|
|
//add the trend data to the proper arrays if the top node is set
|
|
let cableTrendData = binTrend.cableTrend[cable.key()];
|
|
tempTrends.push(cableTrendData.grainCelciusTrend);
|
|
humidTrends.push(cableTrendData.grainHumidityTrend);
|
|
emcTrends.push(cableTrendData.grainEmcTrend);
|
|
|
|
//if the lowest temp for this cable is less than the temp in the low node conditions or the low node conditions are not set
|
|
//use this cables lowest node and trend data in the condition
|
|
if (!lowNodeConditions || Math.min(...filteredNodes.temps) < lowNodeConditions.tempC) {
|
|
//determine which node is the coldest so that the data displayed is for the same node
|
|
let lowTempIndex =
|
|
filteredNodes.temps.indexOf(Math.min(...filteredNodes.temps)) === -1 ? 0 : filteredNodes.temps.indexOf(Math.min(...filteredNodes.temps));
|
|
lowNodeConditions = {
|
|
tempC: filteredNodes.temps[lowTempIndex],
|
|
humidity: filteredNodes.humids[lowTempIndex],
|
|
emc: filteredNodes.moistures[lowTempIndex],
|
|
tempCTrend: cableTrendData.coldNodeTrend?.tempTrend ?? 0,
|
|
humidityTrend: cableTrendData.coldNodeTrend?.humidityTrend ?? 0,
|
|
emcTrend: cableTrendData.coldNodeTrend?.emcTrend ?? 0
|
|
};
|
|
}
|
|
|
|
//do the same for the high node
|
|
if (!highNodeConditions || Math.max(...filteredNodes.temps) > highNodeConditions.tempC) {
|
|
let highTempIndex =
|
|
filteredNodes.temps.indexOf(Math.max(...filteredNodes.temps)) === -1 ? 0 : filteredNodes.temps.indexOf(Math.max(...filteredNodes.temps));
|
|
highNodeConditions = {
|
|
tempC: filteredNodes.temps[highTempIndex],
|
|
humidity: filteredNodes.humids[highTempIndex],
|
|
emc: filteredNodes.moistures[highTempIndex],
|
|
tempCTrend: cableTrendData.hotNodeTrend?.tempTrend ?? 0,
|
|
humidityTrend: cableTrendData.hotNodeTrend?.humidityTrend ?? 0,
|
|
emcTrend: cableTrendData.hotNodeTrend?.emcTrend ?? 0
|
|
};
|
|
}
|
|
});
|
|
|
|
//set the average conditions
|
|
avgConditions = {
|
|
tempC: average(temps),
|
|
humidity: average(humids),
|
|
emc: average(emcs),
|
|
tempCTrend: average(tempTrends),
|
|
humidityTrend: average(humidTrends),
|
|
emcTrend: average(emcTrends)
|
|
};
|
|
setAverageConditions(avgConditions);
|
|
setLowNodeConditions(lowNodeConditions);
|
|
setHighNodeConditions(highNodeConditions);
|
|
})
|
|
.catch(err => {
|
|
console.log(err);
|
|
openSnack("Failed to retrieve trend data");
|
|
})
|
|
.finally(() => {
|
|
setLoadingTrend(false);
|
|
});
|
|
}
|
|
}
|
|
}, [binAPI, bin, cables, openSnack]); //eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
useEffect(() => {
|
|
let cm = 0;
|
|
//just pull the number from the status
|
|
if (bin.status.distance && bin.status.distance > 0) {
|
|
//note that no conversions are happening as the unit measurement model is not being used so the value will be in cm
|
|
cm = bin.status.distance;
|
|
let height = or(bin.settings.specs?.heightCm, 0);
|
|
let ratio = 1 - cm / height;
|
|
let capacity = or(bin.settings.specs?.bushelCapacity, 0);
|
|
let lidarEstimate = Math.round(capacity * ratio);
|
|
setLidarBushels(lidarEstimate);
|
|
setLidarPercentage(Math.round((lidarEstimate / capacity) * 100));
|
|
} else {
|
|
setLidarBushels(undefined);
|
|
setLidarPercentage(undefined);
|
|
}
|
|
}, [bin]);
|
|
|
|
useEffect(() => {
|
|
let newMap = new Map<number, Device>();
|
|
devices.forEach(dev => {
|
|
newMap.set(dev.id(), dev);
|
|
});
|
|
setDevMap(newMap);
|
|
}, [devices]);
|
|
|
|
const resetFillPercentage = useCallback(() => {
|
|
if (bin.settings.inventory && bin.settings.specs) {
|
|
const isEmpty = bin.settings.inventory?.empty === true;
|
|
const capacity = bin.settings.specs.bushelCapacity;
|
|
const grainBushels = bin.bushels();
|
|
setSliderCoulour("gold");
|
|
if (!capacity) {
|
|
setFillPercentage(null);
|
|
} else if (isEmpty || !grainBushels) {
|
|
setFillPercentage(0);
|
|
} else {
|
|
setFillPercentage(Math.round((grainBushels / capacity) * 100));
|
|
}
|
|
}
|
|
}, [bin.settings.specs, bin.settings.inventory]);
|
|
|
|
useEffect(() => {
|
|
resetFillPercentage();
|
|
}, [resetFillPercentage]);
|
|
|
|
const getVolume = (val: number) => {
|
|
if (bin.storage() === pond.BinStorage.BIN_STORAGE_FERTILIZER) {
|
|
return Math.round(val * 35.239 * 100) / 100;
|
|
} else {
|
|
if (user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE && bin.bushelsPerTonne() > 1) {
|
|
return Math.round((val / bin.bushelsPerTonne()) * 100) / 100;
|
|
} else if (user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TON && bin.bushelsPerTonne() > 1) {
|
|
return Math.round((val / (bin.bushelsPerTonne()*0.907)) * 100) / 100;
|
|
}
|
|
}
|
|
return val;
|
|
};
|
|
|
|
const getCapacityDisplay = (bushelCapacity: number) => {
|
|
let c = 1;
|
|
if (bushelCapacity !== 0) {
|
|
c = bushelCapacity;
|
|
}
|
|
let capacity = getVolume(c);
|
|
if (bin.storage() === pond.BinStorage.BIN_STORAGE_FERTILIZER) {
|
|
return " / " + capacity.toLocaleString() + " L";
|
|
}
|
|
if (user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE && bin.bushelsPerTonne() > 1) {
|
|
return "mT (" + bin.fillPercent() + "%)";
|
|
} else if (user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TON && bin.bushelsPerTonne() > 1) {
|
|
return "t (" + bin.fillPercent() + "%)";
|
|
} else {
|
|
return " / " + capacity.toLocaleString() + " bu";
|
|
}
|
|
};
|
|
|
|
// const grainErrorCheck = () => {
|
|
// if (isNaN(parseFloat(bushPerTonne))) return true;
|
|
// return false;
|
|
// };
|
|
|
|
const inventoryOverview = () => {
|
|
const capacity = bin.settings.specs?.bushelCapacity ?? 0;
|
|
const grainBushels = bin.bushels();
|
|
const isEmpty = bin.settings.inventory?.empty === true || !grainBushels || grainBushels <= 0;
|
|
const grainSubtype = bin.settings.inventory?.grainSubtype;
|
|
return (
|
|
<Box>
|
|
{/* grain display */}
|
|
<Box display="flex" justifyContent="space-between">
|
|
<Typography variant="subtitle2" color="textPrimary" style={{ fontWeight: 800 }}>
|
|
{isEmpty ? "" : bin.grainName()}
|
|
{grainSubtype !== "" ? " - " + grainSubtype : ""}
|
|
</Typography>
|
|
<Box
|
|
style={{ cursor: "pointer" }}
|
|
onClick={() => {
|
|
setGrainChangeDialog(true);
|
|
}}>
|
|
<Edit />
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* box that display capacity */}
|
|
<Box className={classes.bgItem}>
|
|
<div style={{ display: "flex", flexDirection: "row" }}>
|
|
<Typography variant="body2" style={{ fontWeight: "bold", marginRight: "4px" }}>
|
|
{isEmpty ? "Empty" : (grainBushels ? getVolume(grainBushels) : 0).toLocaleString()}
|
|
</Typography>
|
|
<Typography variant="body2">{isEmpty ? "" : getCapacityDisplay(capacity)}</Typography>
|
|
</div>
|
|
{!isEmpty &&
|
|
lidarBushels &&
|
|
bin.inventoryControl() !==
|
|
pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR && (
|
|
<div style={{ display: "flex", flexDirection: "row" }}>
|
|
<Typography variant="body2">
|
|
{(lidarBushels ? getVolume(lidarBushels) : 0).toLocaleString()} /{" "}
|
|
{(capacity ? getVolume(capacity) : 0).toLocaleString()} bu (est)
|
|
</Typography>
|
|
</div>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const CtoF = (celsius: number) => {
|
|
return Math.round((celsius * (9 / 5) + 32) * 100) / 100;
|
|
};
|
|
|
|
const FtoC = (fahrenheit: number) => {
|
|
return Math.round((fahrenheit - 32) * (5 / 9) * 100) / 100;
|
|
};
|
|
|
|
const displayTemp = () => {
|
|
let temp = "--";
|
|
let valC: number | undefined;
|
|
switch (valueDisplay) {
|
|
case "avg":
|
|
if (averageConditions && !isNaN(averageConditions.tempC)) {
|
|
valC = averageConditions.tempC;
|
|
}
|
|
break;
|
|
case "high":
|
|
if (highNodeConditions && !isNaN(highNodeConditions.tempC)) {
|
|
valC = highNodeConditions.tempC;
|
|
}
|
|
break;
|
|
case "low":
|
|
if (lowNodeConditions && !isNaN(lowNodeConditions.tempC)) {
|
|
valC = lowNodeConditions.tempC;
|
|
}
|
|
break;
|
|
}
|
|
if (valC) {
|
|
if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
|
temp = CtoF(valC).toFixed(1) + "°F";
|
|
} else {
|
|
temp = valC.toFixed(1) + "°C";
|
|
}
|
|
}
|
|
return (
|
|
<Typography
|
|
align="center"
|
|
noWrap
|
|
style={{
|
|
fontSize: isMobile ? "0.85rem" : "1.0rem",
|
|
fontWeight: 650,
|
|
color: tempColour
|
|
}}>
|
|
{temp}
|
|
</Typography>
|
|
);
|
|
};
|
|
|
|
const displayHumidityOrEMC = () => {
|
|
let humidity = "--";
|
|
let useEMC = false;
|
|
let val: number | undefined;
|
|
switch (valueDisplay) {
|
|
case "avg":
|
|
if (averageConditions) {
|
|
if (averageConditions.emc) {
|
|
useEMC = true;
|
|
val = averageConditions.emc;
|
|
} else {
|
|
val = averageConditions.humidity;
|
|
}
|
|
}
|
|
break;
|
|
case "high":
|
|
if (highNodeConditions) {
|
|
if (highNodeConditions.emc) {
|
|
useEMC = true;
|
|
val = highNodeConditions.emc;
|
|
} else {
|
|
val = highNodeConditions.humidity;
|
|
}
|
|
}
|
|
break;
|
|
case "low":
|
|
if (lowNodeConditions) {
|
|
if (lowNodeConditions.emc) {
|
|
useEMC = true;
|
|
val = lowNodeConditions.emc;
|
|
} else {
|
|
val = lowNodeConditions.humidity;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
if (val && !isNaN(val)) {
|
|
humidity = val.toFixed(1) + "%";
|
|
}
|
|
return (
|
|
<Typography
|
|
align="center"
|
|
noWrap
|
|
style={{
|
|
fontSize: isMobile ? "0.85rem" : "1.0rem",
|
|
fontWeight: 650,
|
|
color: useEMC ? emcColour : humidColour
|
|
}}>
|
|
{humidity}
|
|
</Typography>
|
|
);
|
|
};
|
|
|
|
const trendArrow = (val: number) => {
|
|
if (val > 0) return <TrendingUp style={{ fill: "green" }} />;
|
|
if (val < 0) return <TrendingDown style={{ fill: "red" }} />;
|
|
return <TrendingFlat />;
|
|
};
|
|
|
|
const displayTempTrend = () => {
|
|
let temp = "--";
|
|
let valC: number | undefined;
|
|
switch (valueDisplay) {
|
|
case "avg":
|
|
if (averageConditions && !isNaN(averageConditions.tempCTrend)) {
|
|
valC = averageConditions.tempCTrend;
|
|
}
|
|
break;
|
|
case "high":
|
|
if (highNodeConditions && !isNaN(highNodeConditions.tempCTrend)) {
|
|
valC = highNodeConditions.tempCTrend;
|
|
}
|
|
break;
|
|
case "low":
|
|
if (lowNodeConditions && !isNaN(lowNodeConditions.tempCTrend)) {
|
|
valC = lowNodeConditions.tempCTrend;
|
|
}
|
|
break;
|
|
}
|
|
if (valC) {
|
|
if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
|
temp = Math.abs(valC * 1.8).toFixed(1) + "°F"; //since this is a measurement of change a change in 1 degrre celsius is equivalent to 1.8 fahrenheit
|
|
} else {
|
|
temp = Math.abs(valC).toFixed(1) + "°C";
|
|
}
|
|
}
|
|
return (
|
|
<Box height={20} display="flex" justifyContent="center">
|
|
{valC !== undefined && !isNaN(valC) && trendArrow(valC)}
|
|
<Typography
|
|
align="center"
|
|
noWrap
|
|
style={{ fontSize: isMobile ? "0.85rem" : "1.0rem", fontWeight: 650 }}>
|
|
{temp}
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const displayHumidOrEmcTrend = () => {
|
|
let display = "--";
|
|
let val: number | undefined;
|
|
switch (valueDisplay) {
|
|
case "avg":
|
|
if (averageConditions) {
|
|
if (averageConditions.emcTrend) {
|
|
val = averageConditions.emcTrend;
|
|
} else {
|
|
val = averageConditions.humidityTrend;
|
|
}
|
|
}
|
|
break;
|
|
case "high":
|
|
if (highNodeConditions) {
|
|
if (highNodeConditions.emcTrend) {
|
|
val = highNodeConditions.emcTrend;
|
|
} else {
|
|
val = highNodeConditions.humidityTrend;
|
|
}
|
|
}
|
|
break;
|
|
case "low":
|
|
if (lowNodeConditions) {
|
|
if (lowNodeConditions.emcTrend) {
|
|
val = lowNodeConditions.emcTrend;
|
|
} else {
|
|
val = lowNodeConditions.humidityTrend;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
if (val && !isNaN(val)) {
|
|
display = Math.abs(val).toFixed(1) + "%";
|
|
}
|
|
return (
|
|
<Box height={20} display="flex" justifyContent="center">
|
|
{val !== undefined && !isNaN(val) && trendArrow(val)}
|
|
<Typography
|
|
align="center"
|
|
noWrap
|
|
style={{ fontSize: isMobile ? "0.85rem" : "1.0rem", fontWeight: 650 }}>
|
|
{display}
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
//this function is used to reset the state variable to what is in the bin whne they close it without saving any changes
|
|
const closeGrain = () => {
|
|
setIsCustomInventory(bin.storage() !== pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN);
|
|
setStorageType(bin.storage());
|
|
if (bin.settings.inventory) {
|
|
setTargetMoisture(bin.settings.inventory.initialMoisture);
|
|
setCustomTypeName(bin.settings.inventory.customTypeName);
|
|
setGrainType(bin.settings.inventory.grainType);
|
|
setGrainOption(ToGrainOption(bin.settings.inventory.grainType));
|
|
//setBushPerTonne(bin.settings.inventory.bushelsPerTonne.toFixed(2));
|
|
setNewGrainSettings(bin.customGrain())
|
|
setGrainSubtype(bin.subtype());
|
|
}
|
|
setGrainChangeDialog(false);
|
|
};
|
|
|
|
const changeGrain = () => {
|
|
const canEdit = permissions ? permissions.includes(pond.Permission.PERMISSION_WRITE) : false;
|
|
return (
|
|
<ResponsiveDialog
|
|
open={grainChangeDialog}
|
|
onClose={() => {
|
|
closeGrain();
|
|
}}>
|
|
<DialogTitle>Change Grain Type</DialogTitle>
|
|
<DialogContent>
|
|
<Grid container justifyContent="space-between" alignItems="center">
|
|
<Grid>
|
|
<Grid container alignItems="center">
|
|
<Grid >Grain</Grid>
|
|
<Grid >
|
|
<Switch
|
|
color="default"
|
|
value={isCustomInventory}
|
|
checked={isCustomInventory}
|
|
onChange={(_, checked) => {
|
|
setIsCustomInventory(checked);
|
|
//setBushPerTonne("0");
|
|
setGrainSubtype("");
|
|
setCustomTypeName("");
|
|
setGrainOption(ToGrainOption(pond.Grain.GRAIN_NONE));
|
|
if (checked) {
|
|
setStorageType(pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN);
|
|
setGrainType(pond.Grain.GRAIN_CUSTOM);
|
|
setNewGrainSettings(bin.customGrain())
|
|
} else {
|
|
setStorageType(pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN);
|
|
setGrainType(pond.Grain.GRAIN_NONE);
|
|
setNewGrainSettings(undefined)
|
|
}
|
|
}}
|
|
name="storage"
|
|
/>
|
|
</Grid>
|
|
<Grid >Custom</Grid>
|
|
</Grid>
|
|
</Grid>
|
|
</Grid>
|
|
{!isCustomInventory && (
|
|
<Box className={classes.bottomSpacing}>
|
|
<SearchSelect
|
|
label="Type"
|
|
selected={grainOption}
|
|
changeSelection={option => {
|
|
let newGrainType = option
|
|
? pond.Grain[option.value as keyof typeof pond.Grain]
|
|
: pond.Grain.GRAIN_INVALID;
|
|
setGrainOption(ToGrainOption(newGrainType));
|
|
setGrainType(newGrainType);
|
|
//setBushPerTonne(GrainDescriber(newGrainType).bushelsPerTonne.toFixed(2));
|
|
}}
|
|
group
|
|
disabled={!canEdit}
|
|
options={grainOptions}
|
|
/>
|
|
</Box>
|
|
)}
|
|
{isCustomInventory ? (
|
|
<React.Fragment>
|
|
{/* <TextField
|
|
label="Type"
|
|
value={customTypeName}
|
|
type="text"
|
|
onChange={event => {
|
|
setCustomTypeName(event.target.value);
|
|
}}
|
|
fullWidth
|
|
variant="outlined"
|
|
className={classes.bottomSpacing}
|
|
/>
|
|
{storageType !== pond.BinStorage.BIN_STORAGE_FERTILIZER && (
|
|
<TextField
|
|
label="Bushels Per Tonne"
|
|
value={bushPerTonne}
|
|
type="number"
|
|
error={isNaN(parseFloat(bushPerTonne))}
|
|
helperText={isNaN(parseFloat(bushPerTonne)) && "Must be a valid number"}
|
|
onChange={event => {
|
|
setBushPerTonne(event.target.value);
|
|
}}
|
|
fullWidth
|
|
variant="outlined"
|
|
className={classes.bottomSpacing}
|
|
/>
|
|
)} */}
|
|
<CustomGrainSelector initialGrain={bin.customGrain()} onGrainSettingsChange={settings => {setNewGrainSettings(settings)}}/>
|
|
</React.Fragment>
|
|
) : (
|
|
<TextField
|
|
label="Grain Variant"
|
|
value={grainSubtype}
|
|
type="text"
|
|
onChange={event => {
|
|
setGrainSubtype(event.target.value);
|
|
}}
|
|
fullWidth
|
|
variant="outlined"
|
|
disabled={!grainOption}
|
|
className={classes.bottomSpacing}
|
|
/>
|
|
)}
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button
|
|
onClick={() => {
|
|
closeGrain();
|
|
}}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
variant="contained"
|
|
color="primary"
|
|
// disabled={grainErrorCheck()}
|
|
onClick={() => {
|
|
updateBin();
|
|
setGrainChangeDialog(false);
|
|
}}>
|
|
Confirm
|
|
</Button>
|
|
</DialogActions>
|
|
</ResponsiveDialog>
|
|
);
|
|
};
|
|
|
|
//the box that displays the grain conditions
|
|
const grainCableOverview = () => {
|
|
return (
|
|
<Box>
|
|
<Typography style={{ fontSize: isMobile ? "0.85rem" : "1.0rem", fontWeight: 650 }}>
|
|
Grain Condition
|
|
</Typography>
|
|
<Box className={classes.displayBoxBinLeft}>
|
|
<Grid container alignContent="center" alignItems="center" direction="row">
|
|
<Grid size={{ xs: 2 }}>
|
|
<TemperatureIcon heightWidth={isMobile ? 20 : 25} />
|
|
</Grid>
|
|
<Grid size={{ xs: 4 }}>
|
|
<Box height={20} display="flex" justifyContent="center">
|
|
{displayTemp()}
|
|
</Box>
|
|
<Box display="flex" justifyContent="center">
|
|
<Typography
|
|
align="center"
|
|
color="textSecondary"
|
|
noWrap
|
|
style={{ fontSize: "0.6rem" }}>
|
|
{valueDisplay === "low" ? "Low" : valueDisplay === "high" ? "High" : "Average"}
|
|
</Typography>
|
|
</Box>
|
|
</Grid>
|
|
<Grid size={{ xs: 6 }}>
|
|
{displayTempTrend()}
|
|
<Box display="flex" justifyContent="center">
|
|
<Typography
|
|
align="center"
|
|
color="textSecondary"
|
|
noWrap
|
|
style={{ fontSize: "0.6rem" }}>
|
|
Past Week
|
|
</Typography>
|
|
</Box>
|
|
</Grid>
|
|
</Grid>
|
|
<Grid container alignContent="center" alignItems="center" direction="row">
|
|
<Grid size={{ xs: 2 }} style={{ paddingLeft: isMobile ? 5 : 8 }}>
|
|
<HumidityIcon height={isMobile ? 20 : 25} />
|
|
</Grid>
|
|
<Grid size={{ xs: 4 }}>
|
|
<Box height={20} display="flex" justifyContent="center">
|
|
{displayHumidityOrEMC()}
|
|
</Box>
|
|
<Box display="flex" justifyContent="center">
|
|
<Typography
|
|
align="center"
|
|
color="textSecondary"
|
|
noWrap
|
|
style={{ fontSize: "0.6rem" }}>
|
|
{valueDisplay === "low" ? "Low" : valueDisplay === "high" ? "High" : "Average"}
|
|
</Typography>
|
|
</Box>
|
|
</Grid>
|
|
<Grid size={{ xs: 6 }}>
|
|
{displayHumidOrEmcTrend()}
|
|
<Box display="flex" justifyContent="center">
|
|
<Typography
|
|
align="center"
|
|
color="textSecondary"
|
|
noWrap
|
|
style={{ fontSize: "0.6rem" }}>
|
|
Past Week
|
|
</Typography>
|
|
</Box>
|
|
</Grid>
|
|
</Grid>
|
|
<Grid
|
|
container
|
|
direction="row"
|
|
alignContent="center"
|
|
alignItems="center"
|
|
justifyContent="space-between"
|
|
style={{ marginTop: 10 }}>
|
|
<Grid size={{ xs: 4 }}>
|
|
<Button
|
|
onClick={() => {
|
|
setValueDisplay("low");
|
|
}}
|
|
style={{
|
|
width: "100%",
|
|
minWidth: 0,
|
|
zIndex: valueDisplay === "low" ? 1 : 0,
|
|
background:
|
|
valueDisplay === "low" ? "gold" : darken(theme.palette.background.paper, 0.2),
|
|
borderRadius: 10
|
|
}}>
|
|
<Typography
|
|
align="center"
|
|
noWrap
|
|
style={{
|
|
fontWeight: 650,
|
|
fontSize: "0.7rem",
|
|
color: valueDisplay === "low" ? "black" : "white"
|
|
}}>
|
|
Low
|
|
</Typography>
|
|
</Button>
|
|
</Grid>
|
|
<Grid size={{ xs: 4 }}>
|
|
<Button
|
|
onClick={() => {
|
|
setValueDisplay("avg");
|
|
}}
|
|
style={{
|
|
width: "100%",
|
|
minWidth: 0,
|
|
zIndex: valueDisplay === "avg" ? 1 : 0,
|
|
background:
|
|
valueDisplay === "avg" ? "gold" : darken(theme.palette.background.paper, 0.2),
|
|
borderRadius: 10
|
|
}}>
|
|
<Typography
|
|
align="center"
|
|
noWrap
|
|
style={{
|
|
fontWeight: 650,
|
|
fontSize: "0.7rem",
|
|
color: valueDisplay === "avg" ? "black" : "white"
|
|
}}>
|
|
Avg
|
|
</Typography>
|
|
</Button>
|
|
</Grid>
|
|
<Grid size={{ xs: 4 }}>
|
|
<Button
|
|
onClick={() => {
|
|
setValueDisplay("high");
|
|
}}
|
|
style={{
|
|
width: "100%",
|
|
minWidth: 0,
|
|
zIndex: valueDisplay === "high" ? 1 : 0,
|
|
background:
|
|
valueDisplay === "high" ? "gold" : darken(theme.palette.background.paper, 0.2),
|
|
borderRadius: 10
|
|
}}>
|
|
<Typography
|
|
align="center"
|
|
noWrap
|
|
style={{
|
|
fontWeight: 650,
|
|
fontSize: "0.7rem",
|
|
color: valueDisplay === "high" ? "black" : "white"
|
|
}}>
|
|
High
|
|
</Typography>
|
|
</Button>
|
|
</Grid>
|
|
</Grid>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const plenumOverview = () => {
|
|
return (
|
|
<Box style={{ position: "absolute", bottom: 5, width: "100%" }}>
|
|
<Box display="flex" flexDirection="row" alignItems="center">
|
|
<Typography style={{ fontSize: isMobile ? "0.85rem" : "1.0rem", fontWeight: 650 }}>
|
|
Plenum {plenums && plenums?.length > 1 ? activePlenumIndex+1 : ""}
|
|
</Typography>
|
|
{plenums && plenums.length > 1 &&
|
|
<IconButton sx={{background: darken(theme.palette.background.paper, 0.1), marginLeft: 1}} size="small" onClick={()=>{
|
|
let newIndex = activePlenumIndex + 1
|
|
if(newIndex > combinedPlenums.length - 1){
|
|
newIndex = 0
|
|
}
|
|
setActivePlenum(combinedPlenums[newIndex])
|
|
setActivePlenumIndex(newIndex)
|
|
}}>
|
|
<ArrowForwardIos sx={{ fontSize: 15}}/>
|
|
</IconButton>
|
|
}
|
|
</Box>
|
|
<Box
|
|
className={classes.displayBoxBinLeft}
|
|
style={{ marginRight: isMobile ? -70 : -90 }}
|
|
position="relative">
|
|
{activePlenum?.tempHumidity ? (
|
|
<React.Fragment>
|
|
<Grid
|
|
container
|
|
alignContent="center"
|
|
alignItems="center"
|
|
direction="row"
|
|
style={{ paddingLeft: 5 }}>
|
|
<Grid size={{ xs: 2 }}>
|
|
<Avatar
|
|
variant="square"
|
|
src={GetComponentIcon(
|
|
quack.ComponentType.COMPONENT_TYPE_DHT,
|
|
undefined,
|
|
theme.palette.mode
|
|
)}
|
|
style={{ height: isMobile ? 20 : 25, width: isMobile ? 20 : 25 }}
|
|
/>
|
|
</Grid>
|
|
<Grid size={{ xs: 5 }}>
|
|
<Typography
|
|
align="center"
|
|
noWrap
|
|
style={{
|
|
fontSize: isMobile ? "0.85rem" : "1.0rem",
|
|
fontWeight: 650,
|
|
color: tempColour
|
|
}}>
|
|
{activePlenum.tempHumidity.getTempString(user.settings.temperatureUnit)}
|
|
</Typography>
|
|
</Grid>
|
|
<Grid size={{ xs: 5 }}>
|
|
<Typography
|
|
align="center"
|
|
noWrap
|
|
style={{
|
|
fontSize: isMobile ? "0.85rem" : "1.0rem",
|
|
fontWeight: 650,
|
|
color: humidColour
|
|
}}>
|
|
{activePlenum.tempHumidity.getHumidityString()}
|
|
</Typography>
|
|
</Grid>
|
|
</Grid>
|
|
{/* <Divider
|
|
style={{
|
|
marginLeft: theme.spacing(1),
|
|
marginTop: 5,
|
|
marginBottom: 5,
|
|
background: "gold",
|
|
marginRight: -40
|
|
}}
|
|
/> */}
|
|
{pressureOverview()}
|
|
</React.Fragment>
|
|
) : (
|
|
<Box flexDirection={"row"} display="flex">
|
|
<Avatar variant="square" className={classes.avatarIcon}>
|
|
<BindaptIcon />
|
|
</Avatar>
|
|
<Box textAlign="center" width={"85%"}>
|
|
<Typography>No Plenums found</Typography>
|
|
</Box>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const cfmDryWarning = (cfm: number) => {
|
|
if (cfm > 2) {
|
|
return (
|
|
<IconButton onClick={() => setCFMHighOpen(true)}>
|
|
<Error style={{ color: "yellow" }} />
|
|
</IconButton>
|
|
);
|
|
} else if (cfm < 1) {
|
|
return (
|
|
<IconButton onClick={() => setCFMLowOpen(true)}>
|
|
<Warning style={{ color: "red" }} />
|
|
</IconButton>
|
|
);
|
|
} else {
|
|
return (
|
|
<IconButton disabled>
|
|
<CheckCircleOutline style={{ color: "green" }} />
|
|
</IconButton>
|
|
);
|
|
}
|
|
};
|
|
|
|
const cfmHighDialog = () => {
|
|
return (
|
|
<ResponsiveDialog
|
|
open={cfmHighOpen}
|
|
onClose={() => {
|
|
setCFMHighOpen(false);
|
|
}}>
|
|
<DialogTitle>CFM Warning</DialogTitle>
|
|
<DialogContent>
|
|
<Typography>Your CFM seems abnormally high</Typography>
|
|
Your fan may have a loose connection
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button
|
|
color="primary"
|
|
onClick={() => {
|
|
setCFMHighOpen(false);
|
|
}}>
|
|
Close
|
|
</Button>
|
|
</DialogActions>
|
|
</ResponsiveDialog>
|
|
);
|
|
};
|
|
|
|
const cfmLowDialog = () => {
|
|
return (
|
|
<ResponsiveDialog
|
|
open={cfmLowOpen}
|
|
onClose={() => {
|
|
setCFMLowOpen(false);
|
|
}}>
|
|
<DialogTitle>CFM Warning</DialogTitle>
|
|
<DialogContent>
|
|
<Typography>Your CFM is low</Typography>
|
|
Your bin may be too full for your fan, try reducing inventory.
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button
|
|
color="primary"
|
|
onClick={() => {
|
|
setCFMLowOpen(false);
|
|
}}>
|
|
Close
|
|
</Button>
|
|
</DialogActions>
|
|
</ResponsiveDialog>
|
|
);
|
|
};
|
|
|
|
const pressureOverview = () => {
|
|
return (
|
|
<Grid
|
|
container
|
|
alignContent="center"
|
|
alignItems="center"
|
|
direction="row"
|
|
style={{ paddingLeft: 5, marginTop: 4 }}>
|
|
<Grid size={{ xs: 2 }}>
|
|
<Avatar
|
|
variant="square"
|
|
src={GetComponentIcon(
|
|
quack.ComponentType.COMPONENT_TYPE_PRESSURE,
|
|
undefined,
|
|
theme.palette.mode
|
|
)}
|
|
style={{ height: isMobile ? 20 : 25, width: isMobile ? 20 : 25 }}
|
|
/>
|
|
</Grid>
|
|
{activePlenum?.pressure ? (
|
|
<React.Fragment>
|
|
<Grid size={{ xs: 5 }}>
|
|
<Typography
|
|
align="center"
|
|
noWrap
|
|
style={{
|
|
fontSize: isMobile ? "0.85rem" : "1.0rem",
|
|
fontWeight: 650,
|
|
color: pressColour
|
|
}}>
|
|
{activePlenum.pressure.getPressureString(user.settings.pressureUnit)}
|
|
</Typography>
|
|
</Grid>
|
|
{/* <Grid item xs={5}>
|
|
<Typography
|
|
align="center"
|
|
noWrap
|
|
style={{
|
|
fontSize: isMobile ? "0.85rem" : "1.0rem",
|
|
fontWeight: 650,
|
|
color: pressColour
|
|
}}>
|
|
{bin.status.fanCfm.toFixed(0) + "CFM"}
|
|
</Typography>
|
|
</Grid> */}
|
|
</React.Fragment>
|
|
) : (
|
|
<React.Fragment>
|
|
<Grid size={{ xs: 10 }}>
|
|
<Typography
|
|
color="textSecondary"
|
|
style={{ fontSize: isMobile ? "0.85rem" : "1.0rem", fontWeight: 650 }}>
|
|
N/A
|
|
</Typography>
|
|
</Grid>
|
|
</React.Fragment>
|
|
)}
|
|
</Grid>
|
|
);
|
|
};
|
|
|
|
const overview = () => {
|
|
return (
|
|
<React.Fragment>
|
|
{inventoryOverview()}
|
|
{grainCableOverview()}
|
|
{plenumOverview()}
|
|
</React.Fragment>
|
|
);
|
|
};
|
|
|
|
const getBinSVGHeight = () => {
|
|
if (isMobile) {
|
|
return fullScreenHandler.active ? viewport.height * 0.8 : viewport.width * 0.8;
|
|
}
|
|
return fullScreenHandler.active ? viewport.height * 0.8 : viewport.height * 0.4;
|
|
};
|
|
|
|
const visual = () => {
|
|
let diffDisplay = grainDiff;
|
|
let pendingDisplay = pendingGrainAmount;
|
|
if (bin.storage() === pond.BinStorage.BIN_STORAGE_FERTILIZER) {
|
|
diffDisplay = diffDisplay * 35.239;
|
|
if (pendingDisplay) {
|
|
pendingDisplay = pendingDisplay * 35.239;
|
|
}
|
|
}
|
|
|
|
if (user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE && bin.bushelsPerTonne() > 1) {
|
|
diffDisplay = diffDisplay / bin.bushelsPerTonne();
|
|
if (pendingDisplay) {
|
|
pendingDisplay = pendingDisplay / bin.bushelsPerTonne();
|
|
}
|
|
}
|
|
|
|
if (user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TON && bin.bushelsPerTonne() > 1) {
|
|
diffDisplay = diffDisplay / (bin.bushelsPerTonne()*0.907);
|
|
if (pendingDisplay) {
|
|
pendingDisplay = pendingDisplay / (bin.bushelsPerTonne()*0.907);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Box display="flex" width={1} justifyContent="flex-end">
|
|
<FullScreenWrapper>
|
|
<Box
|
|
position="relative"
|
|
height={1}
|
|
width={1}
|
|
display="flex"
|
|
justifyContent="center"
|
|
alignItems="center"
|
|
alignContent="flex-end">
|
|
{fullScreenHandler.active && (
|
|
<Box position="absolute" bottom={10} right={10}>
|
|
<IconButton onClick={fullScreenHandler.exit}>
|
|
<FullscreenExitIcon />
|
|
</IconButton>
|
|
</Box>
|
|
)}
|
|
<Box zIndex={5}>
|
|
<Box>
|
|
{grainDiff > 0 && (
|
|
<div
|
|
className={classes.grainOVerlay}
|
|
style={{
|
|
top: bin.binShape() === pond.BinShape.BIN_SHAPE_HOPPER_BOTTOM ? "30%" : "35%",
|
|
color: "#5CE422"
|
|
}}>
|
|
<Typography variant={"h5"} style={{ fontWeight: 750 }}>
|
|
+{diffDisplay.toLocaleString()}
|
|
</Typography>
|
|
</div>
|
|
)}
|
|
{pendingDisplay !== undefined && (
|
|
<div
|
|
className={classes.grainOVerlay}
|
|
style={{
|
|
top: bin.binShape() === pond.BinShape.BIN_SHAPE_HOPPER_BOTTOM ? "40%" : "45%"
|
|
}}>
|
|
<Typography variant={"h4"} style={{ fontWeight: 750 }}>
|
|
{pendingDisplay.toLocaleString()}
|
|
</Typography>
|
|
</div>
|
|
)}
|
|
{grainDiff < 0 && (
|
|
<div
|
|
className={classes.grainOVerlay}
|
|
style={{
|
|
top: bin.binShape() === pond.BinShape.BIN_SHAPE_HOPPER_BOTTOM ? "50%" : "55%",
|
|
color: "#E42222"
|
|
}}>
|
|
<Typography variant={"h5"} style={{ fontWeight: 750 }}>
|
|
{diffDisplay.toLocaleString()}
|
|
</Typography>
|
|
</div>
|
|
)}
|
|
</Box>
|
|
<BinSVGV2
|
|
height={getBinSVGHeight()}
|
|
hasMinHeight
|
|
binShape={bin.settings.specs?.shape}
|
|
fillPercentage={fillPercentage ?? 0}
|
|
lidarEstimate={lidarPercentage}
|
|
cables={grainCables}
|
|
co2Sensors={bin.status.co2}
|
|
highTemp={bin.settings.highTemp}
|
|
lowTemp={bin.settings.lowTemp}
|
|
showTempHum={iOS ? nodeDetails : fullScreenHandler.active}
|
|
grainType={bin.settings.inventory?.grainType}
|
|
isFullScreen={fullScreenHandler.active}
|
|
cableEstimate={bin.settings.autoGrainNode}
|
|
hottestNodeTemp={valueDisplay === "high" ? highNodeConditions?.tempC : undefined}
|
|
coldestNodeTemp={valueDisplay === "low" ? lowNodeConditions?.tempC : undefined}
|
|
cableNodeClicked={(cable, fillNode) => {
|
|
if (cable) {
|
|
let devId = componentDevices.get(cable.key());
|
|
if (devId) {
|
|
let device = devMap.get(devId);
|
|
if (device) {
|
|
setSelectedCable(cable);
|
|
setSelectedNode(fillNode);
|
|
setCableDevice(device);
|
|
//need to set the filtered components so that components on other devices do not show up as options in interaction settings
|
|
let filtered: Component[] = []
|
|
components.forEach((component, key) => {
|
|
if(componentDevices.get(key) === devId){
|
|
filtered.push(component)
|
|
}
|
|
})
|
|
setFilteredComponents(filtered)
|
|
setOpenNodeDialog(true);
|
|
}
|
|
}
|
|
}
|
|
}}
|
|
inventoryControl={bin.inventoryControl()}
|
|
customGrain={bin.customGrain()}
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
</FullScreenWrapper>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const valueLabel = (value: number) => {
|
|
return (
|
|
<Box display="flex" sx={{background: sliderColour, height: 30, width: 30, borderRadius: 20}}>
|
|
<Typography sx={{fontSize: 12, fontWeight: 650, color: "black", textAlign: "center", margin: "auto"}}>{value}%</Typography>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
const controls = () => {
|
|
if (bin.settings.inventory?.empty === true) return null;
|
|
const canEdit = permissions ? permissions.includes(pond.Permission.PERMISSION_WRITE) : false;
|
|
return (
|
|
<Box
|
|
height={1}
|
|
width={1}
|
|
display="flex"
|
|
flexDirection="column"
|
|
justifyContent="flex-end"
|
|
alignContent="flex-end">
|
|
{(bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_MANUAL ||
|
|
bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_UNKNOWN) && (
|
|
<React.Fragment>
|
|
<Box display="flex" height={isMobile ? 35 : 40} marginBottom={1}>
|
|
<IconButton
|
|
disabled={!canEdit}
|
|
onClick={() => {
|
|
setGrainUpdate(true);
|
|
}}
|
|
style={{
|
|
margin: "auto",
|
|
backgroundColor: canEdit ? "gold" : "grey",
|
|
color: "black",
|
|
height: "100%",
|
|
width: isMobile ? 35 : 40
|
|
}}>
|
|
<Typography sx={{fontSize: isMobile ? 17 : 20, fontWeight: 650}}>+/-</Typography>
|
|
</IconButton>
|
|
</Box>
|
|
<Box
|
|
height={0.7}
|
|
width={1}
|
|
display="flex"
|
|
justifyContent="center"
|
|
alignContent="flex-end">
|
|
{fillPercentage !== null && (
|
|
<Slider
|
|
disabled={!canEdit}
|
|
orientation="vertical"
|
|
value={fillPercentage}
|
|
classes={{
|
|
// root: classes.sliderRoot,
|
|
// rail: classes.sliderRail,
|
|
// track: classes.sliderTrack,
|
|
// thumb: isMobile ? classes.mobileSliderThumb : classes.sliderThumb,
|
|
valueLabel: classes.sliderValLabel,
|
|
}}
|
|
style={{ height: "100%", color: canEdit ? sliderColour : "gray" }}
|
|
min={0}
|
|
max={100}
|
|
valueLabelDisplay="auto"
|
|
valueLabelFormat={valueLabel}
|
|
onChange={(_, value) => {
|
|
setFillPercentage(value as number);
|
|
const capacity = bin.settings.specs ? bin.settings.specs.bushelCapacity : 0;
|
|
const current = bin.bushels()
|
|
let grainAmount = ((value as number) / 100) * capacity;
|
|
|
|
if (grainAmount < current) {
|
|
setSliderCoulour("#E42222");
|
|
} else if (grainAmount > current) {
|
|
setSliderCoulour("#5CE422");
|
|
} else {
|
|
setSliderCoulour("gold");
|
|
}
|
|
setGrainDiff(round(grainAmount - current, 0));
|
|
setPendingGrainAmount(round(grainAmount, 0));
|
|
}}
|
|
onChangeCommitted={() => {
|
|
setGrainUpdate(true);
|
|
}}
|
|
aria-labelledby="grain-amount"
|
|
/>
|
|
)}
|
|
</Box>
|
|
</React.Fragment>
|
|
)}
|
|
<Box height={0.1} width={1} />
|
|
<Box width={1} textAlign="center">
|
|
{iOS ? (
|
|
<IconButton size="small" onClick={() => setNodeDetails(!nodeDetails)}>
|
|
<InfoOutlined />
|
|
</IconButton>
|
|
) : (
|
|
<IconButton size="small" onClick={() => fullScreenHandler.enter()}>
|
|
<FullscreenIcon />
|
|
</IconButton>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const controllerDisplay = () => {
|
|
return (
|
|
<Box style={{ marginTop: 5 }}>
|
|
<Typography style={{ fontSize: isMobile ? "0.85rem" : "1.0rem", fontWeight: 650 }}>
|
|
Controllers
|
|
</Typography>
|
|
<Box className={classes.displayBox}>
|
|
{bin.status.fans.map(fan => {
|
|
let fanComp = components.get(fan.key)
|
|
let device = componentDevices.get(fan.key)
|
|
if(fanComp && device){
|
|
return (
|
|
<BinControllerDisplay key={fanComp.key()} component={fanComp} deviceID={device} currentState={fan.state}/>
|
|
)
|
|
}
|
|
})}
|
|
{bin.status.heaters.map(heater => {
|
|
let heaterComp = components.get(heater.key)
|
|
let device = componentDevices.get(heater.key)
|
|
if(heaterComp && device){
|
|
return (
|
|
<BinControllerDisplay key={heaterComp.key()} component={heaterComp} deviceID={device} currentState={heater.state}/>
|
|
)
|
|
}
|
|
})}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const fanPerformance = () => {
|
|
let totalCFM = bin.status.fanCfm;
|
|
let bushelCFM = bin.status.cfmPerBushel;
|
|
let emc = ExtractMoisture(
|
|
bin.grain(),
|
|
activePlenum?.tempHumidity?.temperature ?? 0,
|
|
activePlenum?.tempHumidity?.humidity ?? 0,
|
|
bin.customGrain()
|
|
)
|
|
return (
|
|
<Box>
|
|
<Typography style={{ fontSize: isMobile ? "0.85rem" : "1.0rem", fontWeight: 650 }}>
|
|
Fan Performance
|
|
</Typography>
|
|
{activePlenum?.pressure && activePlenum.pressure.fanId === 0 && !bin.settings.fan?.type && bin.fanID() === 0 && (
|
|
<Typography variant="subtitle2" color="textPrimary" style={{ fontWeight: 600 }}>
|
|
No fans found to calculate CFM
|
|
</Typography>
|
|
)}
|
|
<Box className={classes.displayBox}>
|
|
<Grid
|
|
container
|
|
direction="row"
|
|
alignContent="center"
|
|
alignItems="center"
|
|
justifyContent="space-between">
|
|
<Grid >
|
|
<Box display="flex">
|
|
<Box>
|
|
<Typography
|
|
align="center"
|
|
style={{
|
|
marginLeft: 10,
|
|
fontWeight: 650,
|
|
color: pressColour
|
|
}}>
|
|
{totalCFM.toFixed(0) + " CFM"}
|
|
</Typography>
|
|
<Typography
|
|
align="center"
|
|
style={{
|
|
marginLeft: 10,
|
|
fontWeight: 650,
|
|
color: pressColour
|
|
}}>
|
|
{bushelCFM.toFixed(2) + " CFM/bu"}
|
|
</Typography>
|
|
</Box>
|
|
{cfmDryWarning(bushelCFM)}
|
|
</Box>
|
|
</Grid>
|
|
<Grid>
|
|
{emc !== activePlenum?.tempHumidity?.humidity &&
|
|
<Box className={classes.lightBox}>
|
|
<Typography align="center" style={{ fontWeight: 650, color: emcColour }}>
|
|
{emc.toFixed(2)}
|
|
%
|
|
</Typography>
|
|
<Typography align="center" style={{ fontWeight: 650 }}>
|
|
Plenum EMC
|
|
</Typography>
|
|
</Box>
|
|
}
|
|
</Grid>
|
|
</Grid>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const ambientDisplay = () => {
|
|
let emc = ExtractMoisture(
|
|
bin.grain(),
|
|
ambient?.temperature ?? 0,
|
|
ambient?.humidity ?? 0,
|
|
bin.customGrain()
|
|
)
|
|
return (
|
|
<Box>
|
|
<Typography style={{ fontSize: isMobile ? "0.85rem" : "1.0rem", fontWeight: 650 }}>
|
|
Ambient
|
|
</Typography>
|
|
<Box className={classes.displayBox}>
|
|
<Grid
|
|
container
|
|
direction="row"
|
|
alignContent="center"
|
|
alignItems="center"
|
|
justifyContent="space-between">
|
|
<Grid >
|
|
<Box display="flex" marginY={1}>
|
|
<TemperatureIcon />
|
|
<Typography
|
|
style={{
|
|
marginLeft: 10,
|
|
fontWeight: 650,
|
|
color: tempColour
|
|
}}>
|
|
{ambient?.getTempString(user.tempUnit())}
|
|
</Typography>
|
|
</Box>
|
|
<Box display="flex" marginY={1}>
|
|
<Box style={{ width: 24, paddingLeft: 6 }}>
|
|
<HumidityIcon height={24} width={15} />
|
|
</Box>
|
|
<Typography
|
|
style={{
|
|
marginLeft: 10,
|
|
fontWeight: 650,
|
|
color: humidColour
|
|
}}>
|
|
{ambient?.getHumidityString()}
|
|
</Typography>
|
|
</Box>
|
|
</Grid>
|
|
<Grid >
|
|
{emc !== ambient?.humidity &&
|
|
<Box className={classes.lightBox}>
|
|
<Typography align="center" style={{ fontWeight: 650, color: emcColour }}>
|
|
{emc.toFixed(2)}
|
|
%
|
|
</Typography>
|
|
<Typography align="center" style={{ fontWeight: 650 }}>
|
|
Ambient EMC
|
|
</Typography>
|
|
</Box>
|
|
}
|
|
</Grid>
|
|
</Grid>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const determineToggle = (binMode: pond.BinMode) => {
|
|
switch (binMode) {
|
|
case pond.BinMode.BIN_MODE_STORAGE:
|
|
return [0]
|
|
case pond.BinMode.BIN_MODE_COOLDOWN:
|
|
return [1]
|
|
case pond.BinMode.BIN_MODE_DRYING:
|
|
case pond.BinMode.BIN_MODE_HYDRATING:
|
|
return [2]
|
|
}
|
|
}
|
|
|
|
const binMode = () => {
|
|
const mode = bin.settings.mode;
|
|
return (
|
|
<Box
|
|
display="flex"
|
|
marginBottom={1}
|
|
flexGrow={1}
|
|
height={"100%"}
|
|
width={1}
|
|
justifyContent="space-between"
|
|
alignItems="center"
|
|
alignSelf="flex-end">
|
|
<Typography variant="subtitle1" style={{ fontWeight: 800 }}>
|
|
Bin Mode
|
|
</Typography>
|
|
{modeChangeInProgress &&
|
|
<CircularProgress color="primary" size={25}/>
|
|
}
|
|
<Box id="tour-bin-mode">
|
|
<ButtonGroup
|
|
disableAll={modeChangeInProgress}
|
|
buttons={[
|
|
{
|
|
title: "Storage",
|
|
function: () => {
|
|
setModeStorage()
|
|
}
|
|
},
|
|
{
|
|
title: "Cooldown",
|
|
function: () => {
|
|
setModeCooldown()
|
|
}
|
|
},
|
|
{
|
|
title: bin.settings.inventory && bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture ? "Hydrating" : "Drying",
|
|
function: () => {
|
|
// setShowInputMoisture(true)
|
|
setModeConditioning() //will set it to either drying or hydrating based on the initial and target moisture
|
|
}
|
|
}
|
|
]}
|
|
toggledButtons={determineToggle(mode)}
|
|
toggle
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const showDays = (time: Moment) => {
|
|
let now = moment();
|
|
let duration = moment.duration(time.diff(now));
|
|
let days = duration.asDays();
|
|
days = Math.abs(days);
|
|
duration.subtract(moment.duration(days, "days"));
|
|
let hours = duration.hours();
|
|
hours = Math.abs(hours);
|
|
|
|
if (bin.settings.mode === pond.BinMode.BIN_MODE_NONE) {
|
|
return (
|
|
<Typography variant="subtitle2" className={classes.days}>
|
|
Select Mode
|
|
</Typography>
|
|
);
|
|
}
|
|
|
|
if (days > 50 && bin.settings.mode === pond.BinMode.BIN_MODE_DRYING) {
|
|
return (
|
|
<Typography variant="subtitle2" className={classes.days}>
|
|
Calculating...
|
|
</Typography>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Typography variant="subtitle2" className={classes.days} style={{ fontWeight: 700 }}>
|
|
{Math.floor(days)}d {hours}h
|
|
</Typography>
|
|
);
|
|
};
|
|
|
|
const updateStorageTime = () => {
|
|
let status = bin.status;
|
|
let newTimestamp = storageDate + " " + storageTime;
|
|
setModeTime(moment(newTimestamp));
|
|
status.lastModeChange = newTimestamp;
|
|
binAPI
|
|
.updateBinStatus(bin.key(), status, as)
|
|
.then(resp => {
|
|
openSnack("Reset the storage time");
|
|
})
|
|
.catch(err => {
|
|
openSnack("There was a problem resetting the storage time");
|
|
});
|
|
};
|
|
|
|
const storageTimeDialog = () => {
|
|
return (
|
|
<ResponsiveDialog open={openStorageTime}>
|
|
<DialogTitle>Set Storage Time</DialogTitle>
|
|
<DialogContent>
|
|
<Typography>Set the date and time that the grain was initially stored</Typography>
|
|
<TextField
|
|
//style={{ width: "45%" }}
|
|
fullWidth
|
|
margin="normal"
|
|
type="date"
|
|
label="Storage Date"
|
|
value={storageDate}
|
|
onChange={e => setStorageDate(e.target.value)}
|
|
/>
|
|
<TextField
|
|
//style={{ width: "45%", marginLeft: "10%" }}
|
|
fullWidth
|
|
margin="normal"
|
|
type="time"
|
|
label="Storage Time"
|
|
value={storageTime}
|
|
onChange={e => setStorageTime(e.target.value)}
|
|
/>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Box display="flex" justifyContent="space-between">
|
|
<Button
|
|
onClick={() => {
|
|
setOpenStorageTime(false);
|
|
}}>
|
|
Close
|
|
</Button>
|
|
<Button
|
|
variant="contained"
|
|
color="primary"
|
|
onClick={() => {
|
|
updateStorageTime();
|
|
setOpenStorageTime(false);
|
|
}}>
|
|
Confirm
|
|
</Button>
|
|
</Box>
|
|
</DialogActions>
|
|
</ResponsiveDialog>
|
|
);
|
|
};
|
|
|
|
const dryingEstimate = () => {
|
|
if (
|
|
bin.settings.mode === pond.BinMode.BIN_MODE_DRYING ||
|
|
bin.settings.mode === pond.BinMode.BIN_MODE_HYDRATING
|
|
) {
|
|
let time = moment(bin.status.targetMoistureEstimation);
|
|
return (
|
|
<Box className={classes.extraDetails}>
|
|
<Box style={{ position: "relative", transform: "translate(50%, 25%)" }}>
|
|
<IconButton style={{ position: "absolute" }} size="small" onClick={() => refresh(true)}>
|
|
<RefreshIcon />
|
|
</IconButton>
|
|
</Box>
|
|
<Box padding={0} style={{ flex: 1, textAlign: "center" }}>
|
|
<Typography variant="body2" style={{ margin: theme.spacing(0.5) }}>
|
|
{bin.settings.mode === pond.BinMode.BIN_MODE_DRYING ? "Drying" : "Hydrating"} Time
|
|
Estimate:
|
|
</Typography>
|
|
|
|
{showDays(time)}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|
|
if (bin.settings.mode === pond.BinMode.BIN_MODE_COOLDOWN) {
|
|
return (
|
|
<Box className={classes.extraDetails}>
|
|
<Box style={{ position: "relative", transform: "translate(50%, 25%)" }}>
|
|
<IconButton style={{ position: "absolute" }} size="small" onClick={() => refresh(true)}>
|
|
<RefreshIcon />
|
|
</IconButton>
|
|
</Box>
|
|
<Box padding={0} style={{ flex: 1, textAlign: "center" }}>
|
|
<Typography variant="body2" style={{ margin: theme.spacing(0.5) }}>
|
|
Cooling:
|
|
</Typography>
|
|
{showDays(modeTime)}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|
|
return (
|
|
<Box className={classes.extraDetails}>
|
|
<Box style={{ position: "relative", transform: "translate(0%, 25%)" }}>
|
|
<IconButton style={{ position: "absolute" }} size="small" onClick={() => refresh(true)}>
|
|
<RefreshIcon />
|
|
</IconButton>
|
|
</Box>
|
|
<Box padding={0} style={{ flex: 1, textAlign: "center" }}>
|
|
<Typography variant="body2" style={{ margin: theme.spacing(0.5) }}>
|
|
{bin.settings.mode === pond.BinMode.BIN_MODE_STORAGE ? "In storage for:" : "^"}
|
|
</Typography>
|
|
{showDays(modeTime)}
|
|
</Box>
|
|
<Box style={{ position: "relative", transform: "translate(0%, 25%)" }}>
|
|
<IconButton
|
|
style={{ position: "absolute", right: 1 }}
|
|
size="small"
|
|
onClick={() => {
|
|
setOpenStorageTime(true);
|
|
}}>
|
|
<AccessTime />
|
|
</IconButton>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const setModeStorage = () => {
|
|
// setNewPreset(pond.BinMode.BIN_MODE_STORAGE);
|
|
setNewBinMode(pond.BinMode.BIN_MODE_STORAGE);
|
|
setOpenModeChange(true)
|
|
};
|
|
|
|
const setModeCooldown = () => {
|
|
if (bin.settings.mode === pond.BinMode.BIN_MODE_COOLDOWN) {
|
|
return;
|
|
}
|
|
// setNewPreset(pond.BinMode.BIN_MODE_COOLDOWN);
|
|
setNewBinMode(pond.BinMode.BIN_MODE_COOLDOWN);
|
|
setOpenModeChange(true)
|
|
};
|
|
|
|
const setModeConditioning = () => {
|
|
if (
|
|
bin.settings.inventory &&
|
|
bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture
|
|
) {
|
|
// setNewPreset(pond.BinMode.BIN_MODE_HYDRATING);
|
|
setNewBinMode(pond.BinMode.BIN_MODE_HYDRATING);
|
|
} else {
|
|
// setNewPreset(pond.BinMode.BIN_MODE_DRYING);
|
|
setNewBinMode(pond.BinMode.BIN_MODE_DRYING);
|
|
}
|
|
setOpenModeChange(true)
|
|
};
|
|
|
|
if (loading) {
|
|
return <Skeleton variant="rectangular" height={460} />;
|
|
}
|
|
|
|
const updateBin = () => {
|
|
let b = bin;
|
|
b.settings.storage = storageType;
|
|
if (b.settings.inventory) {
|
|
if (b.settings.inventory.initialMoisture)
|
|
b.settings.inventory.initialMoisture = targetMoisture;
|
|
//update all the grain stuff
|
|
b.settings.inventory.grainType = grainType;
|
|
b.settings.inventory.customTypeName = customTypeName;
|
|
b.settings.inventory.bushelsPerTonne = GrainDescriber(grainType).bushelsPerTonne
|
|
// b.settings.inventory.bushelsPerTonne = isNaN(parseFloat(bushPerTonne))
|
|
// ? 0
|
|
// : parseFloat(bushPerTonne);
|
|
b.settings.inventory.grainSubtype = grainSubtype;
|
|
b.settings.inventory.customGrain = newGrainSettings
|
|
}
|
|
|
|
if (b.settings.outdoorTemp !== undefined) {
|
|
let t = outdoorTemp;
|
|
if (user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
|
t = FtoC(outdoorTemp);
|
|
}
|
|
b.settings.outdoorTemp = t;
|
|
}
|
|
if (b.settings.outdoorHumidity !== undefined)
|
|
b.settings.outdoorHumidity = outdoorHumidity;
|
|
|
|
if (b.settings.mode != newBinMode){
|
|
if(b.settings.inventory){
|
|
b.settings.inventory.initialTimestamp = moment().format();
|
|
}
|
|
b.settings.mode = newBinMode;
|
|
}
|
|
binAPI.updateBin(bin.key(), b.settings, as).then(resp => {
|
|
refresh();
|
|
});
|
|
};
|
|
|
|
return (
|
|
<Card raised className={classes.cardContent}>
|
|
{cfmLowDialog()}
|
|
{cfmHighDialog()}
|
|
{changeGrain()}
|
|
{storageTimeDialog()}
|
|
<ModeChangeDialog
|
|
binKey={bin.key()}
|
|
binMode={newBinMode}
|
|
grain={bin.settings.inventory?.grainType}
|
|
devices={devices}
|
|
open={openModeChange}
|
|
binCables={cables}
|
|
presets={binPresets}
|
|
compDevMap={componentDevices}
|
|
preferences={preferences}
|
|
onClose={(refresh) => {
|
|
setOpenModeChange(false)
|
|
if(refresh) updateBin();
|
|
}}
|
|
defaultTargetMoisture={bin.settings.inventory?.initialMoisture}
|
|
defaultOutdoorTemp={outdoorTemp}
|
|
defaultOutdoorHumidity={outdoorHumidity}
|
|
changeTargetMoisture={newMoisture => {
|
|
setTargetMoisture(newMoisture)
|
|
}}
|
|
changeOutdoorTemp={newTemp => {
|
|
setOutdoorTemp(newTemp)
|
|
}}
|
|
changeOutdoorHumidity={newHumidity => {
|
|
setOutdoorHumidity(newHumidity)
|
|
}}
|
|
startChange={() => {
|
|
setModeChangeInProgress(true)
|
|
}}
|
|
changeComplete={() => {
|
|
setModeChangeInProgress(false)
|
|
}}
|
|
|
|
/>
|
|
<Box paddingRight={1}>
|
|
{binMode()}
|
|
<Grid container justifyContent="flex-start" alignItems="stretch">
|
|
<Grid size={5.5} style={{ position: "relative" }}>
|
|
{overview()}
|
|
</Grid>
|
|
<Grid size={5} style={{ position: "relative" }}>
|
|
{visual()}
|
|
</Grid>
|
|
<Grid size={1.5} >{controls()}</Grid>
|
|
</Grid>
|
|
{plenums && pressures && fanPerformance()}
|
|
{ambient && ambientDisplay()}
|
|
{(bin.status.fans.length > 0 || bin.status.heaters.length > 0) && controllerDisplay()}
|
|
{dryingEstimate()}
|
|
</Box>
|
|
<GrainTransaction
|
|
open={grainUpdate}
|
|
mainObject={bin}
|
|
grainAdjustment={grainDiff}
|
|
close={() => {
|
|
setGrainUpdate(false);
|
|
setPendingGrainAmount(undefined);
|
|
resetFillPercentage();
|
|
setGrainDiff(0);
|
|
}}
|
|
callback={props.afterUpdate}
|
|
/>
|
|
{selectedCable && cableDevice && (
|
|
<GrainNodeInteractions
|
|
binKey={bin.key()}
|
|
interactionComponents={filteredComponents}
|
|
grain={bin.grain()}
|
|
cable={selectedCable}
|
|
open={openNodeDialog}
|
|
device={cableDevice}
|
|
selectedNode={selectedNode}
|
|
close={() => {
|
|
setOpenNodeDialog(false);
|
|
}}
|
|
permissions={permissions ?? []}
|
|
updateComponentCallback={updateComponentCallback}
|
|
/>
|
|
)}
|
|
<div style={{ bottom: "0px" }}></div>
|
|
</Card>
|
|
);
|
|
}
|