2500 lines
93 KiB
TypeScript
2500 lines
93 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 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(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[]>([])
|
|
|
|
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 }),
|
|
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 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 ?? 0)
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
console.log(form)
|
|
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)
|
|
}}
|
|
/>
|
|
</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 && (
|
|
<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>
|
|
)}
|
|
{isCustomInventory ? (
|
|
<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}
|
|
/>
|
|
{storageType !== pond.BinStorage.BIN_STORAGE_FERTILIZER && (
|
|
<TextField
|
|
label="Bushels Per Tonne"
|
|
value={bushPerTonne}
|
|
type="number"
|
|
onChange={event => {
|
|
updateForm(
|
|
"inventory",
|
|
pond.BinInventory.create({
|
|
...form.inventory,
|
|
bushelsPerTonne: +event.target.value
|
|
})
|
|
);
|
|
updateFormExtension("bushelsPerTonne", event.target.value);
|
|
}}
|
|
fullWidth
|
|
variant="outlined"
|
|
className={classes.bottomSpacing}
|
|
/>
|
|
)}
|
|
</React.Fragment>
|
|
) : (
|
|
<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}
|
|
/>
|
|
)}
|
|
{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>
|
|
{!isCustomInventory && (
|
|
<Box marginTop={1}>
|
|
<Typography variant="body1" align="center" gutterBottom>
|
|
Moisture
|
|
</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>
|
|
);
|
|
}
|