frontend/src/bin/BinSettings.tsx

2684 lines
101 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 { or } 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;
grainWeight: string;
grainType: Option | null;
grainUse: Option | null;
grainSubtype: string;
customTypeName: string;
highTemp: number;
lowTemp: number;
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: "",
grainWeight: "",
grainType: null,
grainUse: null,
grainSubtype: "",
customTypeName: "",
highTemp: 20,
lowTemp: 10,
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 [bushelConversion, setBushelConversion] = useState(0) //the number used to convert the bushels to either tonne or ton based on user preference
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 ton/ne is set so bins that don't have it in the settings yet can still do it before they get updated properly
const grain = GrainDescriber(
initForm.inventory.grainType
)
if (!initForm.inventory.bushelsPerTonne) {
initForm.inventory.bushelsPerTonne = grain.bushelsPerTonne;
}
if(user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE){
setBushelConversion(initForm.inventory.bushelsPerTonne)
}else if (user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
setBushelConversion(initForm.inventory.bushelsPerTonne * 0.907)
}
if (initForm.inventory.customGrain){
setCustomGrain(initForm.inventory.customGrain)
if(user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE){
setBushelConversion(initForm.inventory.customGrain.bushelsPerTonne)
}else if (user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
setBushelConversion(initForm.inventory.customGrain.bushelsPerTonne * 0.907)
}
}
//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 (user.distanceUnit() === 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 (user.tempUnit() === 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;
let weight = ""
if(initForm.inventory){
if(user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE){
weight = (gb/initForm.inventory.bushelsPerTonne).toFixed(2)
}else if (user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
weight = (gb/(initForm.inventory.bushelsPerTonne * 0.907)).toFixed(2)
}
if(initForm.inventory.customGrain){
if(user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE){
weight = (gb/initForm.inventory.customGrain.bushelsPerTonne).toFixed(2)
}else if (user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
weight = (gb/(initForm.inventory.customGrain.bushelsPerTonne * 0.907)).toFixed(2)
}
}
}
setForm(initForm);
setHighTempC(initForm.highTemp ?? 20);
setLowTempC(initForm.lowTemp ?? 10);
setFormExtension({
capacity: initForm.specs?.bushelCapacity.toString() ?? "",
height: h,
diameter: d,
grainBushels: gb.toFixed(2),
grainWeight: weight,
// grainTons: initForm.inventory?.bushelsPerTon
// ? (gb / initForm.inventory.bushelsPerTon).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
});
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(user.distanceUnit() === 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, user]);
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 = user.distanceUnit() === 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 = user.distanceUnit() === 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 inventoryAmount = () => {
const grainWeight = formExtension.grainWeight;
const binQuantity = formExtension.grainBushels;
//as long as the storage type is not fertilizer it is some sort of grain, whether it is supported or custom is not important in this instance so we do not need to check for unknown
if(storageType !== pond.BinStorage.BIN_STORAGE_FERTILIZER && ((user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE || user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TON) && bushelConversion !== 0 && grainWeight !== "")){
return (
<TextField
label="Amount"
value={grainWeight}
type="number"
onChange={event => {
let newWeight = event.target.value;
//get the bushel amount
if (!isNaN(+newWeight)) {
let newBushels = Math.round(+newWeight * bushelConversion * 100) / 100;
updateForm(
"inventory",
pond.BinInventory.create({
...form.inventory,
grainBushels: newBushels
})
);
//updateFormExtension("grainBushels", newBushels.toString())
updateFormExtension("grainWeight", newWeight);
}
}}
fullWidth
variant="outlined"
disabled={!canEdit}
InputProps={{
endAdornment: <InputAdornment position="end">{user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE ? "mT" : "t"}</InputAdornment>
}}
className={classes.bottomSpacing}
/>
)
}else{
return (
<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}
/>
)
}
}
const inventoryForm = () => {
const inventory = form.inventory;
const empty = inventory ? inventory.empty : false;
const binQuantity = formExtension.grainBushels;
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);
setBushelConversion(0)
} 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 = "";
setBushelConversion(0)
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">
{user.distanceUnit() === 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)
let conversion = 0
if(user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE){
conversion = newGrainSettings.bushelsPerTonne
}else if(user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
conversion = newGrainSettings.bushelsPerTonne * 0.907
}
updateForm(
"inventory",
pond.BinInventory.create({
...form.inventory,
bushelsPerTonne: newGrainSettings.bushelsPerTonne,
})
);
setBushelConversion(conversion)
if(conversion !== 0 && !isNaN(+binQuantity)){
updateFormExtension("grainWeight", (+binQuantity/conversion).toFixed(2))
}
}}/>
</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,
})
);
let conversion = 0
if(user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE){
conversion = GrainDescriber(newGrainType).bushelsPerTonne
}else if(user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
conversion = GrainDescriber(newGrainType).bushelsPerTon
}
setBushelConversion(conversion)
if(conversion !== 0 && !isNaN(+binQuantity)){
//updateFormExtension("grainWeight", (+binQuantity/conversion).toFixed(2))
let newWeight = (+binQuantity/conversion).toFixed(2)
formExtension.grainWeight = newWeight
}
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>
}
{inventoryAmount()}
{/* {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={grainWeight}
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("grainWeight", 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 (user.distanceUnit() === 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 (user.distanceUnit() === 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 (user.distanceUnit() === 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">
{user.distanceUnit() !== 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 (user.distanceUnit() === 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(user.distanceUnit() === 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">
{user.distanceUnit() !== 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 =
user.distanceUnit() === 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(user.distanceUnit() === 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 (user.distanceUnit() === 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(user.distanceUnit() === 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">
{user.distanceUnit() !== 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 =
user.distanceUnit() === 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 (user.distanceUnit() === 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">
{user.distanceUnit() !== 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 (user.distanceUnit() === 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">
{user.distanceUnit() !== 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 (user.distanceUnit() === 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 (user.tempUnit() === 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">
{user.tempUnit() === 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 (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
valueC = ((Number(value) - 32) * (5 / 9)).toFixed(2);
}
setLowTempC(+valueC);
updateFormExtension("lowTemp", value);
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{user.tempUnit() === 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 (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
valueC = ((Number(value) - 32) * (5 / 9)).toFixed(2);
}
setHighTempC(+valueC);
updateFormExtension("highTemp", value);
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{user.tempUnit() === 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>
);
}