added tons of files for component functionality; manual component adding added temporarily

This commit is contained in:
Carter 2025-02-11 16:31:03 -06:00
parent 58830d480e
commit 45ff49bcea
121 changed files with 25883 additions and 65 deletions

523
src/grain/GrainDescriber.ts Normal file
View 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;
}

View 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>
);
}

View 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
View 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);
}

View 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
View 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
View 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
View file

@ -0,0 +1,5 @@
export * from "./GrainDescriber";
export * from "./GrainDryingCalculator";
export * from "./GrainMoisture";
export * from "./GrainUse";
export * from "./GrainUtils";