frontend/src/bin/BinSettings.tsx

2532 lines
95 KiB
TypeScript

import {
Accordion,
AccordionDetails,
AccordionSummary,
AppBar,
Box,
Button,
Checkbox,
Collapse,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
FormControl,
FormControlLabel,
FormHelperText,
FormLabel,
Grid2 as Grid,
IconButton,
InputAdornment,
MobileStepper,
Radio,
RadioGroup,
Step,
StepLabel,
Stepper,
Switch,
Tab,
Tabs,
TextField,
Theme,
Toolbar,
Tooltip,
Typography,
useTheme
} from "@mui/material";
import { Close, ExpandMore, Info, KeyboardArrowLeft, KeyboardArrowRight } from "@mui/icons-material";
// import { getBinModel } from "common/DataImports/BinCables/BinCableImporter";
//import { jsonBin } from "common/DataImports/BinCables/BinCableImporter";
import DeleteButton from "common/DeleteButton";
import OutlinedBox from "common/OutlinedBox";
import ResponsiveDialog from "common/ResponsiveDialog";
import SearchSelect, { Option } from "common/SearchSelect";
import {
ConeVolume,
CylinderVolume,
RadiansToDegrees,
TriangleOppositeLength
} from "common/TrigFunctions";
import FanPicker from "fans/fanPicker";
import { GrainOptions, ToGrainOption } from "grain";
import GrainDescriber from "grain/GrainDescriber";
import GrainTransaction from "grain/GrainTransaction";
import { GetGrainUseOptions, ToGrainUseOption } from "grain/GrainUse";
import { useMobile, useSnackbar } from "hooks";
import { clone, cloneDeep } from "lodash";
import { Bin } from "models";
import moment from "moment";
import { GetBinShapeDescribers } from "pbHelpers/Bin";
import { pond } from "protobuf-ts/pond";
import { useBinAPI, useBinYardAPI, useGlobalState, useLibraCartProxyAPI } from "providers";
import React, { useCallback, useEffect, useState } from "react";
// import { useHistory } from "react-router";
import { getDistanceUnit, or, getTemperatureUnit, getGrainUnit } from "utils";
import { makeStyles } from "@mui/styles";
import { useNavigate } from "react-router-dom";
import BinSelector from "./BinSelector";
import CustomGrainSelector from "grain/CustomGrainSelector";
// import BinSelector from "./BinSelector";
const useStyles = makeStyles((theme: Theme) => {
return ({
stepper: {
padding: theme.spacing(0.5)
},
secondaryColor: {
color: theme.palette.secondary.main
},
textSecondaryColor: {
color: theme.palette.text.secondary
},
bottomSpacing: {
marginBottom: theme.spacing(1)
},
tabs: {
width: "100%",
boxSizing: "border-box",
flexShrink: 1
},
tabSmall: {
width: "100%",
boxSizing: "border-box",
flexShrink: 1,
minWidth: "48px"
}
})
});
type Mode = "add" | "update" | "remove";
interface Props {
open: boolean;
onClose: (refresh?: boolean) => void;
mode?: Mode;
bin?: Bin;
canEdit: boolean;
openedBinYard?: string;
userID: string;
coords?: { longitude: number; latitude: number };
binYards?: pond.BinYardSettings[];
}
interface BinStep {
label: string;
}
const getSteps = (): BinStep[] => {
return [{ label: "General" }, { label: "Fan" }, { label: "Inventory" }, { label: "Thresholds" }];
};
interface BinFormExtension {
capacity: string;
height: string;
topConeHeight: string;
sidewallHeight: string;
hopperHeight: string;
diameter: string;
grainBushels: string;
grainTonnes: string;
grainType: Option | null;
grainUse: Option | null;
grainSubtype: string;
customTypeName: string;
highTemp: number;
lowTemp: number;
bushelsPerTonne: string;
tempTarget: number;
}
export default function BinSettings(props: Props) {
const classes = useStyles();
//the conversion constant to convert cubic feet to bushels
const conversionConstantFT = 0.7786;
const isMobile = useMobile();
const theme = useTheme();
// const history = useHistory();
const navigate = useNavigate();
const binAPI = useBinAPI();
const binYardAPI = useBinYardAPI();
const { openSnack } = useSnackbar();
const { open, bin, onClose, mode, canEdit, openedBinYard, userID, coords } = props;
const steps = getSteps();
const [activeStep, setActiveStep] = useState(0);
const [form, setForm] = useState<pond.BinSettings>(pond.BinSettings.create());
const [formExtension, setFormExtension] = useState<BinFormExtension>({
capacity: "",
height: "",
topConeHeight: "",
sidewallHeight: "",
hopperHeight: "",
diameter: "",
grainBushels: "",
grainTonnes: "",
grainType: null,
grainUse: null,
grainSubtype: "",
customTypeName: "",
highTemp: 20,
lowTemp: 10,
bushelsPerTonne: "",
tempTarget: 15
});
const [initialized, setInitialized] = useState(false);
const [removeIsOpen, setRemoveIsOpen] = useState(false);
const [binYardOptions, setBinYardOptions] = useState<Option[] | null>(null);
const [selectedBinYard, setSelectedBinYard] = useState<Option | null>(null);
const grainOptions = GrainOptions();
const grainUseOptions = GetGrainUseOptions();
const [inputCapacity, setInputCapacity] = useState<string>("");
const [{ user, as }] = useGlobalState();
const [grainDiff, setGrainDiff] = useState(0);
const [isCustomInventory, setIsCustomInventory] = useState<boolean>(false);
const [grainUpdate, setGrainUpdate] = useState(false);
const [inMoistureString, setInMoistureString] = useState("0");
const [tMoistureString, setTMoistureString] = useState("0");
const [moistureTargetDeviation, setMoistureTargetDeviation] = useState("0");
const [autoFillThreshold, setAutoFillThreshold] = useState(5);
const [lidarDropDistance, setLidarDropDistance] = useState(0);
const [validInitial, setValidInitial] = useState(true);
const [validTarget, setValidTarget] = useState(true);
const [validDeviation, setValidDeviation] = useState(true);
const [highTempC, setHighTempC] = useState(20);
const [lowTempC, setLowTempC] = useState(10);
const [isCustomBin, setIsCustomBin] = useState(false);
const [binModelOps, setBinModelOptions] = useState<Option[]>([]);
const [selectedBinModel, setSelectedBinModel] = useState<Option | null>(null);
const [modelID, setModelID] = useState(0);
const [autoTopNode, setAutoTopNode] = useState(false);
const [inventoryControl, setInventoryControl] = useState<pond.BinInventoryControl>(
pond.BinInventoryControl.BIN_INVENTORY_CONTROL_UNKNOWN
);
const [storageType, setStorageType] = useState<pond.BinStorage>(
pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN
);
//libracart stuff
const libracartAPI = useLibraCartProxyAPI()
const [lcDestination, setlcDestination] = useState<Option | null>()
const [lcDestinationOptions, setlcDestinationOptions] = useState<Option[]>([])
const [customGrain, setCustomGrain] = useState<pond.GrainSettings>()
useEffect(() => {
if (open) {
if (bin && bin.settings.specs) {
setIsCustomBin(true);
setModelID(bin.settings.specs.modelId);
}
let initForm = pond.BinSettings.create(
bin
? bin.settings
: {
...{
specs: pond.BinSpecs.create({ shape: pond.BinShape.BIN_SHAPE_FLAT_BOTTOM }),
inventory: pond.BinInventory.create({ grainBushels: 0, targetTemperature: 15, autoThreshold: 5 }),
highTemp: 20,
lowTemp: 10
}
}
);
if (mode === "add") {
initForm.yardKey = or(openedBinYard, "");
}
if (initForm.inventory) {
setInventoryControl(initForm.inventory.inventoryControl);
//make sure bushels per tonne is set so bins that don't have it in the settings yet can still do it before they get updated properly
if (!initForm.inventory.bushelsPerTonne) {
initForm.inventory.bushelsPerTonne = GrainDescriber(
initForm.inventory.grainType
).bushelsPerTonne;
}
if (initForm.inventory.customGrain){
setCustomGrain(initForm.inventory.customGrain)
}
//if the target temp is not set (older bins) make it the midpoint of the high and low temps assuming they are set otherwise make it 15
if (!initForm.inventory.targetTemperature || initForm.inventory.targetTemperature) {
if (initForm.highTemp && initForm.lowTemp) {
initForm.inventory.targetTemperature = (initForm.lowTemp + initForm.highTemp) / 2;
} else {
initForm.inventory.targetTemperature = 15;
}
}
}
let h = "";
let d = "";
let topconeHeight = "";
let sidewallHeight = "";
let hopperHeight = "";
if (initForm.specs) {
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET) {
h = (initForm.specs.heightCm * 0.0328).toFixed(2);
d = (initForm.specs.diameterCm * 0.0328).toFixed(2);
if (initForm.specs.advancedDimensions) {
topconeHeight = (initForm.specs.advancedDimensions.topConeHeight * 0.0328).toFixed(2);
sidewallHeight = (initForm.specs.advancedDimensions.sidewallHeight * 0.0328).toFixed(2);
hopperHeight = (initForm.specs.advancedDimensions.hopperHeight * 0.0328).toFixed(2);
}
} else {
h = (initForm.specs.heightCm / 100).toFixed(2);
d = (initForm.specs.diameterCm / 100).toFixed(2);
if (initForm.specs.advancedDimensions) {
topconeHeight = (initForm.specs.advancedDimensions.topConeHeight / 100).toFixed(2);
sidewallHeight = (initForm.specs.advancedDimensions.sidewallHeight / 100).toFixed(2);
hopperHeight = (initForm.specs.advancedDimensions.hopperHeight / 100).toFixed(2);
}
}
}
let high = initForm.highTemp ?? 20;
let low = initForm.lowTemp ?? 10;
let target = initForm.inventory?.targetTemperature ?? 15;
if (getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
high = parseFloat((high * 1.8 + 32).toFixed(2));
low = parseFloat((low * 1.8 + 32).toFixed(2));
target = parseFloat((target * 1.8 + 32).toFixed(2));
}
let gb =
initForm.storage === pond.BinStorage.BIN_STORAGE_FERTILIZER
? initForm.inventory?.grainBushels
? initForm.inventory.grainBushels * 35.239
: 0
: initForm.inventory?.grainBushels ?? 0;
setForm(initForm);
setHighTempC(initForm.highTemp ?? 20);
setLowTempC(initForm.lowTemp ?? 10);
setFormExtension({
capacity: initForm.specs?.bushelCapacity.toString() ?? "",
height: h,
diameter: d,
grainBushels: gb.toFixed(2),
grainTonnes: initForm.inventory?.bushelsPerTonne
? (gb / initForm.inventory.bushelsPerTonne).toFixed(2)
: "",
grainType: initForm.inventory?.grainType
? ToGrainOption(initForm.inventory.grainType)
: null,
grainUse: initForm.inventory?.grainUse
? ToGrainUseOption(initForm.inventory.grainUse)
: null,
grainSubtype: initForm.inventory?.grainSubtype ?? "",
customTypeName: initForm.inventory?.customTypeName ?? "",
highTemp: high,
lowTemp: low,
tempTarget: target,
hopperHeight: hopperHeight,
topConeHeight: topconeHeight,
sidewallHeight: sidewallHeight,
bushelsPerTonne: initForm.inventory?.bushelsPerTonne.toString() ?? "0"
});
setInMoistureString(initForm.inventory?.initialMoisture.toString() ?? "0");
setTMoistureString(initForm.inventory?.targetMoisture.toString() ?? "0");
setMoistureTargetDeviation(initForm.inventory?.moistureTargetDeviation.toString() ?? "0");
setAutoFillThreshold(initForm.inventory?.autoThreshold ?? 5)
let dropDistance = initForm.inventory?.lidarDropCm ?? 0
if(getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET){
dropDistance = Math.round((dropDistance/30.48)*100)/100
}
setLidarDropDistance(dropDistance)
setActiveStep(0);
if (bin) {
setAutoTopNode(bin.settings.autoGrainNode);
setIsCustomInventory(bin.storage() !== pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN);
}
setStorageType(initForm.storage);
setInitialized(true);
} else {
setInitialized(false);
}
}, [bin, open, mode, openedBinYard]);
useEffect(() => {
if (mode === "remove") {
setRemoveIsOpen(true);
}
}, [mode]);
const loadBinYards = useCallback(() => {
if (props.binYards) {
let yards: Option[] = props.binYards.map(yard => {
let option = { value: yard.key, label: yard.name } as Option;
if (yard.key === form.yardKey) {
setSelectedBinYard(option);
}
return option;
});
let y: Option[] = [];
y.push({ value: "", label: "No Yard" } as Option);
y = yards.concat(y);
setBinYardOptions(y);
} else {
binYardAPI
.listBinYards(350, 0, "desc", as ? as : userID, undefined, undefined, as)
.then(resp => {
let data = resp.data.yard ? resp.data.yard : [];
let yards: Option[] = data.map((yard: pond.BinYard) => {
let option = { value: yard.settings?.key, label: yard.settings?.name } as Option;
if (yard.settings?.key === form.yardKey) {
setSelectedBinYard(option);
}
return option;
});
let y: Option[] = [];
y.push({ value: "", label: "No Yard" } as Option);
y = yards.concat(y);
setBinYardOptions(y);
})
.catch(() => {});
}
}, [binYardAPI, form.yardKey, userID, as, props.binYards]);
const loadLibraCartDestinations = useCallback(()=>{
if(inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART){
//load the destinations for the user/team they are viewing as
let options: Option[] = []
libracartAPI.listDestinations(0,0,undefined, as)
.then(resp=>{
//set the options for the search select
resp.data.destinations.forEach(d => {
let newOp: Option = {
label: d.name,
value: d.id
}
options.push(newOp)
//set the current option based on the key in the form
if (d.id === form.libracartDestinationKey){
setlcDestination(newOp)
}
})
setlcDestinationOptions(options)
}).catch(()=>{})
}
},[libracartAPI, inventoryControl, as, form.libracartDestinationKey])
useEffect(() => {
loadBinYards();
loadLibraCartDestinations();
}, [loadBinYards, loadLibraCartDestinations]);
useEffect(() => {
if (bin?.settings.specs?.bushelCapacity) {
setInputCapacity(bin.settings.specs.bushelCapacity.toFixed(0));
}
}, [bin]);
const close = (refresh?: boolean) => {
onClose(refresh);
};
const addBin = () => {
// If initial moisture is updated, so does the timestamp
if (form.inventory?.initialMoisture !== bin?.settings.inventory?.initialMoisture) {
if (form.inventory) form.inventory.initialTimestamp = moment().format();
}
if (form.inventory?.initialTimestamp === "") {
form.inventory.initialTimestamp = moment().format();
}
if (form.specs) {
form.specs.bushelCapacity = Number(inputCapacity);
if (form.specs.useAdvanced && form.specs.advancedDimensions) {
let adD = form.specs.advancedDimensions;
form.specs.heightCm = adD.hopperHeight + adD.sidewallHeight + adD.topConeHeight;
}
}
//if coordinates are passed in give the bin a location
if (coords) {
form.location = pond.Location.create({
longitude: coords.longitude,
latitude: coords.latitude
});
}
form.mode = pond.BinMode.BIN_MODE_STORAGE; //make sure bins are in storage mode when creating new bins
form.storage = storageType;
form.highTemp = highTempC;
form.lowTemp = lowTempC;
form.autoGrainNode = autoTopNode;
if (form.inventory) {
form.inventory.inventoryControl = inventoryControl
form.inventory.autoThreshold = autoFillThreshold
form.inventory.lidarDropCm = getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? lidarDropDistance * 30.48 : lidarDropDistance
if(form.inventory.grainType !== pond.Grain.GRAIN_CUSTOM){
form.inventory.customGrain = undefined
}else{
form.inventory.customGrain = customGrain
}
}
binAPI
.addBin(form, as)
.then(resp => {
openSnack("Successfully created bin");
if (!coords) {
let binPath: string = "/bins/" + resp.data.bin;
navigate(binPath);
}
close(true);
})
.catch((err: any) => {
openSnack("Failed to create bin");
close();
});
};
const updateBin = () => {
// If initial moisture is updated, so does the timestamp
if (form.inventory?.initialMoisture !== bin?.settings.inventory?.initialMoisture) {
if (form.inventory) form.inventory.initialTimestamp = moment().format();
}
if (form.inventory?.initialTimestamp === "") {
form.inventory.initialTimestamp = moment().format();
}
if (form.specs) {
form.specs.bushelCapacity = Number(inputCapacity);
if (form.specs.useAdvanced && form.specs.advancedDimensions) {
let adD = form.specs.advancedDimensions;
form.specs.heightCm = adD.hopperHeight + adD.sidewallHeight + adD.topConeHeight;
}
}
if (form.inventory?.grainBushels !== bin?.settings.inventory?.grainBushels) {
let current = bin?.settings.inventory?.grainBushels ?? 0;
let newVal = form.inventory?.grainBushels ?? 0;
setGrainDiff(newVal - current);
setGrainUpdate(true);
} else {
if (bin && bin.key()) {
form.storage = storageType;
form.highTemp = highTempC;
form.lowTemp = lowTempC;
form.autoGrainNode = autoTopNode;
if (form.inventory) {
form.inventory.inventoryControl = inventoryControl
form.inventory.autoThreshold = autoFillThreshold
//if the users preferences are in feet convert the distance to cm otherwise it was entered as cm so use that
form.inventory.lidarDropCm = getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? lidarDropDistance * 30.48 : lidarDropDistance
if(form.inventory.grainType !== pond.Grain.GRAIN_CUSTOM){
form.inventory.customGrain = undefined
}else{
form.inventory.customGrain = customGrain
}
}
binAPI
.updateBin(bin.key(), form, as)
.then(response => {
openSnack("Updated " + bin.name());
close(true);
})
.catch((err: any) => {
openSnack("Failed to update " + bin.name());
close();
});
}
}
};
const grainInventoryController = () => {
if (bin) {
let newBin = clone(bin);
newBin.settings = cloneDeep(form); //need to make sure to use all the changed settings
if (newBin.settings.inventory) {
newBin.settings.inventory.grainBushels = bin.settings.inventory?.grainBushels ?? 0; //but also need to use the old inventory amount so the diff is applied correctly
}
return (
<GrainTransaction
open={grainUpdate}
mainObject={newBin}
grainAdjustment={grainDiff}
close={() => {
setGrainUpdate(false);
setGrainDiff(0);
}}
callback={() => {
close(true);
}}
/>
);
}
};
const removeBin = () => {
if (bin && bin.key()) {
binAPI
.removeBin(bin.key(), as)
.then(response => {
openSnack(bin.name() + " was removed");
navigate("/bins");
})
.catch(err => {
openSnack("Failed to remove " + bin.name() + ".");
setRemoveIsOpen(false);
close();
});
}
};
const isFormValid = () => {
let valid = false;
if (
validInitial &&
validTarget &&
validDeviation &&
+formExtension.highTemp > +formExtension.lowTemp &&
+formExtension.lowTemp <= +formExtension.tempTarget &&
+formExtension.highTemp >= +formExtension.tempTarget
) {
valid = true;
}
return valid;
};
const stepComplete = (step: number) => {
switch (step) {
case 1:
let fan = form.fan;
let fanValid =
fan && fan.type !== pond.FanType.FAN_TYPE_UNKNOWN
? fan.size !== pond.FanHp.FAN_HP_UNKNOWN
: true;
return fanValid;
case 0:
let specs = form.specs;
let specsValid =
specs && Number(inputCapacity) > 0 && specs.diameterCm > 0 && specs.heightCm > 0;
return specsValid
default:
return true;
}
};
const stepNext = () => {
setActiveStep(activeStep + 1);
};
const stepBack = () => {
setActiveStep(activeStep - 1);
};
const updateForm = (
key: keyof pond.BinSettings,
value: pond.BinSettings[keyof pond.BinSettings]
) => {
setForm(pond.BinSettings.create({ ...form, [key]: value }));
};
const updateFormExtension = (
key: keyof BinFormExtension,
value: BinFormExtension[keyof BinFormExtension]
) => {
setFormExtension({ ...formExtension, [key]: value });
};
const titleText = () => {
return mode === "add" ? "Create Bin" : "Update " + (bin ? bin.name() : "Bin");
};
const stepper = () => {
if (mode === "add") {
return (
<Stepper
activeStep={activeStep}
alternativeLabel
classes={{
root: classes.stepper
}}>
{steps.map((s, i) => (
<Step
key={i}
onClick={() => {
const incompleteUpTo = (upTo: number): number => {
for (let j = 0; j < i; j++) {
if (!stepComplete(j)) {
return j;
}
}
return -1;
};
if (i <= activeStep || incompleteUpTo(i) === -1) {
setActiveStep(i);
} else {
openSnack("Step " + (incompleteUpTo(i) + 1).toString() + " is incomplete");
}
}}>
<StepLabel>{s.label}</StepLabel>
</Step>
))}
</Stepper>
);
}
return (
<Box display="flex" justifyContent="center" width="100%">
<Tabs
value={activeStep}
onChange={(_, value) => setActiveStep(value)}
indicatorColor="primary"
textColor="primary"
variant="fullWidth"
aria-label="bin tabs"
classes={{ root: classes.tabs }}>
{steps.map((s, i) => (
<Tab key={i} label={s.label} aria-label={s.label} />
))}
</Tabs>
</Box>
);
};
const title = () => {
if (isMobile) {
return (
<AppBar position="relative">
<Toolbar>
<IconButton edge="start" color="inherit" onClick={() => close()} aria-label="close">
<Close />
</IconButton>
<Typography variant="h6" style={{ flex: 1 }}>
{titleText()}
</Typography>
{mode === "add" ? (
<Button
onClick={() => addBin()}
disabled={!isFormValid() || activeStep !== steps.length - 1}
aria-label="create bin"
color="inherit">
Create
</Button>
) : (
<Button
onClick={() => updateBin()}
disabled={!canEdit || !isFormValid()}
aria-label="update bin">
Update
</Button>
)}
</Toolbar>
{stepper()}
</AppBar>
);
}
return (
<React.Fragment>
<DialogTitle>{titleText()}</DialogTitle>
{mode === "update" && (
<Tabs
value={activeStep}
onChange={(_, value) => setActiveStep(value)}
indicatorColor="primary"
textColor="primary"
variant="fullWidth"
aria-label="bin tabs"
classes={{ root: classes.tabSmall }}>
{steps.map((s, i) => (
<Tab className={classes.tabSmall} key={i} label={s.label} aria-label={s.label} />
))}
</Tabs>
)}
</React.Fragment>
);
};
const binYardForm = () => {
return (
<Box style={{ marginTop: theme.spacing(1) }}>
<Typography variant="subtitle2" align="center" color="primary" gutterBottom>
Bin Yard
</Typography>
<Box marginY={1}>
<SearchSelect
selected={selectedBinYard}
changeSelection={(option: Option | null) => {
let newForm = form;
newForm.yardKey = option?.value;
setForm(newForm);
setSelectedBinYard(option ? option : null);
}}
disabled={!canEdit}
options={binYardOptions ? binYardOptions : []}
label="Yard"
loading={false}
/>
</Box>
</Box>
);
};
const inventoryControlLabel = (control: pond.BinInventoryControl) => {
switch(control){
case pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR:
return "Auto (lidar)"
case pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC:
return "Auto (cable)"
case pond.BinInventoryControl.BIN_INVENTORY_CONTROL_HYBRID_LIDAR:
return "Hybrid (lidar)"
case pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART:
return "Auto (Libra Cart)"
default:
return "Manual"
}
};
const inventoryForm = () => {
const inventory = form.inventory;
const empty = inventory ? inventory.empty : false;
const bushPerTonne = formExtension.bushelsPerTonne;
const binQuantity = formExtension.grainBushels;
const grainTonnes = formExtension.grainTonnes;
const grainType = formExtension.grainType;
const grainSubtype = formExtension.grainSubtype;
const grainUse = formExtension.grainUse;
const customTypeName = formExtension.customTypeName;
return (
<Box>
<Typography variant="subtitle2" align="center" color="primary">
Bin Inventory
</Typography>
<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);
if (checked) {
setStorageType(pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN);
updateForm(
"inventory",
pond.BinInventory.create({
...form.inventory,
grainType: pond.Grain.GRAIN_CUSTOM,
bushelsPerTonne: 0
})
);
updateFormExtension("grainType", null);
updateFormExtension("bushelsPerTonne", "");
} else {
setStorageType(pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN);
updateForm(
"inventory",
pond.BinInventory.create({
...form.inventory,
customTypeName: undefined
})
);
updateFormExtension("customTypeName", "");
}
}}
name="storage"
/>
</Grid>
<Grid>Custom</Grid>
</Grid>
</Grid>
<Grid>
<FormControlLabel
control={
<Switch
checked={empty}
onChange={(_, checked) =>
updateForm(
"inventory",
pond.BinInventory.create({
...form.inventory,
empty: checked
})
)
}
name="empty"
/>
}
disabled={!canEdit}
label="Empty"
labelPlacement="start"
/>
</Grid>
</Grid>
{isCustomInventory && (
<RadioGroup
row
value={storageType}
onChange={(_, value) => {
let storageType = parseFloat(value) as pond.BinStorage;
setStorageType(storageType);
if (storageType === pond.BinStorage.BIN_STORAGE_FERTILIZER) {
formExtension.bushelsPerTonne = "";
formExtension.grainBushels = (
parseFloat(formExtension.grainBushels) * 35.239
).toFixed(0);
} else {
formExtension.grainBushels = (
parseFloat(formExtension.grainBushels) / 35.239
).toFixed(0);
}
}}>
<FormControlLabel
value={pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN}
control={<Radio />}
label={"Grain"}
/>
<FormControlLabel
value={pond.BinStorage.BIN_STORAGE_FERTILIZER}
control={<Radio />}
label={"Fertilizer"}
/>
</RadioGroup>
)}
<Accordion className={classes.bottomSpacing}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Grid
width={"100%"}
container
direction="row"
alignContent="center"
alignItems="center"
justifyContent="space-between">
<Grid>
<Typography>Inventory Control</Typography>
</Grid>
<Grid>
<Typography>{inventoryControlLabel(inventoryControl)}</Typography>
</Grid>
</Grid>
</AccordionSummary>
<AccordionDetails>
<RadioGroup
row
value={inventoryControl}
onChange={(_, value) => {
setInventoryControl(+value);
}}>
<FormControlLabel
value={pond.BinInventoryControl.BIN_INVENTORY_CONTROL_MANUAL}
control={<Radio />}
label={"Manual"}
/>
<FormControlLabel
value={pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC}
control={<Radio />}
label={"Auto (Cable)"}
/>
<FormControlLabel
value={pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR}
control={<Radio />}
label={"Auto (Lidar)"}
/>
<FormControlLabel
value={pond.BinInventoryControl.BIN_INVENTORY_CONTROL_HYBRID_LIDAR}
control={<Radio />}
label={"Hybrid (Lidar)"}
/>
{user.hasFeature("libra-cart") &&
<FormControlLabel
value={pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART}
control={<Radio />}
label={"Auto (Libra Cart)"}
/>
}
</RadioGroup>
</AccordionDetails>
{inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC &&
!autoTopNode && (
<Box marginX={2} marginBottom={2}>
<Typography variant="caption">
Auto Top Node must be activated for automatic inventory control
</Typography>
</Box>
)}
{inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_HYBRID_LIDAR && (
<Box marginX={2} marginBottom={2}>
<Typography variant="caption">
Hybrid control will submit a transaction that must be approved by the user before inventory is changed
</Typography>
</Box>
)}
{(inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR ||
inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_HYBRID_LIDAR) &&
<Box width="100%" padding={2}>
<TextField
fullWidth
variant="outlined"
label="Auto Threshold Percent"
helperText="Fill percent threshold that must be crossed to affect inventory"
InputProps={{
endAdornment: (
<InputAdornment position="end">
%
</InputAdornment>
)
}}
type="number"
value={autoFillThreshold}
onChange={e => {
setAutoFillThreshold(+e.target.value)
}}
/>
<TextField
sx={{marginTop: 4}}
fullWidth
variant="outlined"
label="Drop Distance"
helperText="The distance between the lidar and the peak"
type="number"
InputProps={{
endAdornment: (
<InputAdornment position="end">
{getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "ft" : "cm"}
</InputAdornment>
)
}}
value={lidarDropDistance}
onChange={e => {
setLidarDropDistance(+e.target.value)
}}
/>
</Box>
}
{inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART &&
<Box width="100%" padding={2}>
<SearchSelect
label="Libra Cart Destination"
selected={lcDestination}
changeSelection={option => {
let newForm = form;
newForm.libracartDestinationKey = option?.value;
setForm(newForm);
setlcDestination(option ? option : null);
}}
disabled={!canEdit}
options={lcDestinationOptions}
/>
<Typography variant="caption">
The linked bins inventory will be adjusted When the Libra Cart Destination data is synced every 6 hours
</Typography>
</Box>
}
</Accordion>
{empty ? (
<Box marginTop={3} display="flex" flexDirection="column" alignItems="center">
<Typography variant="subtitle1" color="textPrimary">
Bin is currently empty
</Typography>
<Typography variant="body1" color="textSecondary">
Inventory settings will be ignored
</Typography>
<Typography variant="body2" color="textSecondary">
We may ask for your inventory again in the future
</Typography>
</Box>
) : (
<Box>
{/* <Typography variant="subtitle1" color="textSecondary" gutterBottom>
{isCustom ? "Custom" : "Grain"}
</Typography> */}
<Box>
{isCustomInventory ?
<React.Fragment>
{storageType === pond.BinStorage.BIN_STORAGE_FERTILIZER ?
<React.Fragment>
<TextField
label="Type"
value={customTypeName}
type="text"
onChange={event => {
updateForm(
"inventory",
pond.BinInventory.create({
...form.inventory,
customTypeName: event.target.value
})
);
updateFormExtension("customTypeName", event.target.value);
}}
fullWidth
variant="outlined"
className={classes.bottomSpacing}
/>
</React.Fragment>
:
<Box sx={{border: "1px solid white", borderRadius: 2, padding: 2, marginBottom: 2}}>
<CustomGrainSelector initialGrain={customGrain} onGrainSettingsChange={(newGrainSettings) => {setCustomGrain(newGrainSettings)}}/>
</Box>
}
</React.Fragment>
:
<React.Fragment>
<Box className={classes.bottomSpacing}>
<SearchSelect
label="Type"
selected={grainType}
changeSelection={option => {
let newGrainType = option
? pond.Grain[option.value as keyof typeof pond.Grain]
: pond.Grain.GRAIN_INVALID;
updateForm(
"inventory",
pond.BinInventory.create({
...form.inventory,
grainType: newGrainType,
bushelsPerTonne: GrainDescriber(newGrainType).bushelsPerTonne
})
);
//doesn't need to update the form extension to display the bushels per tonne as it is not changeable for the supported grain
//and is reset whenever the user switches between custom or not
updateFormExtension("grainType", option);
}}
group
disabled={!canEdit}
options={grainOptions}
/>
</Box>
<TextField
label="Grain Variant"
value={grainSubtype}
type="text"
onChange={event => {
updateForm(
"inventory",
pond.BinInventory.create({
...form.inventory,
grainSubtype: event.target.value
})
);
updateFormExtension("grainSubtype", event.target.value);
}}
fullWidth
variant="outlined"
disabled={!grainType}
className={classes.bottomSpacing}
/>
</React.Fragment>
}
{getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_BUSHELS ||
storageType === pond.BinStorage.BIN_STORAGE_FERTILIZER ||
formExtension.bushelsPerTonne === "0" ||
formExtension.bushelsPerTonne === "" ? (
<TextField
label="Amount"
value={binQuantity}
type="number"
onChange={event => {
let capacity =
bin?.settings.storage === pond.BinStorage.BIN_STORAGE_FERTILIZER
? +inputCapacity * 35.239
: inputCapacity;
let value = event?.target.value;
let v =
value === ""
? 0
: isNaN(parseFloat(value))
? 0
: parseFloat(value) >= +capacity
? +capacity
: parseFloat(value) <= 0
? 0
: parseFloat(value);
if (bin?.storage() === pond.BinStorage.BIN_STORAGE_FERTILIZER && v) {
v = v / 35.239;
}
//setNewGrainQuantity(v);
updateForm(
"inventory",
pond.BinInventory.create({
...form.inventory,
grainBushels: v
})
);
updateFormExtension("grainBushels", value);
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{storageType === pond.BinStorage.BIN_STORAGE_FERTILIZER
? "Litres"
: "Bushels"}
</InputAdornment>
)
}}
fullWidth
variant="outlined"
disabled={
!canEdit ||
inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC
}
className={classes.bottomSpacing}
/>
) : (
<TextField
label="Amount"
value={grainTonnes}
type="number"
onChange={event => {
let newWeight = event.target.value;
const bushelsPerTonne = +formExtension.bushelsPerTonne;
//get the bushel amount
if (!isNaN(+newWeight) && !isNaN(+bushelsPerTonne)) {
let newBushels = Math.round(+newWeight * +bushelsPerTonne * 100) / 100;
updateForm(
"inventory",
pond.BinInventory.create({
...form.inventory,
grainBushels: newBushels
})
);
//updateFormExtension("grainBushels", newBushels.toString())
updateFormExtension("grainTonnes", newWeight);
}
}}
fullWidth
variant="outlined"
disabled={!canEdit}
InputProps={{
endAdornment: <InputAdornment position="end">mT</InputAdornment>
}}
className={classes.bottomSpacing}
/>
)}
{!isCustomInventory && (
<SearchSelect
label="Use"
selected={grainUse}
changeSelection={option => {
let grainUse = option
? pond.GrainUse[option.value as keyof typeof pond.GrainUse]
: pond.GrainUse.GRAIN_USE_UNKNOWN;
updateForm(
"inventory",
pond.BinInventory.create({
...form.inventory,
grainUse: grainUse
})
);
updateFormExtension("grainUse", option);
}}
group
disabled={!canEdit}
options={grainUseOptions}
/>
)}
</Box>
{/*<Box marginTop={1}>
<DateTimePicker
disabled={!canEdit}
renderInput={props => <TextField {...props} helperText="" />}
value={time}
onChange={date =>
updateForm(
"inventory",
pond.BinInventory.create({
...form.inventory,
initialTimestamp: moment(date ?? undefined).format()
})
)
}
disableFuture
label="Start Time"
/>
</Box>*/}
</Box>
)}
</Box>
);
};
const fanPicker = () => {
return (
<FanPicker
updateFan={(fanId: number) => {
if (fanId !== undefined) {
//set the fan id in the bin settings form
form.fanId = fanId;
//remove the old fan from the bin
form.fan = null;
}
}}
fanID={form.fanId}
useCardView
/>
);
};
const getBushels = (height: number, diameter: number): number => {
let d = diameter * 3.281;
let h = height * 3.281;
return 0.628 * (d * d) * h;
};
/**
* takes in the dimensions of the bin to calculate its bushel capacity
* @param diameter
* @param topConeHeight
* @param sidewallHeight
* @param hopperHeight
* @returns the bushel capacity of the bin with given dimensions
*/
const advancedGrainCalc = (
diameter: number,
topConeHeight: number,
sidewallHeight: number,
hopperHeight?: number
) => {
//if the units are in meters will need to convert to feet
let tcH = topConeHeight;
let sH = sidewallHeight;
let hH = hopperHeight ?? 0;
let d = diameter;
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
tcH = tcH * 3.281;
sH = sH * 3.281;
hH = hH * 3.281;
d = d * 3.281;
}
let topConeBushels = ConeVolume(d / 2, tcH) * conversionConstantFT;
let sidewallBushels = CylinderVolume(d / 2, sH) * conversionConstantFT;
let hopperBushels = ConeVolume(d / 2, hH ?? 0) * conversionConstantFT;
return Math.round(topConeBushels + sidewallBushels + hopperBushels);
};
const generalForm = () => {
const name = form?.name ?? "";
const shape = form.specs ? form.specs.shape.valueOf() : "";
const advanced = form.specs ? form.specs.useAdvanced : false;
const roofAngle =
form.specs && form.specs.advancedDimensions ? form.specs.advancedDimensions.roofAngle : 0;
const hopperAngle =
form.specs && form.specs.advancedDimensions ? form.specs.advancedDimensions.hopperAngle : 0;
let height = formExtension.height;
let topConeHeight = formExtension.topConeHeight;
let sidewallHeight = formExtension.sidewallHeight;
let hopperHeight = formExtension.hopperHeight;
let diameter = formExtension.diameter;
// if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET) {
// diameterM = (Number(diameterM) * 3.28084).toFixed(2);
// heightM = (Number(heightM) * 3.28084).toFixed(2);
// }
return (
<Box>
<Box width={"100%"}>
<FormControlLabel
control={
<Switch
value={isCustomBin}
checked={isCustomBin}
title="Build Custom Bin"
onClick={() => {
setIsCustomBin(!isCustomBin);
}}
/>
}
label="Build Custom Bin"
labelPlacement="start"
/>
</Box>
<Grid container direction="row" alignItems="center" alignContent="center">
<Grid >
<FormControlLabel
control={
<Checkbox
value={autoTopNode}
checked={autoTopNode}
onChange={(_, checked) => {
setAutoTopNode(checked);
}}
/>
}
label={"Automatic Top Node"}
/>
</Grid>
<Grid >
<Tooltip
title={
<React.Fragment>
<Typography>
Will run every day using the Day/Night cycle of temperatures on each of the bins
cables to determine which nodes are in the grain as opposed to in the air and
update its interactions accordingly.
</Typography>
<Typography>
If a bin is determined to be empty it will remove the interactions on its
cables.
</Typography>
</React.Fragment>
}>
<Info />
</Tooltip>
</Grid>
</Grid>
<Typography variant="subtitle2" align="center" color="primary" gutterBottom>
Bin Information
</Typography>
<Typography variant="body1" align="center" gutterBottom>
{/* {modelID !== 0
? getBinModel(modelID)?.Manufacturer + " " + getBinModel(modelID)?.Model
: "Custom"} */}
</Typography>
<Box marginTop={1} marginBottom={1}>
<TextField
label="Name"
fullWidth
variant="outlined"
value={name}
disabled={!canEdit}
onChange={e => updateForm("name", e.target.value)}
/>
</Box>
{isCustomBin ? (
<Box>
<Box marginTop={1} alignContent="center" justifyContent="center">
<TextField
label="Height"
type="number"
required
value={height}
disabled={!canEdit || advanced}
onChange={event => {
let value = event?.target.value;
let d = diameter;
let valueM = value;
let dM = diameter;
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET) {
valueM = (Number(value) * 0.3048).toFixed(2);
dM = (Number(d) * 0.3048).toFixed(2);
}
setModelID(0);
updateForm(
"specs",
pond.BinSpecs.create({
...form.specs,
modelId: 0,
heightCm:
valueM === ""
? undefined
: isNaN(parseFloat(valueM))
? 0
: parseFloat(valueM) * 100
})
);
updateFormExtension("height", value);
setInputCapacity(getBushels(Number(valueM), Number(dM)).toFixed(0));
//updateFormExtension("capacity", getBushels(Number(value), Number(d)).toFixed(0));
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{getDistanceUnit() !== pond.DistanceUnit.DISTANCE_UNIT_FEET ? "m" : "ft"}
</InputAdornment>
)
}}
style={{ width: "59%" }}
variant="outlined"
/>
<FormControlLabel
style={{ width: "38%", paddingTop: 5 }}
control={
<Switch
disabled={!canEdit}
value={advanced}
checked={advanced}
title="Advanced Dimensions"
onChange={(_, checked) => {
updateForm(
"specs",
pond.BinSpecs.create({
...form.specs,
useAdvanced: checked
})
);
if (checked) {
let ext = cloneDeep(formExtension);
let s = isNaN(parseFloat(sidewallHeight)) ? 0 : parseFloat(sidewallHeight);
let t = isNaN(parseFloat(topConeHeight)) ? 0 : parseFloat(topConeHeight);
let h = isNaN(parseFloat(hopperHeight)) ? 0 : parseFloat(hopperHeight);
//change the total height to use the section values
ext.height = (s + t + h).toFixed(2);
//update the capacity with those dimensions
let diameter = isNaN(parseFloat(ext.diameter))
? 0
: parseFloat(ext.diameter);
let bushelCap = advancedGrainCalc(diameter, t, s, h);
setInputCapacity(bushelCap.toFixed(0));
ext.capacity = bushelCap.toFixed(0);
setFormExtension(ext);
}
}}
/>
}
label="Advanced"
labelPlacement="start"
/>
</Box>
<Collapse in={advanced}>
<Box marginTop={1}>
<Grid container spacing={1}>
<Grid size={{ xs: 8 }}>
<TextField
label="Top Cone Height"
type="number"
value={topConeHeight}
disabled={!canEdit}
onChange={event => {
let value = event?.target.value;
let valueM = value;
setModelID(0);
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET) {
valueM = (Number(value) * 0.3048).toFixed(2);
}
let coneCM =
valueM === ""
? undefined
: isNaN(parseFloat(valueM))
? 0
: parseFloat(valueM) * 100;
//Calculate the roof angle
let dCm = form.specs?.diameterCm ?? 0; //get the diameter
let angle = RadiansToDegrees(Math.atan((coneCM ?? 0) / (dCm / 2))); //('arcTan(opposite / adjacent))' and convert to degrees
angle = Math.round(angle * 100) / 100; // round to 2 digits
let ext = cloneDeep(formExtension);
//calculate the total height from the parts
let s = isNaN(parseFloat(sidewallHeight)) ? 0 : parseFloat(sidewallHeight);
let t = isNaN(parseFloat(value)) ? 0 : parseFloat(value);
let h = isNaN(parseFloat(hopperHeight)) ? 0 : parseFloat(hopperHeight);
//trigger the advanced calculation for capacity
let diameter = isNaN(parseFloat(ext.diameter))
? 0
: parseFloat(ext.diameter);
let bushelCap = advancedGrainCalc(diameter, t, s, h);
setInputCapacity(bushelCap.toFixed(0));
ext.capacity = bushelCap.toFixed(0);
ext.topConeHeight = value; //set the new top cone heigh
ext.height = (s + t + h).toFixed(2); // set the new total height
//update the extensions with the new values
setFormExtension({ ...ext });
//need to get the total height in cm
let totalHeightCM = s + t + h; //the total height in the users units
if(getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET){
totalHeightCM = totalHeightCM * 30.48 //convert from feet to cm
}else{
totalHeightCM = totalHeightCM * 100 //convert from m to cm
}
//update the form with the new specs
updateForm(
"specs",
pond.BinSpecs.create({
...form.specs,
modelId: 0,
heightCm: Math.round(totalHeightCM),
advancedDimensions: pond.BinAdvancedDimensions.create({
...form.specs?.advancedDimensions,
topConeHeight: coneCM,
roofAngle: angle
})
})
);
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{getDistanceUnit() !== pond.DistanceUnit.DISTANCE_UNIT_FEET
? "m"
: "ft"}
</InputAdornment>
)
}}
variant="outlined"
/>
</Grid>
<Grid size={{ xs: 4 }}>
<TextField
label="Roof Angle"
type="number"
value={roofAngle}
disabled={!canEdit}
onChange={event => {
let value = event.target.value;
let angle =
value === ""
? undefined
: isNaN(parseFloat(value))
? 0
: parseFloat(value);
let d = form.specs?.diameterCm ?? 0; //get the diameter
//calculate the new cone height for the roof angle
let coneCM = TriangleOppositeLength(d / 2, angle ?? 0);
let newConeHeight =
getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET
? (coneCM / 30.48).toFixed(2)
: (coneCM / 100).toFixed(2);
let ext = cloneDeep(formExtension);
let s = isNaN(parseFloat(sidewallHeight)) ? 0 : parseFloat(sidewallHeight);
let t = isNaN(parseFloat(newConeHeight)) ? 0 : parseFloat(newConeHeight);
let h = isNaN(parseFloat(hopperHeight)) ? 0 : parseFloat(hopperHeight);
//trigger the advanced calculation for capacity
let diameter = isNaN(parseFloat(ext.diameter))
? 0
: parseFloat(ext.diameter);
let bushelCap = advancedGrainCalc(diameter, t, s, h);
setInputCapacity(bushelCap.toFixed(0));
ext.capacity = bushelCap.toFixed(0);
ext.topConeHeight = newConeHeight;
ext.height = (s + t + h).toFixed(2)
setFormExtension(ext);
//need to get the total height in cm
let totalHeightCM = s + t + h; //the total height in the users units
if(getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET){
totalHeightCM = totalHeightCM * 30.48 //convert from feet to cm
}else{
totalHeightCM = totalHeightCM * 100 //convert from m to cm
}
setModelID(0);
//update the form with the new specs
updateForm(
"specs",
pond.BinSpecs.create({
...form.specs,
modelId: 0,
heightCm: Math.round(totalHeightCM),
advancedDimensions: pond.BinAdvancedDimensions.create({
...form.specs?.advancedDimensions,
roofAngle: angle ?? 0,
topConeHeight: coneCM
})
})
);
}}
InputProps={{
endAdornment: <InputAdornment position="end">°</InputAdornment>
}}
variant="outlined"
/>
</Grid>
<Grid size={{ xs: 8 }}>
<TextField
label="Sidewall Height"
type="number"
value={sidewallHeight}
disabled={!canEdit}
onChange={event => {
let value = event?.target.value;
let valueM = value;
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET) {
valueM = (Number(value) * 0.3048).toFixed(2);
}
let ext = cloneDeep(formExtension);
let s = isNaN(parseFloat(value)) ? 0 : parseFloat(value);
let t = isNaN(parseFloat(topConeHeight)) ? 0 : parseFloat(topConeHeight);
let h = isNaN(parseFloat(hopperHeight)) ? 0 : parseFloat(hopperHeight);
//trigger the advanced calculation for capacity
let diameter = isNaN(parseFloat(ext.diameter))
? 0
: parseFloat(ext.diameter);
let bushelCap = advancedGrainCalc(diameter, t, s, h);
setInputCapacity(bushelCap.toFixed(0));
ext.capacity = bushelCap.toFixed(0);
ext.sidewallHeight = value;
ext.height = (s + t + h).toFixed(2);
setFormExtension({ ...ext });
setModelID(0);
//need to get the total height in cm
let totalHeightCM = s + t + h; //the total height in the users units
if(getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET){
totalHeightCM = totalHeightCM * 30.48 //convert from feet to cm
}else{
totalHeightCM = totalHeightCM * 100 //convert from m to cm
}
updateForm(
"specs",
pond.BinSpecs.create({
...form.specs,
modelId: 0,
heightCm: Math.round(totalHeightCM),
advancedDimensions: pond.BinAdvancedDimensions.create({
...form.specs?.advancedDimensions,
sidewallHeight:
valueM === ""
? undefined
: isNaN(parseFloat(valueM))
? 0
: parseFloat(valueM) * 100
})
})
);
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{getDistanceUnit() !== pond.DistanceUnit.DISTANCE_UNIT_FEET
? "m"
: "ft"}
</InputAdornment>
)
}}
variant="outlined"
/>
</Grid>
{shape === pond.BinShape.BIN_SHAPE_HOPPER_BOTTOM && (
<React.Fragment>
<Grid size={{ xs: 4 }}>
<TextField
label="Hopper Angle"
type="number"
value={hopperAngle}
disabled={!canEdit}
onChange={event => {
let value = event.target.value;
let angle =
value === ""
? undefined
: isNaN(parseFloat(value))
? 0
: parseFloat(value);
let d = form.specs?.diameterCm ?? 0; //get the diameter
//calculate the new cone height for the roof angle
let coneCM = TriangleOppositeLength(d / 2, angle ?? 0);
let newHopperHeight =
getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET
? (coneCM / 30.48).toFixed(2)
: (coneCM / 100).toFixed(2);
let ext = cloneDeep(formExtension);
let s = isNaN(parseFloat(sidewallHeight))
? 0
: parseFloat(sidewallHeight);
let t = isNaN(parseFloat(topConeHeight))
? 0
: parseFloat(topConeHeight);
let h = isNaN(parseFloat(newHopperHeight))
? 0
: parseFloat(newHopperHeight);
//trigger the advanced calculation for capacity
let diameter = isNaN(parseFloat(ext.diameter))
? 0
: parseFloat(ext.diameter);
let bushelCap = advancedGrainCalc(diameter, t, s, h);
setInputCapacity(bushelCap.toFixed(0));
ext.capacity = bushelCap.toFixed(0);
ext.hopperHeight = newHopperHeight;
ext.height = (s + t + h).toFixed(2);
setFormExtension(ext);
//update the form with the new specs
setModelID(0);
updateForm(
"specs",
pond.BinSpecs.create({
...form.specs,
modelId: 0,
advancedDimensions: pond.BinAdvancedDimensions.create({
...form.specs?.advancedDimensions,
hopperAngle: angle ?? 0,
hopperHeight: coneCM
})
})
);
//need to update the total height with the new height calculated from the angle
}}
InputProps={{
endAdornment: <InputAdornment position="end">°</InputAdornment>
}}
variant="outlined"
/>
</Grid>
<Grid size={{ xs: 8 }}>
<TextField
label="Hopper Height"
type="number"
value={hopperHeight}
disabled={!canEdit}
onChange={event => {
let value = event?.target.value;
let valueM = value;
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET) {
valueM = (Number(value) * 0.3048).toFixed(2);
}
let hopperHeightCM =
valueM === ""
? undefined
: isNaN(parseFloat(valueM))
? 0
: parseFloat(valueM) * 100;
//Calculate the hopper angle
let d = form.specs?.diameterCm ?? 0; //get the diameter
let angle = RadiansToDegrees(
Math.atan((hopperHeightCM ?? 0) / (d / 2))
); //('arcTan(opposite / adjacent))' and convert to degrees
angle = Math.round(angle * 100) / 100; // round to 2 digits
setModelID(0);
updateForm(
"specs",
pond.BinSpecs.create({
...form.specs,
modelId: 0,
advancedDimensions: pond.BinAdvancedDimensions.create({
...form.specs?.advancedDimensions,
hopperHeight: hopperHeightCM,
hopperAngle: angle
})
})
);
let ext = cloneDeep(formExtension);
let s = isNaN(parseFloat(sidewallHeight))
? 0
: parseFloat(sidewallHeight);
let t = isNaN(parseFloat(topConeHeight))
? 0
: parseFloat(topConeHeight);
let h = isNaN(parseFloat(value)) ? 0 : parseFloat(value);
//trigger the advanced calculation for capacity
let diameter = isNaN(parseFloat(ext.diameter))
? 0
: parseFloat(ext.diameter);
let bushelCap = advancedGrainCalc(diameter, t, s, h);
setInputCapacity(bushelCap.toFixed(0));
ext.capacity = bushelCap.toFixed(0);
ext.hopperHeight = value;
ext.height = (s + t + h).toFixed(2);
setFormExtension({ ...ext });
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{getDistanceUnit() !== pond.DistanceUnit.DISTANCE_UNIT_FEET
? "m"
: "ft"}
</InputAdornment>
)
}}
variant="outlined"
/>
</Grid>
</React.Fragment>
)}
</Grid>
</Box>
</Collapse>
<Box marginTop={1}>
<TextField
label="Diameter"
type="number"
required
value={diameter}
disabled={!canEdit}
onChange={event => {
let value = event?.target.value;
let h = height;
let valueM = value;
let hM = height;
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET) {
valueM = (Number(value) * 0.3048).toFixed(2);
hM = (Number(h) * 0.3048).toFixed(2);
}
let diameterCM =
valueM === ""
? undefined
: isNaN(parseFloat(valueM))
? 0
: parseFloat(valueM) * 100;
//adjust the angles using the new diameter
let d = diameterCM ?? 0; //get the diameter
//Calculate the hopper angle
let hopperAngle = RadiansToDegrees(
Math.atan((form.specs?.advancedDimensions?.hopperHeight ?? 0) / (d / 2))
); //('arcTan(opposite / adjacent))' and convert to degrees
hopperAngle = Math.round(hopperAngle * 100) / 100; // round to 2 digits
//Calculate the roof angle
let roofAngle = RadiansToDegrees(
Math.atan((form.specs?.advancedDimensions?.topConeHeight ?? 0) / (d / 2))
); //('arcTan(opposite / adjacent))' and convert to degrees
roofAngle = Math.round(roofAngle * 100) / 100; // round to 2 digits
setModelID(0);
updateForm(
"specs",
pond.BinSpecs.create({
...form.specs,
modelId: 0,
diameterCm: diameterCM,
advancedDimensions: pond.BinAdvancedDimensions.create({
...form.specs?.advancedDimensions,
hopperAngle: hopperAngle,
roofAngle: roofAngle
})
})
);
let ext = cloneDeep(formExtension);
ext.diameter = value;
if (advanced) {
//advanced capacity calc using the 3 sections
} else {
//basic capcity calc using only height and diameter
setInputCapacity(getBushels(Number(hM), Number(valueM)).toFixed(0));
}
setFormExtension({ ...ext });
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{getDistanceUnit() !== pond.DistanceUnit.DISTANCE_UNIT_FEET ? "m" : "ft"}
</InputAdornment>
)
}}
fullWidth
variant="outlined"
/>
</Box>
<Box marginTop={1}>
<TextField
label="Capacity"
type="number"
required
value={inputCapacity}
disabled={!canEdit}
onChange={event => {
let value = event?.target.value;
value = isNaN(parseFloat(value)) ? "" : parseFloat(value) >= 0 ? value : "";
setInputCapacity(value);
updateForm(
"specs",
pond.BinSpecs.create({
...form.specs,
bushelCapacity:
value === "" ? undefined : isNaN(parseFloat(value)) ? 0 : parseFloat(value)
})
);
updateFormExtension("capacity", value);
}}
InputProps={{
endAdornment: <InputAdornment position="end">bushels</InputAdornment>
}}
fullWidth
variant="outlined"
/>
</Box>
<Box marginTop={2}>
<FormControl component="fieldset" variant="outlined" fullWidth disabled={!canEdit}>
{/* <OutlinedBox label={<FormLabel component="legend">Shape</FormLabel>} padding={1}> */}
<FormLabel component="legend">Shape</FormLabel>
<RadioGroup
aria-label="bin shape"
name="binShape"
value={shape}
onChange={(_, value) => {
//if user changed from hopper to flat bottom and they are using advanced dimensions
if (parseInt(value) === pond.BinShape.BIN_SHAPE_FLAT_BOTTOM && advanced) {
let ext = cloneDeep(formExtension);
//set the hopper measurements (height/angle) to be 0
ext.hopperHeight = "0";
//re-calc the total height for the extension, the settings will be done upon saving the bin
let s = isNaN(parseFloat(sidewallHeight)) ? 0 : parseFloat(sidewallHeight);
let t = isNaN(parseFloat(topConeHeight)) ? 0 : parseFloat(topConeHeight);
ext.height = (s + t).toFixed(2);
//re-calc the capacity
let diameter = isNaN(parseFloat(ext.diameter))
? 0
: parseFloat(ext.diameter);
let bushelCap = advancedGrainCalc(diameter, t, s);
setInputCapacity(bushelCap.toFixed(0));
ext.capacity = bushelCap.toFixed(0);
//update extension with new values
setFormExtension(ext);
}
updateForm(
"specs",
pond.BinSpecs.create({
...form.specs,
shape: parseInt(value),
advancedDimensions: pond.BinAdvancedDimensions.create({
...form.specs?.advancedDimensions,
hopperAngle: 0,
hopperHeight: 0
})
})
);
}}>
{GetBinShapeDescribers()
.filter(s => s.key !== pond.BinShape.BIN_SHAPE_UNKNOWN)
.map(s => (
<FormControlLabel
key={s.key}
value={s.key}
control={<Radio />}
label={s.label}
/>
))}
</RadioGroup>
{/* </OutlinedBox> */}
<FormHelperText>Aeration effectiveness varies with bin shape</FormHelperText>
</FormControl>
</Box>
</Box>
) : (
<Box>
<BinSelector
vertical
optionsChanged={options => {
let ops: Option[] = [];
options.forEach(o => {
ops.push({
label: o.Manufacturer + " - " + o.Model + " - " + o.Bottom,
value: o
});
});
setBinModelOptions(ops);
}}
/>
<Box marginTop={1}>
<SearchSelect
options={binModelOps}
selected={selectedBinModel}
label="Select Bin Model"
changeSelection={selected => {
if (selected) {
setSelectedBinModel(selected);
let jsonBin = selected.value;
setModelID(jsonBin.ID);
let bushelCap = 0;
//need to set all the form (form is a bin settings object) data according to the bin that was selected
if (form.specs) {
//json bins lengths are in feet so for the data stored in the backend need to convert it to cm
let hopperHeightFt = TriangleOppositeLength(
jsonBin.Diameter / 2,
jsonBin.HopperAngle
);
// bushelCap = advancedGrainCalc(jsonBin.Diameter, jsonBin.Peak - jsonBin.Sidewall, jsonBin.Sidewall, hopperHeightFt)
// console.log(bushelCap)
let topConeBushels =
ConeVolume(jsonBin.Diameter / 2, jsonBin.Peak - jsonBin.Sidewall) *
conversionConstantFT;
let cylinderBushels =
CylinderVolume(jsonBin.Diameter / 2, jsonBin.Sidewall) *
conversionConstantFT;
let hopperBushels =
ConeVolume(jsonBin.Diameter / 2, hopperHeightFt) * conversionConstantFT;
bushelCap = topConeBushels + cylinderBushels + hopperBushels;
//set all the values that would be stored in the backend in bin settings
updateForm(
"specs",
pond.BinSpecs.create({
...form.specs,
bushelCapacity: Math.round(bushelCap),
heightCm:
(jsonBin.Peak +
TriangleOppositeLength(jsonBin.Diameter / 2, jsonBin.HopperAngle)) *
30.48,
diameterCm: jsonBin.Diameter * 30.48,
advancedDimensions: pond.BinAdvancedDimensions.create({
sidewallHeight: jsonBin.Sidewall * 30.48,
topConeHeight: (jsonBin.Peak - jsonBin.Sidewall) * 30.48,
hopperHeight: hopperHeightFt * 30.48,
roofAngle: jsonBin.RoofAngle,
hopperAngle: jsonBin.HopperAngle
}),
modelId: jsonBin.ID,
shape:
jsonBin.Bottom === "Hopper Bottom"
? pond.BinShape.BIN_SHAPE_HOPPER_BOTTOM
: pond.BinShape.BIN_SHAPE_FLAT_BOTTOM
})
);
}
//set the form extensions values that are displayed to the user
let hopperHeight = TriangleOppositeLength(
jsonBin.Diameter / 2,
jsonBin.HopperAngle
);
let ext = cloneDeep(formExtension);
ext.capacity = bushelCap.toFixed(0);
setInputCapacity(bushelCap.toFixed(0));
//the dimensions for the correct unit defined by user settings
let userHeight = jsonBin.Peak + hopperHeight;
let userTopCone = jsonBin.Peak - jsonBin.Sidewall;
let userSidewall = jsonBin.Sidewall;
let userHopperHeight = hopperHeight;
let userDiameter = jsonBin.Diameter;
//since the jsone data is in feet just check if we need to make it in meters
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
userHeight = userHeight / 3.281;
userDiameter = userDiameter / 3.281;
userTopCone = userTopCone / 3.281;
userSidewall = userSidewall / 3.281;
userHopperHeight = userHopperHeight / 3.281;
}
ext.height = userHeight.toFixed(2);
ext.diameter = userDiameter.toFixed(2);
ext.topConeHeight = userTopCone.toFixed(2);
ext.sidewallHeight = userSidewall.toFixed(2);
ext.hopperHeight = userHopperHeight.toFixed(2);
setFormExtension({ ...ext });
}
}}
/>
</Box>
</Box>
)}
{binYardForm()}
</Box>
);
};
const targetForm = () => {
let highTemp = formExtension.highTemp;
let lowTemp = formExtension.lowTemp;
let tempTarget = formExtension.tempTarget;
return (
<Box>
<Typography variant="subtitle2" align="center" color="primary" gutterBottom>
Grain Targets
</Typography>
<Typography variant="body1" align="center" gutterBottom>
Temperature
</Typography>
<Box marginTop={1}>
<TextField
label="Temperature Target"
type="number"
required
error={
+formExtension.lowTemp > +formExtension.tempTarget ||
+formExtension.highTemp < +formExtension.tempTarget
}
helperText={
+formExtension.lowTemp >= +formExtension.highTemp
? "Target must be between the lower and upper limits"
: ""
}
value={tempTarget}
disabled={!canEdit}
onChange={event => {
let value = event?.target.value;
let valueC = value;
if (getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
valueC = ((Number(value) - 32) * (5 / 9)).toFixed(2);
}
if (form.inventory) {
form.inventory.targetTemperature = +valueC;
}
updateFormExtension("tempTarget", value);
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT
? "F"
: "C"}
</InputAdornment>
)
}}
fullWidth
variant="outlined"
/>
</Box>
<Box marginTop={1}>
<Grid container spacing={1}>
<Grid size={{ xs: 6 }}>
<TextField
label="Lower Limit"
type="number"
required
error={+formExtension.lowTemp >= +formExtension.highTemp}
helperText={
+formExtension.lowTemp >= +formExtension.highTemp
? "Lower Limit must be less than the Upper Limit"
: ""
}
value={lowTemp}
disabled={!canEdit}
onChange={event => {
let value = event?.target.value;
let valueC = value;
if (getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
valueC = ((Number(value) - 32) * (5 / 9)).toFixed(2);
}
setLowTempC(+valueC);
updateFormExtension("lowTemp", value);
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT
? "F"
: "C"}
</InputAdornment>
)
}}
fullWidth
variant="outlined"
/>
</Grid>
<Grid size={{ xs: 6 }}>
<TextField
label="Upper Limit"
type="number"
required
error={+formExtension.highTemp <= +formExtension.lowTemp}
helperText={
+formExtension.highTemp <= +formExtension.lowTemp
? "Upper Limit must be greater than the Lower Limit"
: ""
}
value={highTemp}
disabled={!canEdit}
onChange={event => {
let value = event?.target.value;
let valueC = value;
if (getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
valueC = ((Number(value) - 32) * (5 / 9)).toFixed(2);
}
setHighTempC(+valueC);
updateFormExtension("highTemp", value);
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT
? "F"
: "C"}
</InputAdornment>
)
}}
fullWidth
variant="outlined"
/>
</Grid>
</Grid>
</Box>
<Box marginTop={1}>
<Typography variant="body1" align="center" gutterBottom>
Moisture
</Typography>
{isCustomInventory && (
<Typography variant="caption">You are using a custom Grain type, the drying estimate may not be accurate</Typography>
)}
<Box marginTop={1}>
<TextField
label="Target EMC"
InputProps={{
endAdornment: <InputAdornment position="end">% EMC</InputAdornment>
}}
fullWidth
value={tMoistureString}
disabled={!canEdit}
variant="outlined"
className={classes.bottomSpacing}
error={isNaN(+tMoistureString) && tMoistureString !== ""}
onChange={event => {
setTMoistureString(event.target.value);
if (!isNaN(+event.target.value)) {
setValidTarget(true);
updateForm(
"inventory",
pond.BinInventory.create({
...form.inventory,
targetMoisture: +event.target.value
})
);
} else {
setValidTarget(false);
}
}}
/>
</Box>
<Grid container spacing={1}>
<Grid size={{ xs: 6 }}>
<TextField
label="Initial"
InputProps={{
endAdornment: <InputAdornment position="end">% EMC</InputAdornment>
}}
fullWidth
value={inMoistureString}
variant="outlined"
disabled={!canEdit}
className={classes.bottomSpacing}
error={isNaN(+inMoistureString) && inMoistureString !== ""}
onChange={event => {
setInMoistureString(event.target.value);
if (!isNaN(+event.target.value)) {
setValidInitial(true);
updateForm(
"inventory",
pond.BinInventory.create({
...form.inventory,
initialMoisture: +event.target.value
})
);
} else {
setValidInitial(false);
}
}}
/>
</Grid>
<Grid size={{ xs: 6 }} >
<TextField
label="Acceptable Deviation"
fullWidth
InputProps={{
endAdornment: <InputAdornment position="end">% EMC</InputAdornment>
}}
value={moistureTargetDeviation}
variant="outlined"
disabled={!canEdit}
className={classes.bottomSpacing}
error={isNaN(+moistureTargetDeviation) && moistureTargetDeviation !== ""}
onChange={event => {
setMoistureTargetDeviation(event.target.value);
if (!isNaN(+event.target.value)) {
setValidDeviation(true);
updateForm(
"inventory",
pond.BinInventory.create({
...form.inventory,
moistureTargetDeviation: +event.target.value
})
);
} else {
setValidDeviation(false);
}
}}
/>
</Grid>
</Grid>
</Box>
</Box>
);
};
const stepContent = (step: number) => {
switch (step) {
case 1:
return fanPicker();
case 2:
return inventoryForm();
case 3:
return targetForm();
default:
return generalForm();
}
};
const content = () => {
return (
<DialogContent style={{ padding: 0 }}>
<Box paddingX={2} paddingY={1}>
{!isMobile && mode === "add" && stepper()}
{stepContent(activeStep)}
</Box>
</DialogContent>
);
};
const nextButton = () => {
if (activeStep < 3) {
return (
<Button
size="small"
aria-label="next"
onClick={stepNext}
disabled={activeStep === steps.length - 1 || !stepComplete(activeStep)}>
Next
{theme.direction === "rtl" ? <KeyboardArrowLeft /> : <KeyboardArrowRight />}
</Button>
);
}
return (
<Button size="small" aria-label="next" onClick={addBin}>
Create
{theme.direction === "rtl" ? <KeyboardArrowLeft /> : <KeyboardArrowRight />}
</Button>
);
};
const actions = () => {
if (mode === "add" && isMobile) {
return (
<MobileStepper
variant="dots"
steps={steps.length}
position="static"
activeStep={activeStep}
nextButton={nextButton()}
backButton={
<Button size="small" aria-label="back" onClick={stepBack} disabled={activeStep === 0}>
{theme.direction === "rtl" ? <KeyboardArrowRight /> : <KeyboardArrowLeft />}
Back
</Button>
}
/>
);
}
return (
<DialogActions>
<Grid container justifyContent="space-between" direction="row" width="100%">
<Grid size={{ xs: 5 }}>
{mode === "add" ? (
<Button
disabled={activeStep === 0}
onClick={stepBack}
color="primary"
aria-label="Back">
Back
</Button>
) : (
canEdit && <DeleteButton onClick={() => setRemoveIsOpen(true)}>Delete</DeleteButton>
)}
</Grid>
<Grid size={{ xs: 7 }} container justifyContent="flex-end">
{mode === "add" ? (
<React.Fragment>
{!isMobile && (
<Button onClick={() => close()} color="primary" aria-label="Cancel">
Cancel
</Button>
)}
{activeStep === steps.length - 1 ? (
<Button
color="primary"
onClick={addBin}
disabled={!isFormValid()}
aria-label="Submit">
Submit
</Button>
) : (
<Button
color="primary"
onClick={stepNext}
disabled={activeStep === steps.length - 1 || !stepComplete(activeStep)}
aria-label="Next">
Next
</Button>
)}
</React.Fragment>
) : (
!isMobile && (
<React.Fragment>
<Button onClick={() => close()} color="primary" aria-label="Cancel">
Cancel
</Button>
<Button
onClick={updateBin}
color="primary"
disabled={!canEdit || !isFormValid()}
aria-label="Submit">
Submit
</Button>
</React.Fragment>
)
)}
</Grid>
</Grid>
</DialogActions>
);
};
const removeConfirmation = () => {
return (
<ResponsiveDialog
fullScreen={false}
open={removeIsOpen}
onClose={() => setRemoveIsOpen(false)}>
<DialogTitle>Delete {bin?.name() ?? "Bin"}?</DialogTitle>
<DialogContent>
<DialogContentText>
Anyone that has access to {bin?.name() ?? "this bin"} will no longer be able to see it.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setRemoveIsOpen(false)} color="primary">
Cancel
</Button>
<Button onClick={removeBin} color="primary">
Delete
</Button>
</DialogActions>
</ResponsiveDialog>
);
};
if ((!open || !initialized) && !removeIsOpen) {
return null;
}
return (
<ResponsiveDialog
maxWidth={"xs"}
fullWidth
fullScreen={isMobile}
open={open}
scroll="paper"
onClose={() => {
onClose();
setSelectedBinYard(null);
}}>
{title()}
{content()}
{actions()}
{removeConfirmation()}
{grainInventoryController()}
</ResponsiveDialog>
);
}