added tons of files for component functionality; manual component adding added temporarily
This commit is contained in:
parent
58830d480e
commit
45ff49bcea
121 changed files with 25883 additions and 65 deletions
523
src/grain/GrainDescriber.ts
Normal file
523
src/grain/GrainDescriber.ts
Normal file
|
|
@ -0,0 +1,523 @@
|
|||
import BarleyImg from "assets/grain/barley.jpg";
|
||||
import CanolaImg from "assets/grain/canola.jpg";
|
||||
import CornImg from "assets/grain/corn.jpg";
|
||||
import FlaxImg from "assets/grain/flax.jpg";
|
||||
import LentilImg from "assets/grain/lentil.jpg";
|
||||
import OatImg from "assets/grain/oat.jpg";
|
||||
import PeaImg from "assets/grain/pea.jpg";
|
||||
import PeanutImg from "assets/grain/peanut.jpg";
|
||||
import RiceImg from "assets/grain/rice.jpg";
|
||||
import SorghumImg from "assets/grain/sorghum.jpg";
|
||||
import SoybeanImg from "assets/grain/soybean.jpg";
|
||||
import SunflowerImg from "assets/grain/sunflower.jpg";
|
||||
import WheatImg from "assets/grain/wheat.jpg";
|
||||
import { Option } from "common/SearchSelect";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { Equation } from "./GrainMoisture";
|
||||
|
||||
export interface GrainExtension {
|
||||
name: string;
|
||||
group: string;
|
||||
equation: Equation;
|
||||
a: number;
|
||||
b: number;
|
||||
c: number;
|
||||
deprecated?: boolean;
|
||||
setTempC: number; //drying temperature in degrees Celcius
|
||||
targetMC: number; //target moisture content % for grain elevators
|
||||
img?: string;
|
||||
colour: string;
|
||||
weightConversionKg: number;
|
||||
bushelsPerTonne: number;
|
||||
}
|
||||
|
||||
const defaultSetTemp = 30.0;
|
||||
|
||||
const defaultGrain: GrainExtension = {
|
||||
name: "None",
|
||||
group: "",
|
||||
equation: Equation.none,
|
||||
a: 0,
|
||||
b: 0,
|
||||
c: 0,
|
||||
setTempC: defaultSetTemp,
|
||||
targetMC: 15.0,
|
||||
colour: "#424242",
|
||||
weightConversionKg: 0,
|
||||
bushelsPerTonne: 0
|
||||
};
|
||||
|
||||
export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
|
||||
[pond.Grain.GRAIN_INVALID, defaultGrain],
|
||||
[pond.Grain.GRAIN_NONE, defaultGrain],
|
||||
[
|
||||
pond.Grain.GRAIN_CUSTOM,
|
||||
{
|
||||
name: "Custom Type",
|
||||
group: "",
|
||||
equation: Equation.none,
|
||||
a: 0,
|
||||
b: 0,
|
||||
c: 0,
|
||||
setTempC: defaultSetTemp,
|
||||
targetMC: 15.0,
|
||||
colour: "yellow",
|
||||
weightConversionKg: 0,
|
||||
bushelsPerTonne: 0
|
||||
}
|
||||
],
|
||||
[
|
||||
pond.Grain.GRAIN_BARLEY,
|
||||
{
|
||||
name: "Barley",
|
||||
group: "Barley",
|
||||
equation: Equation.chungPfost,
|
||||
a: 475.12,
|
||||
b: 0.14843,
|
||||
c: 71.996,
|
||||
setTempC: 33.0,
|
||||
targetMC: 14.9,
|
||||
img: BarleyImg,
|
||||
colour: "#27632a",
|
||||
weightConversionKg: 21.772657171369,
|
||||
bushelsPerTonne: 45.93
|
||||
}
|
||||
],
|
||||
[
|
||||
pond.Grain.GRAIN_BUCKWHEAT,
|
||||
{
|
||||
name: "Buckwheat",
|
||||
group: "Buckwheat",
|
||||
equation: Equation.chungPfost,
|
||||
a: 103540000,
|
||||
b: 0.1646,
|
||||
c: 15853000,
|
||||
deprecated: true,
|
||||
setTempC: defaultSetTemp,
|
||||
targetMC: 16.1,
|
||||
colour: "#46b298",
|
||||
weightConversionKg: 1,
|
||||
bushelsPerTonne: 45.93
|
||||
}
|
||||
],
|
||||
[
|
||||
pond.Grain.GRAIN_CANOLA,
|
||||
{
|
||||
name: "Canola",
|
||||
group: "Canola",
|
||||
equation: Equation.halsey,
|
||||
a: 3.489,
|
||||
b: -0.010553,
|
||||
c: 1.86,
|
||||
setTempC: 38.0,
|
||||
targetMC: 10.1,
|
||||
img: CanolaImg,
|
||||
// colour: "#cddc39",
|
||||
colour: "#ffff00",
|
||||
weightConversionKg: 22.67961461,
|
||||
bushelsPerTonne: 44.092
|
||||
}
|
||||
],
|
||||
[
|
||||
pond.Grain.GRAIN_RAPESEED,
|
||||
{
|
||||
name: "Rapeseed",
|
||||
group: "Canola",
|
||||
equation: Equation.halsey,
|
||||
a: 3.0026,
|
||||
b: -0.0048967,
|
||||
c: 1.7607,
|
||||
setTempC: 38.0,
|
||||
targetMC: 10.1,
|
||||
img: CanolaImg,
|
||||
colour: "#cddc39",
|
||||
weightConversionKg: 27.215537532,
|
||||
bushelsPerTonne: 44.092
|
||||
}
|
||||
],
|
||||
[
|
||||
pond.Grain.GRAIN_CORN,
|
||||
{
|
||||
name: "Corn",
|
||||
group: "Corn",
|
||||
equation: Equation.henderson,
|
||||
a: 0.000066612,
|
||||
b: 1.9677,
|
||||
c: 42.143,
|
||||
setTempC: 38.0,
|
||||
targetMC: 15.6,
|
||||
img: CornImg,
|
||||
colour: "#ffef62",
|
||||
weightConversionKg: 25.4014333665971,
|
||||
bushelsPerTonne: 39.368
|
||||
}
|
||||
],
|
||||
[
|
||||
pond.Grain.GRAIN_MAIZE_WHITE,
|
||||
{
|
||||
name: "Maize White",
|
||||
group: "Corn",
|
||||
equation: Equation.henderson,
|
||||
a: 0.000066612,
|
||||
b: 1.9677,
|
||||
c: 70.143,
|
||||
setTempC: 38.0,
|
||||
targetMC: 15.6,
|
||||
img: CornImg,
|
||||
colour: "#ffef62",
|
||||
weightConversionKg: 25.4014333665971,
|
||||
bushelsPerTonne: 39.368
|
||||
}
|
||||
],
|
||||
[
|
||||
pond.Grain.GRAIN_MAIZE_YELLOW,
|
||||
{
|
||||
name: "Maize Yellow",
|
||||
group: "Corn",
|
||||
equation: Equation.henderson,
|
||||
a: 0.000066612,
|
||||
b: 1.9677,
|
||||
c: 65.143,
|
||||
setTempC: 38.0,
|
||||
targetMC: 15.6,
|
||||
img: CornImg,
|
||||
colour: "#ffef62",
|
||||
weightConversionKg: 25.4014333665971,
|
||||
bushelsPerTonne: 39.368
|
||||
}
|
||||
],
|
||||
[
|
||||
pond.Grain.GRAIN_OATS,
|
||||
{
|
||||
name: "Oats",
|
||||
group: "Oats",
|
||||
equation: Equation.henderson,
|
||||
a: 0.000085511,
|
||||
b: 2.0087,
|
||||
c: 37.811,
|
||||
setTempC: 32.0,
|
||||
targetMC: 13.6,
|
||||
img: OatImg,
|
||||
colour: "#79955a",
|
||||
weightConversionKg: 15.4222988297197,
|
||||
bushelsPerTonne: 64.842
|
||||
}
|
||||
],
|
||||
[
|
||||
pond.Grain.GRAIN_PEANUTS,
|
||||
{
|
||||
name: "Peanuts",
|
||||
group: "Peanuts",
|
||||
equation: Equation.oswin,
|
||||
a: 8.6588,
|
||||
b: -0.057904,
|
||||
c: 2.6204,
|
||||
setTempC: defaultSetTemp,
|
||||
targetMC: 10.5,
|
||||
img: PeanutImg,
|
||||
colour: "#b2a058",
|
||||
weightConversionKg: 23.3124,
|
||||
bushelsPerTonne: 105.263
|
||||
}
|
||||
],
|
||||
[
|
||||
pond.Grain.GRAIN_RICE_LONG,
|
||||
{
|
||||
name: "Long Grain Rice",
|
||||
group: "Rice",
|
||||
equation: Equation.henderson,
|
||||
a: 0.000041276,
|
||||
b: 2.1191,
|
||||
c: 49.828,
|
||||
setTempC: defaultSetTemp,
|
||||
targetMC: 12,
|
||||
img: RiceImg,
|
||||
colour: "#7c9c99",
|
||||
weightConversionKg: 28.439,
|
||||
bushelsPerTonne: 49.02
|
||||
}
|
||||
],
|
||||
[
|
||||
pond.Grain.GRAIN_RICE_MEDIUM,
|
||||
{
|
||||
name: "Medium Grain Rice",
|
||||
group: "Rice",
|
||||
equation: Equation.henderson,
|
||||
a: 0.000035502,
|
||||
b: 2.31,
|
||||
c: 27.396,
|
||||
setTempC: defaultSetTemp,
|
||||
targetMC: 12,
|
||||
img: RiceImg,
|
||||
colour: "#7c9c99",
|
||||
weightConversionKg: 29.979,
|
||||
bushelsPerTonne: 49.02
|
||||
}
|
||||
],
|
||||
[
|
||||
pond.Grain.GRAIN_RICE_SHORT,
|
||||
{
|
||||
name: "Short Grain Rice",
|
||||
group: "Rice",
|
||||
equation: Equation.henderson,
|
||||
a: 0.000048524,
|
||||
b: 2.0794,
|
||||
c: 45.646,
|
||||
setTempC: defaultSetTemp,
|
||||
targetMC: 12,
|
||||
img: RiceImg,
|
||||
colour: "#7c9c99",
|
||||
weightConversionKg: 30.744,
|
||||
bushelsPerTonne: 49.02
|
||||
}
|
||||
],
|
||||
[
|
||||
pond.Grain.GRAIN_SORGHUM,
|
||||
{
|
||||
name: "Sorghum",
|
||||
group: "Sorghum",
|
||||
equation: Equation.chungPfost,
|
||||
a: 797.33,
|
||||
b: 0.18159,
|
||||
c: 52.238,
|
||||
setTempC: defaultSetTemp,
|
||||
targetMC: 12.0,
|
||||
img: SorghumImg,
|
||||
colour: "#829baf",
|
||||
weightConversionKg: 25.4014333665971,
|
||||
bushelsPerTonne: 39.368
|
||||
}
|
||||
],
|
||||
[
|
||||
pond.Grain.GRAIN_SOYBEANS,
|
||||
{
|
||||
name: "Soybeans",
|
||||
group: "Soybeans",
|
||||
equation: Equation.chungPfost,
|
||||
a: 228.2,
|
||||
b: 0.2072,
|
||||
c: 30,
|
||||
setTempC: defaultSetTemp,
|
||||
targetMC: 14.1,
|
||||
img: SoybeanImg,
|
||||
colour: "#7d6d99",
|
||||
weightConversionKg: 27.2158214642112,
|
||||
bushelsPerTonne: 36.744
|
||||
}
|
||||
],
|
||||
[
|
||||
pond.Grain.GRAIN_SUNFLOWER,
|
||||
{
|
||||
name: "Sunflower",
|
||||
group: "Sunflower",
|
||||
equation: Equation.henderson,
|
||||
a: 0.00031,
|
||||
b: 1.7459,
|
||||
c: 66.603,
|
||||
setTempC: defaultSetTemp,
|
||||
targetMC: 9.6,
|
||||
img: SunflowerImg,
|
||||
colour: "#ffab40",
|
||||
weightConversionKg: 13.6079107321056,
|
||||
bushelsPerTonne: 73.487
|
||||
}
|
||||
],
|
||||
[
|
||||
pond.Grain.GRAIN_WHEAT_DURUM,
|
||||
{
|
||||
name: "Durum Wheat",
|
||||
group: "Wheat",
|
||||
equation: Equation.oswin,
|
||||
a: 13.101,
|
||||
b: -0.052626,
|
||||
c: 2.9987,
|
||||
setTempC: 37.0,
|
||||
targetMC: 14.6,
|
||||
img: WheatImg,
|
||||
colour: "#46b298",
|
||||
weightConversionKg: 27.2158214642112,
|
||||
bushelsPerTonne: 36.744
|
||||
}
|
||||
],
|
||||
[
|
||||
pond.Grain.GRAIN_WHEAT_HARD_RED,
|
||||
{
|
||||
name: "Hard Red Wheat",
|
||||
group: "Wheat",
|
||||
equation: Equation.chungPfost,
|
||||
a: 610.34,
|
||||
b: 0.15526,
|
||||
c: 93.213,
|
||||
setTempC: 37.0,
|
||||
targetMC: 14.6,
|
||||
img: WheatImg,
|
||||
colour: "#46b298",
|
||||
weightConversionKg: 27.2158214642112,
|
||||
bushelsPerTonne: 36.744
|
||||
}
|
||||
],
|
||||
[
|
||||
pond.Grain.GRAIN_WHEAT_SAWOS,
|
||||
{
|
||||
name: "Wheat SAWOS",
|
||||
group: "Wheat",
|
||||
equation: Equation.chungPfost,
|
||||
a: 610.34,
|
||||
b: 0.15526,
|
||||
c: 93.213,
|
||||
setTempC: 37.0,
|
||||
targetMC: 14.6,
|
||||
img: WheatImg,
|
||||
colour: "#46b298",
|
||||
weightConversionKg: 27.2158214642112,
|
||||
bushelsPerTonne: 36.744
|
||||
}
|
||||
],
|
||||
[
|
||||
pond.Grain.GRAIN_FLAX_UNRETTED,
|
||||
{
|
||||
name: "Un-retted Flax",
|
||||
group: "Flax",
|
||||
equation: Equation.halsey,
|
||||
a: 5.11,
|
||||
b: Math.pow(-8.46 * 10, -3),
|
||||
c: 2.26,
|
||||
setTempC: 33.0,
|
||||
targetMC: 10.1,
|
||||
img: FlaxImg,
|
||||
colour: "#6e6d19",
|
||||
weightConversionKg: 25.4014333665971,
|
||||
bushelsPerTonne: 39.368
|
||||
}
|
||||
],
|
||||
[
|
||||
pond.Grain.GRAIN_FLAX_DEW_RETTED,
|
||||
{
|
||||
name: "Dew-retted Flax",
|
||||
group: "Flax",
|
||||
equation: Equation.oswin,
|
||||
a: 6.5,
|
||||
b: Math.pow(-1.68 * 10, -2),
|
||||
c: 3.2,
|
||||
setTempC: 33.0,
|
||||
targetMC: 10.1,
|
||||
img: FlaxImg,
|
||||
colour: "#6e6d19",
|
||||
weightConversionKg: 25.4014333665971,
|
||||
bushelsPerTonne: 39.368
|
||||
}
|
||||
],
|
||||
[
|
||||
pond.Grain.GRAIN_YELLOW_PEAS,
|
||||
{
|
||||
name: "Yellow Peas",
|
||||
group: "Peas",
|
||||
equation: Equation.oswin,
|
||||
a: 14.81,
|
||||
b: -0.109,
|
||||
c: 3.019,
|
||||
setTempC: 32.0,
|
||||
targetMC: 16.1,
|
||||
img: PeaImg,
|
||||
colour: "#ffe100",
|
||||
weightConversionKg: 27.2158214642112,
|
||||
bushelsPerTonne: 36.744
|
||||
}
|
||||
],
|
||||
[
|
||||
pond.Grain.GRAIN_LENTILS_BLAZE,
|
||||
{
|
||||
name: "Blaze Lentils",
|
||||
group: "Lentils",
|
||||
equation: Equation.halsey,
|
||||
a: 5.39,
|
||||
b: -0.015,
|
||||
c: 2.273,
|
||||
setTempC: 30.0,
|
||||
targetMC: 13.1,
|
||||
img: LentilImg,
|
||||
colour: "#cb5e3c",
|
||||
weightConversionKg: 27.2158214642112,
|
||||
bushelsPerTonne: 36.744
|
||||
}
|
||||
],
|
||||
[
|
||||
pond.Grain.GRAIN_LENTILS_REDBERRY,
|
||||
{
|
||||
name: "Redberry Lentils",
|
||||
group: "Lentils",
|
||||
equation: Equation.halsey,
|
||||
a: 4.749,
|
||||
b: -0.0116,
|
||||
c: 2.066,
|
||||
setTempC: 30.0,
|
||||
targetMC: 14.1,
|
||||
img: LentilImg,
|
||||
colour: "#cb5e3c",
|
||||
weightConversionKg: 27.2158214642112,
|
||||
bushelsPerTonne: 36.744
|
||||
}
|
||||
],
|
||||
[
|
||||
pond.Grain.GRAIN_LENTILS_ROBIN,
|
||||
{
|
||||
name: "Robin Lentils",
|
||||
group: "Lentils",
|
||||
equation: Equation.halsey,
|
||||
a: 5.176,
|
||||
b: -0.0065,
|
||||
c: 2.337,
|
||||
setTempC: 30.0,
|
||||
targetMC: 14.1,
|
||||
img: LentilImg,
|
||||
colour: "#cb5e3c",
|
||||
weightConversionKg: 27.2158214642112,
|
||||
bushelsPerTonne: 36.744
|
||||
}
|
||||
]
|
||||
]);
|
||||
|
||||
export function GetGrainExtensionMap(): Map<pond.Grain, GrainExtension> {
|
||||
return cloneDeep(GrainExtensions);
|
||||
}
|
||||
|
||||
export default function GrainDescriber(type: pond.Grain): GrainExtension {
|
||||
let describer = GrainExtensions.get(type);
|
||||
//console.log(describer)
|
||||
if (describer?.name === "None") {
|
||||
//console.trace()
|
||||
}
|
||||
return describer ? describer : defaultGrain;
|
||||
}
|
||||
|
||||
export function grainName(type: pond.Grain): string {
|
||||
return GrainDescriber(type).name;
|
||||
}
|
||||
|
||||
export function GrainColour(type: pond.Grain): string {
|
||||
return GrainDescriber(type).colour;
|
||||
}
|
||||
|
||||
export const ToGrainOption = (grainType: pond.Grain): Option => {
|
||||
let grainDescriber = GrainDescriber(grainType);
|
||||
return {
|
||||
value: pond.Grain[grainType],
|
||||
label: grainDescriber.name,
|
||||
group: grainDescriber.group
|
||||
} as Option;
|
||||
};
|
||||
|
||||
export function GrainOptions(): Option[] {
|
||||
let options: Option[] = [];
|
||||
Object.values(pond.Grain).forEach(grainType => {
|
||||
if (
|
||||
typeof grainType !== "string" &&
|
||||
grainType !== pond.Grain.GRAIN_INVALID &&
|
||||
grainType !== pond.Grain.GRAIN_CUSTOM
|
||||
) {
|
||||
options.push(ToGrainOption(grainType));
|
||||
}
|
||||
});
|
||||
return options;
|
||||
}
|
||||
389
src/grain/GrainDryingCalculator.tsx
Normal file
389
src/grain/GrainDryingCalculator.tsx
Normal file
|
|
@ -0,0 +1,389 @@
|
|||
import {
|
||||
Alert,
|
||||
AlertTitle,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardActions,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
darken,
|
||||
Divider,
|
||||
Grid2 as Grid,
|
||||
InputAdornment,
|
||||
Skeleton,
|
||||
Step,
|
||||
StepLabel,
|
||||
Stepper,
|
||||
TextField,
|
||||
Theme,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
// import { Alert, AlertTitle, Skeleton } from "@material-ui/lab";
|
||||
import { GrainOptions, MoistureToHumidity } from "grain";
|
||||
import GrainDryingChart, { GrainDryingPoint } from "charts/GrainDryingChart";
|
||||
import SearchSelect, { Option } from "common/SearchSelect";
|
||||
import { DateRange, GetDefaultDateRange } from "common/time/DateRange";
|
||||
import DateSelect from "common/time/DateSelect";
|
||||
import { useComponentAPI } from "hooks";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { Component, Device } from "models";
|
||||
import moment, { Moment } from "moment";
|
||||
import { extension } from "pbHelpers/ComponentType";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { quack } from "protobuf-ts/quack";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FindPlenumSensors } from "./GrainUtils";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
const bg = darken(theme.palette.background.paper, theme.palette.mode === "light" ? 0.1 : 0.15);
|
||||
|
||||
return ({
|
||||
card: {
|
||||
backgroundColor: bg
|
||||
},
|
||||
stepper: {
|
||||
padding: 0
|
||||
},
|
||||
header: {
|
||||
paddingBottom: theme.spacing(1)
|
||||
},
|
||||
actions: {
|
||||
paddingTop: theme.spacing(1)
|
||||
},
|
||||
gutter: {
|
||||
padding: `${theme.spacing(1)}px ${theme.spacing(2)}px`
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
interface Props {
|
||||
devices: Device[];
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const devicesToOptions = (devices: Device[]): Option[] => {
|
||||
return devices.map(d => {
|
||||
return { value: d.id().toString(), label: d.name() } as Option;
|
||||
});
|
||||
};
|
||||
|
||||
//NOREF: has no references in codebase, is this not used anywhere?
|
||||
export default function GrainDryingCalculator(props: Props) {
|
||||
const classes = useStyles();
|
||||
const componentAPI = useComponentAPI();
|
||||
const grainOptions = GrainOptions();
|
||||
const { devices, loading } = props;
|
||||
const [device, setDevice] = useState<Option | undefined>(undefined);
|
||||
const [deviceOptions, setDeviceOptions] = useState(devicesToOptions(devices));
|
||||
const [components, setComponents] = useState<Component[]>([]);
|
||||
const [componentOptions, setComponentOptions] = useState<Option[]>([]);
|
||||
const [component, setComponent] = useState<Option | undefined>(undefined);
|
||||
const [dateRange, setDateRange] = useState<DateRange>(GetDefaultDateRange());
|
||||
const [grainType, setGrainType] = useState<Option | undefined>(
|
||||
grainOptions.find(o => o.value === pond.Grain[pond.Grain.GRAIN_NONE])
|
||||
);
|
||||
const [rawGrainMoisture, setRawGrainMoisture] = useState<string>("15.0");
|
||||
const [samples, setSamples] = useState<pond.Measurement[]>([]);
|
||||
const [componentsLoading, setComponentsLoading] = useState(false);
|
||||
const [samplesLoading, setSamplesLoading] = useState(false);
|
||||
const steps = [0, 1, 2];
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const [chartData, setChartData] = useState<GrainDryingPoint[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setDeviceOptions(devicesToOptions(devices));
|
||||
}, [devices]);
|
||||
|
||||
useEffect(() => {
|
||||
let options = FindPlenumSensors(components).map(
|
||||
c =>
|
||||
({
|
||||
value: c.key(),
|
||||
label: c.name()
|
||||
} as Option)
|
||||
);
|
||||
setComponentOptions(options);
|
||||
setComponent(options[0] ? options[0] : undefined);
|
||||
}, [components]);
|
||||
|
||||
useEffect(() => {
|
||||
if (device) {
|
||||
const deviceID = Number(device.value);
|
||||
setComponentsLoading(true);
|
||||
componentAPI
|
||||
.list(deviceID)
|
||||
.then(response => {
|
||||
let data = response.data.components ? response.data.components : [];
|
||||
let rComponents: Component[] = data.map((c: any) => {
|
||||
return Component.create(pond.Component.fromObject(c));
|
||||
});
|
||||
setComponents(rComponents);
|
||||
})
|
||||
.catch(() => {
|
||||
setComponents([]);
|
||||
})
|
||||
.finally(() => {
|
||||
setComponentsLoading(false);
|
||||
});
|
||||
}
|
||||
}, [componentAPI, device]);
|
||||
|
||||
useEffect(() => {
|
||||
if (device && component) {
|
||||
const deviceID = Number(device.value);
|
||||
const componentKey = String(component.value);
|
||||
setSamplesLoading(true);
|
||||
componentAPI
|
||||
.sampleMeasurements(deviceID, componentKey, dateRange.start, dateRange.end, 500)
|
||||
.then(response => {
|
||||
let measurements: pond.Measurement[] | undefined = response.data.measurements;
|
||||
let newSamples = measurements
|
||||
? cloneDeep(measurements).sort((a, b) => {
|
||||
return b.timestamp > a.timestamp ? 1 : -1;
|
||||
})
|
||||
: [];
|
||||
|
||||
setSamples(newSamples);
|
||||
})
|
||||
.catch(() => {
|
||||
setSamples([]);
|
||||
})
|
||||
.finally(() => {
|
||||
setSamplesLoading(false);
|
||||
});
|
||||
}
|
||||
}, [componentAPI, device, component, dateRange]);
|
||||
|
||||
// T = temperature im degrees Celsius
|
||||
// RH = relative humidity (%)
|
||||
const calculateVPD = (T: number, RH: number) => {
|
||||
const es = 0.6108 * Math.exp((17.27 * T) / (T + 237.3));
|
||||
const ea = (RH / 100) * es;
|
||||
return ea - es;
|
||||
};
|
||||
|
||||
const calculateEffectiveVPD = (initialVPD: number, curVPD: number) => {
|
||||
return initialVPD - curVPD;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let data: GrainDryingPoint[] = [];
|
||||
let curComponent = component
|
||||
? components.find(c => c.key() === component.value.toString())
|
||||
: undefined;
|
||||
if (curComponent) {
|
||||
let ext = extension(curComponent.settings.type, curComponent.settings.subtype);
|
||||
const grainMoisture = parseFloat(rawGrainMoisture) || 15.0;
|
||||
samples.forEach(s => {
|
||||
let measurement = s.measurement;
|
||||
if (measurement) {
|
||||
let T = 0;
|
||||
let RH = 0;
|
||||
ext.measurements.forEach(cm => {
|
||||
if (cm.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) {
|
||||
T = cm.extract(measurement as quack.Measurement);
|
||||
}
|
||||
if (cm.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT) {
|
||||
RH = cm.extract(measurement as quack.Measurement);
|
||||
}
|
||||
});
|
||||
|
||||
const grain = grainType
|
||||
? pond.Grain[grainType.value as keyof typeof pond.Grain]
|
||||
: pond.Grain.GRAIN_NONE;
|
||||
let initialRH = MoistureToHumidity(grain, T, grainMoisture);
|
||||
let initialVPD = calculateVPD(T, initialRH);
|
||||
let vpd = calculateVPD(T, RH);
|
||||
data.push({
|
||||
timestamp: moment(s.timestamp).valueOf(),
|
||||
dryScore: calculateEffectiveVPD(initialVPD, vpd)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setChartData(data);
|
||||
}, [samples, rawGrainMoisture, grainType, component, components]);
|
||||
|
||||
const grainMoistureInvalid = () => {
|
||||
return rawGrainMoisture === "" || isNaN(Number(rawGrainMoisture));
|
||||
};
|
||||
|
||||
const initialSetup = () => {
|
||||
const invalidGrainMoisture = grainMoistureInvalid();
|
||||
return (
|
||||
<Box marginX={1}>
|
||||
<Typography variant="subtitle1" color="textSecondary" gutterBottom align="center">
|
||||
Please input your grain type and moisture
|
||||
</Typography>
|
||||
<Box paddingY={1}>
|
||||
<SearchSelect
|
||||
selected={grainType}
|
||||
changeSelection={o => setGrainType(o ? o : undefined)}
|
||||
label="Grain Type"
|
||||
options={grainOptions}
|
||||
group
|
||||
/>
|
||||
</Box>
|
||||
<Box paddingY={1}>
|
||||
<TextField
|
||||
value={rawGrainMoisture}
|
||||
onChange={event => setRawGrainMoisture(event.target.value)}
|
||||
fullWidth
|
||||
label="Grain Moisture"
|
||||
variant="outlined"
|
||||
error={invalidGrainMoisture}
|
||||
helperText={invalidGrainMoisture ? "Invalid number" : ""}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<InputAdornment position="end">% WB</InputAdornment>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const setupChart = () => {
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="subtitle1" color="textSecondary" gutterBottom align="center">
|
||||
Please select your plenum moisture sensor
|
||||
</Typography>{" "}
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<SearchSelect
|
||||
label="Device"
|
||||
selected={device}
|
||||
disabled={loading}
|
||||
options={deviceOptions}
|
||||
changeSelection={option => setDevice(option ? option : undefined)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<SearchSelect
|
||||
label="Moisture Sensor"
|
||||
selected={component}
|
||||
options={componentOptions}
|
||||
disabled={!device}
|
||||
loading={componentsLoading}
|
||||
changeSelection={option => setComponent(option ? option : undefined)}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const analyzeChart = () => {
|
||||
return (
|
||||
<Box>
|
||||
{samplesLoading ? (
|
||||
<Skeleton variant="rectangular" />
|
||||
) : chartData.length > 1 ? (
|
||||
<GrainDryingChart data={chartData} />
|
||||
) : (
|
||||
<Alert severity="info" variant="outlined">
|
||||
<AlertTitle>No data to display graph</AlertTitle>
|
||||
<Box>Tips:</Box>
|
||||
<Box> - increase the time range</Box>
|
||||
<Box> - select an active moisture sensor</Box>
|
||||
</Alert>
|
||||
)}
|
||||
<DateSelect
|
||||
updateDateRange={(newStart: Moment, newEnd: Moment) =>
|
||||
setDateRange({ start: newStart, end: newEnd })
|
||||
}
|
||||
startDate={dateRange.start}
|
||||
endDate={dateRange.end}
|
||||
/>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
Effective Vapor Pressure Deficit (VPD) represents the potential for moisture to be added
|
||||
or removed
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const stepContent = () => {
|
||||
switch (activeStep) {
|
||||
case 1:
|
||||
return setupChart();
|
||||
case 2:
|
||||
return analyzeChart();
|
||||
default:
|
||||
return initialSetup();
|
||||
}
|
||||
};
|
||||
|
||||
const stepComplete = (): boolean => {
|
||||
switch (activeStep) {
|
||||
case 0:
|
||||
return grainType !== undefined && !grainMoistureInvalid();
|
||||
case 1:
|
||||
return device !== undefined && component !== undefined;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const actions = () => {
|
||||
return (
|
||||
<CardActions className={classes.actions}>
|
||||
<Box display="flex" width="100%" justifyContent="space-between">
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={() => setActiveStep(activeStep - 1)}
|
||||
disabled={activeStep <= 0}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={() => setActiveStep(activeStep + 1)}
|
||||
disabled={activeStep >= steps.length - 1 || !stepComplete()}>
|
||||
Next
|
||||
</Button>
|
||||
</Box>
|
||||
</CardActions>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) return <Skeleton variant="rectangular" width="100%" height="300px" />;
|
||||
|
||||
return (
|
||||
<Card elevation={4} className={classes.card}>
|
||||
<CardHeader
|
||||
title="Grain Drying Calculator"
|
||||
subheader={
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Find out how well your grain is drying
|
||||
</Typography>
|
||||
}
|
||||
className={classes.header}
|
||||
/>
|
||||
<CardContent className={classes.gutter}>
|
||||
<Stepper activeStep={activeStep} className={classes.stepper} alternativeLabel>
|
||||
<Step key={0}>
|
||||
<StepLabel>Grain Moisture</StepLabel>
|
||||
</Step>
|
||||
<Step key={1}>
|
||||
<StepLabel>Select Plenum</StepLabel>
|
||||
</Step>
|
||||
<Step key={2}>
|
||||
<StepLabel>Analyze</StepLabel>
|
||||
</Step>
|
||||
</Stepper>
|
||||
<Box paddingY={1}>
|
||||
<Divider />
|
||||
</Box>
|
||||
<Box paddingTop={1}>{stepContent()}</Box>
|
||||
</CardContent>
|
||||
{actions()}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
385
src/grain/GrainInventory.tsx
Normal file
385
src/grain/GrainInventory.tsx
Normal file
|
|
@ -0,0 +1,385 @@
|
|||
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||
import { Bin, Field, HarvestYear } from "models";
|
||||
import React, { useCallback } from "react";
|
||||
import { useFieldAPI, useBinAPI, useHarvestYearAPI, useGlobalState } from "providers";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
MenuItem,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
TextField
|
||||
} from "@material-ui/core";
|
||||
import { useState } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { useSnackbar } from "hooks";
|
||||
import moment from "moment";
|
||||
import GrainDescriber from "./GrainDescriber";
|
||||
|
||||
interface Props {
|
||||
activeBinSettings: pond.BinSettings;
|
||||
newGrainInventory: number;
|
||||
dialogControl: boolean;
|
||||
onClose(cancel?: boolean): void;
|
||||
afterUpdate?(secondBin?: Bin): void;
|
||||
}
|
||||
|
||||
//this component is going to be deprecated by grain transaction
|
||||
export default function GrainInventory(props: Props) {
|
||||
const { activeBinSettings, newGrainInventory, dialogControl, onClose, afterUpdate } = props;
|
||||
const binAPI = useBinAPI();
|
||||
const fieldAPI = useFieldAPI();
|
||||
const hYearAPI = useHarvestYearAPI();
|
||||
const [{ as }] = useGlobalState();
|
||||
const [binIndex, setBinIndex] = useState(0);
|
||||
const [fieldIndex, setFieldIndex] = useState<number>(-1);
|
||||
const [yearIndex, setYearIndex] = useState(0);
|
||||
const [bins, setBins] = useState<Bin[]>([]);
|
||||
const [fields, setFields] = useState<Field[]>([]);
|
||||
const [hYears, setHYears] = useState<Map<number, HarvestYear>>(new Map<number, HarvestYear>());
|
||||
const [currentBushels, setCurrentBushels] = useState(0);
|
||||
const { openSnack } = useSnackbar();
|
||||
const [increaseMode, setIncreaseMode] = useState<"correction" | "purchased" | "grown">(
|
||||
"correction"
|
||||
);
|
||||
const [decreaseMode, setDecreaseMode] = useState<"correction" | "sold" | "moved">("correction");
|
||||
|
||||
useEffect(() => {
|
||||
if (activeBinSettings.inventory && dialogControl) {
|
||||
setCurrentBushels(activeBinSettings.inventory.grainBushels);
|
||||
}
|
||||
}, [activeBinSettings, dialogControl]);
|
||||
|
||||
const loadBins = useCallback(() => {
|
||||
if (!dialogControl) return;
|
||||
binAPI
|
||||
.listBins(50, 0, "asc", "name", undefined, as)
|
||||
.then(resp => {
|
||||
setBins(resp.data.bins.map(b => Bin.any(b)));
|
||||
})
|
||||
.catch();
|
||||
}, [binAPI, as, dialogControl]);
|
||||
|
||||
const loadHYears = useCallback(() => {
|
||||
if (fields.length > 0 && fieldIndex > -1) {
|
||||
hYearAPI.listHarvestYears(50, 0, "asc", "year", fields[fieldIndex].fieldName()).then(resp => {
|
||||
let tempMap = new Map<number, HarvestYear>();
|
||||
resp.data.harvestYear.forEach(hy => {
|
||||
let tempHY = HarvestYear.any(hy);
|
||||
tempMap.set(tempHY.year(), tempHY);
|
||||
});
|
||||
setHYears(tempMap);
|
||||
});
|
||||
}
|
||||
}, [hYearAPI, fields, fieldIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
loadHYears();
|
||||
}, [loadHYears]);
|
||||
|
||||
useEffect(() => {
|
||||
loadBins();
|
||||
fieldAPI
|
||||
.listFields(500, 0, "asc", "fieldName", undefined, as)
|
||||
.then(resp => {
|
||||
setFields(resp.data.fields.map(f => Field.any(f)));
|
||||
})
|
||||
.catch();
|
||||
}, [loadBins, fieldAPI, as]);
|
||||
|
||||
const grainName = (bin: Bin) => {
|
||||
return bin.storage() === pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN
|
||||
? GrainDescriber(bin.grain()).name
|
||||
: bin.customType();
|
||||
};
|
||||
|
||||
const dialogContent = () => {
|
||||
if (newGrainInventory < currentBushels) {
|
||||
return (
|
||||
<Box>
|
||||
<FormControl>
|
||||
<RadioGroup value={decreaseMode} name="radio-buttons-group">
|
||||
<FormControlLabel
|
||||
value="correction"
|
||||
control={<Radio />}
|
||||
label="Correction"
|
||||
onChange={() => setDecreaseMode("correction")}
|
||||
/>
|
||||
{/* Doesnt Actually Do Anything yet */}
|
||||
{/* <FormControlLabel
|
||||
value="sold"
|
||||
control={<Radio />}
|
||||
label="Sold"
|
||||
onChange={() => setDecreaseMode("sold")}
|
||||
/> */}
|
||||
<FormControlLabel
|
||||
value="moved"
|
||||
control={<Radio />}
|
||||
label="Moved"
|
||||
onChange={() => setDecreaseMode("moved")}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
{decreaseMode === "moved" && (
|
||||
<TextField
|
||||
id="bins"
|
||||
fullWidth
|
||||
margin="normal"
|
||||
select
|
||||
label="Bin grain moved to"
|
||||
value={binIndex}
|
||||
onChange={e => setBinIndex(+e.target.value)}>
|
||||
{bins.map((option, i) => (
|
||||
<MenuItem key={i} value={i} disabled={option.key() === activeBinSettings.key}>
|
||||
{option.name()} - {grainName(option)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Box>
|
||||
<FormControl>
|
||||
<RadioGroup value={increaseMode} name="radio-buttons-group">
|
||||
<FormControlLabel
|
||||
value="correction"
|
||||
control={<Radio />}
|
||||
label="Correction"
|
||||
onChange={() => setIncreaseMode("correction")}
|
||||
/>
|
||||
{/* Doesnt Actually Do Anything yet */}
|
||||
{/* <FormControlLabel
|
||||
value="purchased"
|
||||
control={<Radio />}
|
||||
label="Purchased"
|
||||
onChange={() => setIncreaseMode("purchased")}
|
||||
/> */}
|
||||
<FormControlLabel
|
||||
value="grown"
|
||||
control={<Radio />}
|
||||
label="Grown"
|
||||
onChange={() => setIncreaseMode("grown")}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
{increaseMode === "grown" && (
|
||||
<Box>
|
||||
<TextField
|
||||
id="fields"
|
||||
fullWidth
|
||||
margin="normal"
|
||||
select
|
||||
label="Field"
|
||||
helperText="Select the field that provided the grain"
|
||||
value={fieldIndex}
|
||||
onChange={e => setFieldIndex(+e.target.value)}>
|
||||
<MenuItem key="noField" value={-1}>
|
||||
No Field Selected
|
||||
</MenuItem>
|
||||
{fields.map((option, i) => (
|
||||
<MenuItem key={i} value={i}>
|
||||
{option.fieldName()}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<TextField
|
||||
id="harvestYear"
|
||||
fullWidth
|
||||
margin="normal"
|
||||
select
|
||||
label="Harvest Year"
|
||||
helperText="Select which Year to track"
|
||||
value={yearIndex}
|
||||
onChange={e => setYearIndex(+e.target.value)}
|
||||
disabled={fieldIndex < 0}>
|
||||
<MenuItem key={0} value={0}>
|
||||
New Harvest Year
|
||||
</MenuItem>
|
||||
{Array.from(hYears.values()).length > 0 &&
|
||||
Array.from(hYears.values()).map(option => (
|
||||
<MenuItem key={option.year()} value={option.year()}>
|
||||
{option.year()}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const increaseInv = () => {
|
||||
let bin = activeBinSettings;
|
||||
let binInv = pond.BinInventory.fromObject({
|
||||
...bin.inventory
|
||||
});
|
||||
if (binInv.grainBushels === 0 && bin.mode === pond.BinMode.BIN_MODE_NONE) {
|
||||
bin.mode = pond.BinMode.BIN_MODE_STORAGE;
|
||||
}
|
||||
binInv.grainBushels = newGrainInventory;
|
||||
|
||||
if (fields.length > 0 && fieldIndex > -1) {
|
||||
if (fields[fieldIndex].crop() !== activeBinSettings.inventory?.grainType) {
|
||||
openSnack("Grain type of this bin does not match the Field");
|
||||
return;
|
||||
} else if (yearIndex === 0 && hYears.has(moment().year())) {
|
||||
openSnack("Current year already exists for this field");
|
||||
return;
|
||||
}
|
||||
binInv.grainSubtype = fields[fieldIndex].subtype();
|
||||
}
|
||||
|
||||
bin.inventory = binInv;
|
||||
|
||||
binAPI
|
||||
.updateBin(activeBinSettings.key, bin)
|
||||
.then(() => {
|
||||
if (increaseMode === "grown") {
|
||||
if (yearIndex === 0) {
|
||||
let hy = HarvestYear.create();
|
||||
hy.settings.field = fields[fieldIndex].fieldName();
|
||||
hy.settings.grain = fields[fieldIndex].crop();
|
||||
hy.settings.year = moment().year();
|
||||
hy.settings.bushels = newGrainInventory - currentBushels;
|
||||
hYearAPI.addHarvestYear(hy.settings).then(resp => {
|
||||
loadHYears();
|
||||
});
|
||||
} else {
|
||||
let hy = hYears.get(yearIndex);
|
||||
if (hy) {
|
||||
hy.settings.bushels = hy.totalBushels() + (newGrainInventory - currentBushels);
|
||||
hYearAPI.updateHarvestYear(hy.key(), hy.settings).then(resp => {
|
||||
loadHYears();
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (increaseMode === "purchased") {
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
openSnack("Failed to update grain inventory");
|
||||
})
|
||||
.finally(() => {
|
||||
if (afterUpdate) {
|
||||
loadBins();
|
||||
afterUpdate();
|
||||
}
|
||||
closeDialog();
|
||||
});
|
||||
};
|
||||
|
||||
const decreaseInv = () => {
|
||||
let binFrom = activeBinSettings;
|
||||
let binTo: Bin;
|
||||
let binFromInv = pond.BinInventory.fromObject({
|
||||
...binFrom.inventory
|
||||
});
|
||||
|
||||
if (decreaseMode === "moved") {
|
||||
//TODO-CS: re-think the logic for moving between bins since the addition of custom types,
|
||||
//possibly remove restrictions and just have a confirmation for the user to change the destination bins grain type
|
||||
binTo = bins[binIndex];
|
||||
if (binTo.settings.inventory && binTo.settings.specs) {
|
||||
let binToInv = binTo.settings.inventory;
|
||||
//check to make sure the grain types of both bins are the same
|
||||
if (binFromInv.grainType !== binToInv.grainType) {
|
||||
if (
|
||||
binToInv.grainType === pond.Grain.GRAIN_NONE ||
|
||||
binToInv.grainType === pond.Grain.GRAIN_INVALID ||
|
||||
binToInv.grainBushels < 1
|
||||
) {
|
||||
binTo.settings.inventory.grainType = binFromInv.grainType;
|
||||
} else {
|
||||
openSnack("Grain types of both bins must match");
|
||||
return;
|
||||
}
|
||||
}
|
||||
//check if the other bin has enough space to move the grain
|
||||
let space = binTo.settings.specs.bushelCapacity - binTo.settings.inventory.grainBushels;
|
||||
if (space < currentBushels - newGrainInventory) {
|
||||
openSnack("Bin does not have enough space");
|
||||
return;
|
||||
}
|
||||
binToInv.grainBushels = binToInv.grainBushels + (currentBushels - newGrainInventory);
|
||||
}
|
||||
}
|
||||
|
||||
binFromInv.grainBushels = newGrainInventory;
|
||||
binFrom.inventory = binFromInv;
|
||||
|
||||
binAPI
|
||||
.updateBin(activeBinSettings.key, binFrom)
|
||||
.then(() => {
|
||||
//update inventory of the bin that the grain was moved to if it was a decrease
|
||||
if (decreaseMode === "moved") {
|
||||
binAPI
|
||||
.updateBin(bins[binIndex].key(), binTo.settings)
|
||||
.then(() => {
|
||||
openSnack("Grain inventory updated");
|
||||
})
|
||||
.catch(err => {
|
||||
openSnack("Failed to update grain inventory");
|
||||
})
|
||||
.finally(() => {
|
||||
if (afterUpdate) {
|
||||
afterUpdate(binTo);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
openSnack("Failed to update grain inventory");
|
||||
})
|
||||
.finally(() => {
|
||||
if (decreaseMode !== "moved" && afterUpdate) {
|
||||
afterUpdate();
|
||||
}
|
||||
closeDialog();
|
||||
});
|
||||
};
|
||||
|
||||
const setNewInventory = () => {
|
||||
if (currentBushels > newGrainInventory) {
|
||||
decreaseInv();
|
||||
} else {
|
||||
increaseInv();
|
||||
}
|
||||
};
|
||||
|
||||
const closeDialog = (useCallback?: boolean) => {
|
||||
onClose(useCallback);
|
||||
};
|
||||
|
||||
const disableButton = () => {
|
||||
if (increaseMode === "grown") {
|
||||
if (fields.length === 0 || fieldIndex < 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return (
|
||||
<ResponsiveDialog open={dialogControl} onClose={() => closeDialog(true)}>
|
||||
<DialogTitle>Grain Adjustment for {activeBinSettings.name}</DialogTitle>
|
||||
<DialogContent>{dialogContent()}</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => closeDialog(true)} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={setNewInventory} color="primary" disabled={disableButton()}>
|
||||
Confirm
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
}
|
||||
168
src/grain/GrainMoisture.ts
Normal file
168
src/grain/GrainMoisture.ts
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
import { pond } from "protobuf-ts/pond";
|
||||
import GrainDescriber from "./GrainDescriber";
|
||||
|
||||
export enum Equation {
|
||||
"none",
|
||||
"oswin",
|
||||
"halsey",
|
||||
"henderson",
|
||||
"chungPfost"
|
||||
}
|
||||
|
||||
const toERH = (humidity: number): number => {
|
||||
return humidity >= 100 ? 0.99999 : humidity / 100;
|
||||
};
|
||||
|
||||
export function ExtractMoisture(
|
||||
type: pond.Grain | undefined,
|
||||
celsius: number,
|
||||
humidity: number
|
||||
): number {
|
||||
if (humidity <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (
|
||||
type === undefined ||
|
||||
type === pond.Grain.GRAIN_NONE ||
|
||||
type === pond.Grain.GRAIN_INVALID ||
|
||||
type === pond.Grain.GRAIN_CUSTOM
|
||||
) {
|
||||
return humidity;
|
||||
}
|
||||
|
||||
let dry = humidity;
|
||||
let ctx = GrainDescriber(type);
|
||||
dry = toDryMoisture(ctx.equation, celsius, humidity, ctx.a, ctx.b, ctx.c);
|
||||
return dryToWet(dry);
|
||||
}
|
||||
|
||||
export function WaterContent(type: pond.Grain, bushels: number, moistureContent: number): number {
|
||||
let grain = GrainDescriber(type);
|
||||
return bushels * grain.weightConversionKg * moistureContent;
|
||||
}
|
||||
|
||||
export function MoistureToHumidity(
|
||||
type: pond.Grain | undefined,
|
||||
celsius: number,
|
||||
wetMC: number
|
||||
): number {
|
||||
if (wetMC <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (type === undefined || type === pond.Grain.GRAIN_NONE || type === pond.Grain.GRAIN_INVALID) {
|
||||
return wetMC;
|
||||
}
|
||||
|
||||
const dryMC = wetToDry(wetMC);
|
||||
let ctx = GrainDescriber(type);
|
||||
return dryMoistureToHumidity(ctx.equation, celsius, dryMC, ctx.a, ctx.b, ctx.c);
|
||||
}
|
||||
|
||||
function dryToWet(dryMC: number): number {
|
||||
return (100 * dryMC) / (dryMC + 100);
|
||||
}
|
||||
|
||||
function wetToDry(wetMC: number): number {
|
||||
return -((100 * wetMC) / (wetMC - 100));
|
||||
}
|
||||
|
||||
function toDryMoisture(
|
||||
eq: Equation,
|
||||
T: number,
|
||||
RH: number,
|
||||
a: number,
|
||||
b: number,
|
||||
c: number
|
||||
): number {
|
||||
const ERH: number = toERH(RH);
|
||||
switch (eq) {
|
||||
case Equation.chungPfost:
|
||||
return chungPfost(T, ERH, a, b, c);
|
||||
case Equation.halsey:
|
||||
return halsey(T, ERH, a, b, c);
|
||||
case Equation.henderson:
|
||||
return henderson(T, ERH, a, b, c);
|
||||
case Equation.oswin:
|
||||
return oswin(T, ERH, a, b, c);
|
||||
default:
|
||||
return RH;
|
||||
}
|
||||
}
|
||||
|
||||
function dryMoistureToHumidity(
|
||||
eq: Equation,
|
||||
celsius: number,
|
||||
dryMC: number,
|
||||
a: number,
|
||||
b: number,
|
||||
c: number
|
||||
): number {
|
||||
let ERH = 0;
|
||||
switch (eq) {
|
||||
case Equation.chungPfost:
|
||||
ERH = chungPfostInverse(celsius, dryMC, a, b, c);
|
||||
break;
|
||||
case Equation.halsey:
|
||||
ERH = halseyInverse(celsius, dryMC, a, b, c);
|
||||
break;
|
||||
case Equation.henderson:
|
||||
ERH = hendersonInverse(celsius, dryMC, a, b, c);
|
||||
break;
|
||||
case Equation.oswin:
|
||||
ERH = oswinInverse(celsius, dryMC, a, b, c);
|
||||
break;
|
||||
default:
|
||||
return dryMC;
|
||||
}
|
||||
|
||||
let RH = ERH * 100;
|
||||
return RH;
|
||||
}
|
||||
|
||||
function chungPfost(celsius: number, ERH: number, a: number, b: number, c: number): number {
|
||||
const T = celsius;
|
||||
return Math.log((-Math.log(ERH) * (T + c)) / a) / -b;
|
||||
}
|
||||
|
||||
function halsey(celsius: number, ERH: number, a: number, b: number, c: number): number {
|
||||
const T = celsius;
|
||||
return Math.pow(-Math.exp(a + b * T) / Math.log(ERH), 1 / c);
|
||||
}
|
||||
|
||||
function henderson(celsius: number, ERH: number, a: number, b: number, c: number): number {
|
||||
const T = celsius;
|
||||
return Math.pow(Math.log(1 - ERH) / (-a * (T + c)), 1 / b);
|
||||
}
|
||||
|
||||
function oswin(celsius: number, ERH: number, a: number, b: number, c: number): number {
|
||||
const T = celsius;
|
||||
return (a + b * T) / Math.pow(Math.pow(ERH, -1) - 1, 1 / c);
|
||||
}
|
||||
|
||||
function chungPfostInverse(
|
||||
celsius: number,
|
||||
dryMC: number,
|
||||
a: number,
|
||||
b: number,
|
||||
c: number
|
||||
): number {
|
||||
const T = celsius;
|
||||
return Math.exp(-(a / (T + c)) * Math.exp(-b * dryMC));
|
||||
}
|
||||
|
||||
function halseyInverse(celsius: number, dryMC: number, a: number, b: number, c: number): number {
|
||||
const T = celsius;
|
||||
return Math.exp(-(Math.exp(a + b * T) / Math.pow(dryMC, c)));
|
||||
}
|
||||
|
||||
function hendersonInverse(celsius: number, dryMC: number, a: number, b: number, c: number): number {
|
||||
const T = celsius;
|
||||
return 1 - Math.exp(-a * (T + c) * Math.pow(dryMC, b));
|
||||
}
|
||||
|
||||
function oswinInverse(celsius: number, dryMC: number, a: number, b: number, c: number): number {
|
||||
const T = celsius;
|
||||
return Math.pow(Math.pow((a + b * T) / dryMC, c) + 1, -1);
|
||||
}
|
||||
531
src/grain/GrainTransaction.tsx
Normal file
531
src/grain/GrainTransaction.tsx
Normal file
|
|
@ -0,0 +1,531 @@
|
|||
import {
|
||||
Box,
|
||||
Button,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
FormControlLabel,
|
||||
InputAdornment,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
TextField,
|
||||
Typography
|
||||
} from "@material-ui/core";
|
||||
import FileUploader from "common/FileUploads/FileUploader";
|
||||
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||
import SearchSelect, { Option } from "common/SearchSelect";
|
||||
import { useMobile } from "hooks";
|
||||
import { Bin, Field } from "models";
|
||||
import { Contract } from "models/Contract";
|
||||
import { GrainBag } from "models/GrainBag";
|
||||
import moment from "moment";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import {
|
||||
useBinAPI,
|
||||
useFieldAPI,
|
||||
useGlobalState,
|
||||
useGrainBagAPI,
|
||||
useSnackbar,
|
||||
useTransactionAPI
|
||||
} from "providers";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { getGrainUnit } from "utils";
|
||||
|
||||
interface Props {
|
||||
mainObject: Bin | GrainBag | Field | Contract;
|
||||
grainAdjustment?: number;
|
||||
hideFields?: boolean;
|
||||
asDestination?: boolean;
|
||||
restrictMatching?: boolean;
|
||||
open: boolean;
|
||||
close: () => void;
|
||||
callback?: (newTransaction?: pond.Transaction) => void;
|
||||
allowAttachmentUploads?: boolean;
|
||||
}
|
||||
|
||||
export default function GrainTransaction(props: Props) {
|
||||
const {
|
||||
open,
|
||||
mainObject,
|
||||
close,
|
||||
grainAdjustment,
|
||||
callback,
|
||||
restrictMatching,
|
||||
hideFields,
|
||||
asDestination,
|
||||
allowAttachmentUploads
|
||||
} = props;
|
||||
const [isObject, setIsObject] = useState<"source" | "destination" | "">("");
|
||||
const [selectedSource, setSelectedSource] = useState<Option | null>(null);
|
||||
const [selectedDestination, setSelectedDestination] = useState<Option | null>(null);
|
||||
const [binOptions, setBinOptions] = useState<Option[]>([]);
|
||||
const [bagOptions, setBagOptions] = useState<Option[]>([]);
|
||||
const [fieldOptions, setFieldOptions] = useState<Option[]>([]);
|
||||
const [destinationOptions, setDestinationOptions] = useState<Option[]>([]);
|
||||
const [sourceOptions, setSourceOptions] = useState<Option[]>([]);
|
||||
const binAPI = useBinAPI();
|
||||
const grainBagAPI = useGrainBagAPI();
|
||||
const fieldAPI = useFieldAPI();
|
||||
const [{ as }] = useGlobalState();
|
||||
const { openSnack } = useSnackbar();
|
||||
const [grainChangeDialog, setGrainChangeDialog] = useState(false);
|
||||
const [grainEntry, setGrainEntry] = useState("0");
|
||||
const [grainChangeVal, setGrainChangeVal] = useState("0");
|
||||
const transactionAPI = useTransactionAPI();
|
||||
const [grainMessage, setGrainMessage] = useState("");
|
||||
const isMobile = useMobile();
|
||||
const [binsLoading, setBinsLoading] = useState(false);
|
||||
const [bagsLoading, setBagsLoading] = useState(false);
|
||||
const [fieldsLoading, setFieldsLoading] = useState(false);
|
||||
const [uploadingFile, setUploadingFile] = useState(false);
|
||||
const [imageIDs, setImageIDs] = useState<string[]>([]);
|
||||
|
||||
const closeDialogs = (confirmed: boolean, newTransaction?: pond.Transaction) => {
|
||||
close();
|
||||
setGrainChangeVal("0");
|
||||
setGrainEntry("0");
|
||||
setIsObject("");
|
||||
setGrainChangeDialog(false);
|
||||
if (callback && confirmed) {
|
||||
callback(newTransaction);
|
||||
}
|
||||
};
|
||||
|
||||
//get the possible destinations/options (grainbags and bins)
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (grainAdjustment) {
|
||||
setGrainChangeVal(grainAdjustment.toString());
|
||||
} else {
|
||||
setGrainChangeVal("0");
|
||||
}
|
||||
if ((grainAdjustment && grainAdjustment > 0) || asDestination) {
|
||||
setIsObject("destination");
|
||||
setSelectedDestination({
|
||||
label: mainObject.name(),
|
||||
value: mainObject,
|
||||
group: mainObject.objectTypeString()
|
||||
});
|
||||
setSelectedSource(null);
|
||||
} else if (grainAdjustment && grainAdjustment < 0) {
|
||||
setIsObject("source");
|
||||
setSelectedSource({
|
||||
label: mainObject.name(),
|
||||
value: mainObject,
|
||||
group: mainObject.objectTypeString()
|
||||
});
|
||||
setSelectedDestination(null);
|
||||
}
|
||||
|
||||
//list bins and add to both options
|
||||
if (!binsLoading) {
|
||||
setBinsLoading(true);
|
||||
binAPI
|
||||
.listBins(0, 0, undefined, undefined, undefined, as)
|
||||
.then(resp => {
|
||||
//let sourceOps: Option[] = sourceOptions
|
||||
let binOps: Option[] = [];
|
||||
resp.data.bins.forEach(bin => {
|
||||
let b = Bin.create(bin);
|
||||
if (mainObject.key() !== b.key()) {
|
||||
let op: Option = {
|
||||
label: b.name(),
|
||||
value: b,
|
||||
group: "Bins"
|
||||
};
|
||||
(!restrictMatching || grainTypesMatch(op.value, mainObject)) && binOps.push(op);
|
||||
}
|
||||
});
|
||||
setBinOptions([...binOps]);
|
||||
})
|
||||
.finally(() => {
|
||||
setBinsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
//list bags and add to both options
|
||||
if (!bagsLoading) {
|
||||
setBagsLoading(true);
|
||||
grainBagAPI
|
||||
.listGrainBags(0, 0)
|
||||
.then(resp => {
|
||||
//let sourceOps: Option[] = sourceOptions
|
||||
let bagOps: Option[] = [];
|
||||
resp.data.grainBags.forEach(bag => {
|
||||
let b = GrainBag.create(bag);
|
||||
if (mainObject.key() !== b.key()) {
|
||||
let op: Option = {
|
||||
label: b.name(),
|
||||
value: b,
|
||||
group: "Grain Bags"
|
||||
};
|
||||
(!restrictMatching || grainTypesMatch(op.value, mainObject)) && bagOps.push(op);
|
||||
}
|
||||
});
|
||||
setBagOptions([...bagOps]);
|
||||
})
|
||||
.finally(() => {
|
||||
setBagsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
//list fields and add to source options
|
||||
if (!fieldsLoading && !hideFields) {
|
||||
setFieldsLoading(true);
|
||||
fieldAPI
|
||||
.listFields(0, 0, undefined, undefined, undefined, as)
|
||||
.then(resp => {
|
||||
//let sourceOps: Option[] = sourceOptions
|
||||
let fieldOps: Option[] = [];
|
||||
resp.data.fields.forEach(field => {
|
||||
let f = Field.create(field);
|
||||
let op: Option = {
|
||||
label: f.name(),
|
||||
value: f,
|
||||
group: "Fields"
|
||||
};
|
||||
(!restrictMatching || grainTypesMatch(op.value, mainObject)) && fieldOps.push(op);
|
||||
});
|
||||
setFieldOptions([...fieldOps]);
|
||||
})
|
||||
.finally(() => {
|
||||
setFieldsLoading(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [binAPI, fieldAPI, grainBagAPI, as, grainAdjustment, mainObject, open, asDestination]); //eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
//watches the object options arrays and build new options when they change
|
||||
useEffect(() => {
|
||||
setSourceOptions([...binOptions.concat(bagOptions, fieldOptions)]);
|
||||
setDestinationOptions([...binOptions.concat(bagOptions)]);
|
||||
}, [binOptions, bagOptions, fieldOptions]);
|
||||
|
||||
//create a transaction to enter into the table
|
||||
const createTransaction = (finalVal: number) => {
|
||||
let dest = selectedDestination;
|
||||
let source = selectedSource;
|
||||
let bpt = 1;
|
||||
//use the sources bushel per tonne if one was selected otherwise use the destination
|
||||
if (source) {
|
||||
bpt = source.value.bushelsPerTonne();
|
||||
} else if (dest) {
|
||||
bpt = dest.value.bushelsPerTonne();
|
||||
}
|
||||
|
||||
let newTransaction = pond.Transaction.create({
|
||||
timestamp: moment().toISOString(),
|
||||
transaction: pond.TransactionData.create({
|
||||
grainTransaction: pond.GrainTransaction.create({
|
||||
bushels: finalVal,
|
||||
message: grainMessage,
|
||||
bushelsPerTonne: bpt
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
if (dest) {
|
||||
let obj: Contract | Bin | GrainBag = dest.value;
|
||||
newTransaction.toKey = obj.key();
|
||||
newTransaction.toObject = obj.objectType();
|
||||
let grainTransaction = newTransaction.transaction?.grainTransaction;
|
||||
//set the grain in the transaction to use the destination by default
|
||||
if (grainTransaction) {
|
||||
grainTransaction.grainType = obj.grain();
|
||||
grainTransaction.customGrain = obj.customType();
|
||||
grainTransaction.subtype = obj.subtype();
|
||||
}
|
||||
}
|
||||
if (source) {
|
||||
let obj: Field | Bin | GrainBag = source.value;
|
||||
newTransaction.fromKey = obj.key();
|
||||
newTransaction.fromObject = obj.objectType();
|
||||
let grainTransaction = newTransaction.transaction?.grainTransaction;
|
||||
//over write the grain type to be what the source has if there was a source
|
||||
if (grainTransaction) {
|
||||
grainTransaction.grainType = obj.grain();
|
||||
grainTransaction.customGrain = obj.customType();
|
||||
grainTransaction.subtype = obj.subtype();
|
||||
}
|
||||
}
|
||||
transactionAPI.addTransaction(newTransaction, imageIDs).then(resp => {
|
||||
newTransaction.key = resp.data.key;
|
||||
closeDialogs(true, newTransaction);
|
||||
});
|
||||
};
|
||||
|
||||
const updateInventory = () => {
|
||||
//this is the amount to add to the destination
|
||||
let destinationAdd = Math.abs(
|
||||
isNaN(parseFloat(grainChangeVal)) ? 1 : parseFloat(grainChangeVal)
|
||||
);
|
||||
|
||||
//check the source to see how much can actually be moved
|
||||
if (selectedSource) {
|
||||
if (selectedSource?.value.objectTypeString() === "Bin") {
|
||||
let bin: Bin = selectedSource.value as Bin;
|
||||
if (bin.settings.inventory) {
|
||||
//if the difference is less than 0 that means there werent enough bushels in the source
|
||||
if (bin.settings.inventory.grainBushels - destinationAdd < 0) {
|
||||
//set the amount to add to the destination to be what is in the source
|
||||
destinationAdd = bin.settings.inventory.grainBushels;
|
||||
}
|
||||
}
|
||||
} else if (selectedSource?.value.objectTypeString() === "Grain Bag") {
|
||||
let bag: GrainBag = selectedSource.value as GrainBag;
|
||||
if (bag.bushels() - destinationAdd < 0) {
|
||||
destinationAdd = bag.bushels();
|
||||
}
|
||||
}
|
||||
}
|
||||
//check the destination to see if there is enough room to move from the source
|
||||
if (selectedDestination) {
|
||||
if (selectedDestination?.value.objectTypeString() === "Bin") {
|
||||
let bin: Bin = selectedDestination.value as Bin;
|
||||
if (bin.settings.inventory && bin.settings.specs) {
|
||||
if (
|
||||
bin.settings.inventory.grainBushels + destinationAdd >
|
||||
bin.settings.specs.bushelCapacity
|
||||
) {
|
||||
openSnack("Destination does not have enough room in bin for selected amount");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (selectedDestination?.value.objectTypeString() === "Grain Bag") {
|
||||
let bag: GrainBag = selectedDestination.value as GrainBag;
|
||||
if (bag.bushels() + destinationAdd > bag.capacity()) {
|
||||
openSnack("Destination does not have enough room in bag for selected amount");
|
||||
return;
|
||||
}
|
||||
} else if (selectedDestination?.value.objectTypeString() === "Contract") {
|
||||
let contract: Contract = selectedDestination.value as Contract;
|
||||
if (contract.settings.delivered + destinationAdd > contract.settings.size) {
|
||||
openSnack("Selected value will overdeliver on the contract");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//---!!moved the update functionality for updating objects to the backend inside the add transaction call!!---
|
||||
createTransaction(destinationAdd);
|
||||
};
|
||||
|
||||
//select for sources (fields, bins, bags)
|
||||
const sourceSelect = () => {
|
||||
return (
|
||||
<Box>
|
||||
<SearchSelect
|
||||
label="Grain Source"
|
||||
selected={selectedSource}
|
||||
changeSelection={option => {
|
||||
setSelectedSource(option);
|
||||
}}
|
||||
group
|
||||
//disabled={!canEdit}
|
||||
options={sourceOptions}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
//select for destinations (bins,bags)
|
||||
const destinationSelect = () => {
|
||||
return (
|
||||
<Box>
|
||||
<SearchSelect
|
||||
label="Grain Destination"
|
||||
selected={selectedDestination}
|
||||
changeSelection={option => {
|
||||
setSelectedDestination(option);
|
||||
}}
|
||||
group
|
||||
options={destinationOptions}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const grainTypesMatch = (
|
||||
source: Bin | GrainBag | Field | Contract,
|
||||
destination: Bin | GrainBag | Field | Contract
|
||||
) => {
|
||||
let matching = false;
|
||||
//check if the storage types are the same between the two
|
||||
if (source.storage() === destination.storage()) {
|
||||
if (source.storage() === pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN) {
|
||||
if (source.grain() === destination.grain()) {
|
||||
matching = true;
|
||||
}
|
||||
} else if (source.storage() === pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN) {
|
||||
if (
|
||||
source.customType().toLowerCase() === destination.customType().toLowerCase() &&
|
||||
source.bushelsPerTonne() === destination.bushelsPerTonne()
|
||||
) {
|
||||
matching = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return matching;
|
||||
};
|
||||
|
||||
const confirm = () => {
|
||||
if (selectedSource && selectedDestination) {
|
||||
if (grainTypesMatch(selectedSource.value, selectedDestination.value)) {
|
||||
updateInventory();
|
||||
} else {
|
||||
//open dialog saying that the destinations grain type will change from this action
|
||||
setGrainChangeDialog(true);
|
||||
}
|
||||
} else {
|
||||
updateInventory();
|
||||
}
|
||||
};
|
||||
|
||||
const grainChange = () => {
|
||||
return (
|
||||
<ResponsiveDialog
|
||||
open={grainChangeDialog}
|
||||
onClose={() => {
|
||||
setGrainChangeDialog(false);
|
||||
}}>
|
||||
<DialogContent>
|
||||
The grain type of the destination may change as a result of this action.
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setGrainChangeDialog(false);
|
||||
}}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={updateInventory} variant="contained" color="primary">
|
||||
Confirm
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ResponsiveDialog
|
||||
open={open}
|
||||
onClose={() => {
|
||||
close();
|
||||
}}>
|
||||
{grainChange()}
|
||||
<DialogTitle>New Grain Transaction</DialogTitle>
|
||||
<DialogContent style={{ minWidth: isMobile ? "100%" : 500 }}>
|
||||
{/* radio button to determine if the object you are making the adjustment from is the source/destination */}
|
||||
{!grainAdjustment && !asDestination && (
|
||||
<RadioGroup>
|
||||
<FormControlLabel
|
||||
value="source"
|
||||
control={<Radio />}
|
||||
label="Load Out"
|
||||
onChange={() => {
|
||||
setIsObject("source");
|
||||
let option: Option = {
|
||||
value: mainObject,
|
||||
label: mainObject.name(),
|
||||
group: mainObject.objectTypeString()
|
||||
};
|
||||
setSelectedSource(option);
|
||||
setSelectedDestination(null);
|
||||
}}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="destination"
|
||||
control={<Radio />}
|
||||
label="Load In"
|
||||
onChange={() => {
|
||||
setIsObject("destination");
|
||||
let option: Option = {
|
||||
value: mainObject,
|
||||
label: mainObject.name(),
|
||||
group: mainObject.objectTypeString()
|
||||
};
|
||||
setSelectedSource(null);
|
||||
setSelectedDestination(option);
|
||||
}}
|
||||
/>
|
||||
</RadioGroup>
|
||||
)}
|
||||
{!grainAdjustment && (
|
||||
<TextField
|
||||
label="Adjustment"
|
||||
margin="normal"
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
value={grainEntry}
|
||||
error={isNaN(parseFloat(grainEntry))}
|
||||
helperText={isNaN(parseFloat(grainEntry)) ? "must be a valid number" : ""}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
{getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT ? "mT" : "bu"}
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
onChange={e => {
|
||||
//if the user is viewing the grain in weight
|
||||
let grainVal = e.target.value;
|
||||
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT) {
|
||||
//need to convert what the user input (weight) into what is actually stored (bushels)
|
||||
//use source of the conversion value if it was selected, otherwise use the destination
|
||||
if (!isNaN(parseFloat(e.target.value))) {
|
||||
if (selectedSource) {
|
||||
grainVal = (+grainVal * selectedSource.value.bushelsPerTonne()).toFixed(2);
|
||||
} else if (selectedDestination) {
|
||||
grainVal = (+grainVal * selectedDestination.value.bushelsPerTonne()).toFixed(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
setGrainEntry(e.target.value);
|
||||
setGrainChangeVal(grainVal);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Typography>Leave selection blank to perform a correction</Typography>
|
||||
{isObject === "source" && destinationSelect()}
|
||||
{isObject === "destination" && sourceSelect()}
|
||||
<TextField
|
||||
label="Message(optional)"
|
||||
margin="dense"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={grainMessage}
|
||||
onChange={e => setGrainMessage(e.target.value)}
|
||||
/>
|
||||
<Box marginTop={1}>
|
||||
{allowAttachmentUploads && (
|
||||
<FileUploader
|
||||
uploadEnd={fileID => {
|
||||
if (fileID) {
|
||||
let ids = imageIDs;
|
||||
ids.push(fileID);
|
||||
setImageIDs([...ids]);
|
||||
}
|
||||
setUploadingFile(false);
|
||||
}}
|
||||
uploadStart={() => {
|
||||
setUploadingFile(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={() => closeDialogs(false)}>Cancel</Button>
|
||||
<Button
|
||||
onClick={confirm}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={uploadingFile || isNaN(parseFloat(grainEntry))}>
|
||||
Confirm
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
}
|
||||
40
src/grain/GrainUse.ts
Normal file
40
src/grain/GrainUse.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { pond } from "protobuf-ts/pond";
|
||||
import { Option } from "common/SearchSelect";
|
||||
|
||||
interface GrainUseExtension {
|
||||
label: string;
|
||||
key: pond.GrainUse;
|
||||
}
|
||||
|
||||
const DEFAULT_DESCRIBER = { label: "Unknown", key: pond.GrainUse.GRAIN_USE_UNKNOWN };
|
||||
|
||||
const getGrainUseMap = (): Map<pond.GrainUse, GrainUseExtension> => {
|
||||
return new Map([
|
||||
[pond.GrainUse.GRAIN_USE_UNKNOWN, DEFAULT_DESCRIBER],
|
||||
[pond.GrainUse.GRAIN_USE_SEED, { label: "Seed", key: pond.GrainUse.GRAIN_USE_SEED }],
|
||||
[
|
||||
pond.GrainUse.GRAIN_USE_COMMERCIAL,
|
||||
{ label: "Commercial", key: pond.GrainUse.GRAIN_USE_COMMERCIAL }
|
||||
],
|
||||
[pond.GrainUse.GRAIN_USE_FEED, { label: "Feed", key: pond.GrainUse.GRAIN_USE_FEED }]
|
||||
]);
|
||||
};
|
||||
|
||||
export const GrainUseDescriber = (use: pond.GrainUse): GrainUseExtension => {
|
||||
return getGrainUseMap().get(use) ?? DEFAULT_DESCRIBER;
|
||||
};
|
||||
|
||||
export const ToGrainUseOption = (use: pond.GrainUse): Option => {
|
||||
let describer = GrainUseDescriber(use);
|
||||
return { label: describer.label, value: describer.key };
|
||||
};
|
||||
|
||||
export const GetGrainUseOptions = (): Option[] => {
|
||||
let options: Option[] = [];
|
||||
Object.values(pond.GrainUse).forEach(grainUse => {
|
||||
if (typeof grainUse !== "string" && grainUse !== pond.GrainUse.GRAIN_USE_UNKNOWN) {
|
||||
options.push(ToGrainUseOption(grainUse));
|
||||
}
|
||||
});
|
||||
return options;
|
||||
};
|
||||
45
src/grain/GrainUtils.ts
Normal file
45
src/grain/GrainUtils.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { Component } from "models";
|
||||
import { extension } from "pbHelpers/ComponentType";
|
||||
import { quack } from "protobuf-ts/pond";
|
||||
|
||||
export function FindPlenumSensors(components: Component[]): Component[] {
|
||||
let plenumSensors: Component[] = [];
|
||||
|
||||
const required = [
|
||||
quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
|
||||
quack.MeasurementType.MEASUREMENT_TYPE_PERCENT
|
||||
];
|
||||
const blacklist = [quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE];
|
||||
|
||||
components.forEach(c => {
|
||||
if (!blacklist.includes(c.settings.type)) {
|
||||
let ext = extension(c.settings.type, c.settings.subtype);
|
||||
let has = ext.measurements.map(m => m.measurementType);
|
||||
if (required.every(m => has.includes(m))) {
|
||||
plenumSensors.push(c);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return plenumSensors;
|
||||
}
|
||||
|
||||
export function FindGrainCables(components: Component[]): Component[] {
|
||||
let grainCables: Component[] = [];
|
||||
|
||||
components.forEach(c => {
|
||||
if (c.settings.type === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE) {
|
||||
grainCables.push(c);
|
||||
}
|
||||
});
|
||||
|
||||
return grainCables;
|
||||
}
|
||||
|
||||
export function FindHeaters(components: Component[]): Component[] {
|
||||
return components.filter(
|
||||
c =>
|
||||
c.settings.type === quack.ComponentType.COMPONENT_TYPE_BOOLEAN_OUTPUT &&
|
||||
c.settings.subtype === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER
|
||||
);
|
||||
}
|
||||
5
src/grain/index.ts
Normal file
5
src/grain/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export * from "./GrainDescriber";
|
||||
export * from "./GrainDryingCalculator";
|
||||
export * from "./GrainMoisture";
|
||||
export * from "./GrainUse";
|
||||
export * from "./GrainUtils";
|
||||
Loading…
Add table
Add a link
Reference in a new issue