Merge branch 'custom_grain' into staging_environment
This commit is contained in:
commit
8ec08b0c36
32 changed files with 1366 additions and 257 deletions
2
package-lock.json
generated
2
package-lock.json
generated
|
|
@ -10953,7 +10953,7 @@
|
|||
},
|
||||
"node_modules/protobuf-ts": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#3f647071f211c4552c2acc23a8ecabb6904a4156",
|
||||
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#75f64f81d9b4ba7639d9978af37c361fca34c279",
|
||||
"dependencies": {
|
||||
"protobufjs": "^6.8.8"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { makeStyles } from '@mui/styles'
|
|||
import { CssBaseline, Theme } from '@mui/material'
|
||||
import { AppThemeProvider } from 'theme/AppThemeProvider'
|
||||
import HTTPProvider from 'providers/http'
|
||||
import { useSnackbar } from 'hooks'
|
||||
// import FirmwareLoader from './FirmwareLoader'
|
||||
|
||||
const reducer = (state: GlobalState, action: GlobalStateAction): GlobalState => {
|
||||
|
|
@ -64,6 +65,7 @@ export default function UserWrapper(props: Props) {
|
|||
const useAuth = useAuth0();
|
||||
const hasFetched = useRef(false);
|
||||
const [global, setGlobal] = useState<undefined | GlobalState>(undefined)
|
||||
const snackbar = useSnackbar()
|
||||
|
||||
const user_id = or(useAuth.user?.sub, "")
|
||||
|
||||
|
|
@ -81,7 +83,8 @@ export default function UserWrapper(props: Props) {
|
|||
backgroundTasksComplete: false,
|
||||
firmware: new Map()
|
||||
})
|
||||
}).catch(() => {
|
||||
}).catch((err) => {
|
||||
snackbar.error("get user and team: "+err)
|
||||
userAPI.getUser(user_id).then(user => {
|
||||
setGlobal({
|
||||
user: user ? user : User.create(),
|
||||
|
|
@ -92,7 +95,8 @@ export default function UserWrapper(props: Props) {
|
|||
backgroundTasksComplete: false,
|
||||
firmware: new Map()
|
||||
})
|
||||
}).catch(() => {
|
||||
}).catch((err) => {
|
||||
snackbar.error("get user: "+err)
|
||||
setGlobal(globalDefault)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -200,7 +200,8 @@ export default function BinCard(props: Props) {
|
|||
|
||||
const typeDisplay = () => {
|
||||
let grainType = bin.settings.inventory?.grainType ?? pond.Grain.GRAIN_NONE;
|
||||
if (bin.storage() === pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN) {
|
||||
switch (bin.storage()){
|
||||
case pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN:
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="subtitle2" style={{ fontSize: "0.7rem", fontWeight: 700 }}>
|
||||
|
|
@ -210,15 +211,16 @@ export default function BinCard(props: Props) {
|
|||
{bin.settings.inventory?.grainSubtype}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="subtitle2" style={{ fontSize: "0.7rem", fontWeight: 700 }}>
|
||||
{bin.settings.inventory?.customTypeName}
|
||||
{bin.grainName()}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const tempDisplay = () => {
|
||||
|
|
@ -417,6 +419,7 @@ export default function BinCard(props: Props) {
|
|||
hottestNodeTemp={valDisplay === "high" ? hotNode?.temp : undefined}
|
||||
coldestNodeTemp={valDisplay === "low" ? coldNode?.temp : undefined}
|
||||
inventoryControl={bin.inventoryControl()}
|
||||
customGrain={bin.customGrain()}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -39,11 +39,12 @@ interface Props {
|
|||
heaters?: Controller[];
|
||||
fans?: Controller[];
|
||||
grain?: pond.Grain;
|
||||
customGrain?: pond.GrainSettings
|
||||
}
|
||||
|
||||
export default function BinConditioningCard(props: Props) {
|
||||
const classes = useStyles();
|
||||
const { interactions, interactionDevices, plenums, ambients, heaters, fans, grain, mode } = props;
|
||||
const { interactions, interactionDevices, plenums, ambients, heaters, fans, grain, mode, customGrain } = props;
|
||||
const [sourceMap, setSourceMap] = useState<Map<string, Component>>(new Map());
|
||||
const [sinkMap, setSinkMap] = useState<Map<string, Component>>(new Map());
|
||||
|
||||
|
|
@ -87,6 +88,7 @@ export default function BinConditioningCard(props: Props) {
|
|||
sink={sink}
|
||||
source={source}
|
||||
grain={grain}
|
||||
customGrain={customGrain}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,10 +84,11 @@ interface Props {
|
|||
source: Component;
|
||||
sink: Component;
|
||||
grain?: pond.Grain;
|
||||
customGrain?: pond.GrainSettings
|
||||
}
|
||||
|
||||
export default function BinConditioningInteraction(props: Props) {
|
||||
const { interaction, source, sink, grain, deviceId } = props;
|
||||
const { interaction, source, sink, grain, deviceId, customGrain } = props;
|
||||
const [sliderVals, setSliderVals] = useState<Map<quack.MeasurementType, number>>(new Map());
|
||||
const [sliderMarks, setSliderMarks] = useState<Map<quack.MeasurementType, number>>(new Map());
|
||||
//this is the emc value calculated from the interactions temp and humidity conditions
|
||||
|
|
@ -127,7 +128,6 @@ export default function BinConditioningInteraction(props: Props) {
|
|||
if (
|
||||
grain !== undefined &&
|
||||
grain !== pond.Grain.GRAIN_INVALID &&
|
||||
grain !== pond.Grain.GRAIN_CUSTOM &&
|
||||
temp &&
|
||||
hum
|
||||
) {
|
||||
|
|
@ -135,8 +135,9 @@ export default function BinConditioningInteraction(props: Props) {
|
|||
//the emc calc needs the temp to be in celsius
|
||||
temp = fahrenheitToCelsius(temp);
|
||||
}
|
||||
let emc = ExtractMoisture(grain, temp, hum);
|
||||
setBaseEMC(emc);
|
||||
let emc = ExtractMoisture(grain, temp, hum, customGrain)
|
||||
setBaseEMC(emc === hum ? undefined : emc);
|
||||
|
||||
}
|
||||
}, [sliderVals, grain]);
|
||||
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@ interface Props {
|
|||
hottestNodeTemp?: number;
|
||||
coldestNodeTemp?: number;
|
||||
inventoryControl?: pond.BinInventoryControl;
|
||||
customGrain?: pond.GrainSettings;
|
||||
}
|
||||
|
||||
interface GrainNodePoint {
|
||||
|
|
@ -211,7 +212,8 @@ export default function BinSVGV2(props: Props) {
|
|||
hottestNodeTemp,
|
||||
coldestNodeTemp,
|
||||
inventoryControl,
|
||||
co2Sensors
|
||||
co2Sensors,
|
||||
customGrain
|
||||
} = props;
|
||||
const [{ user }] = useGlobalState();
|
||||
const [extraDetails, setExtraDetails] = useState<"temps" | "hums">("temps");
|
||||
|
|
@ -683,7 +685,7 @@ export default function BinSVGV2(props: Props) {
|
|||
x: cablePos,
|
||||
y: nodeY + 10,
|
||||
stroke: "green",
|
||||
value: ExtractMoisture(props.grainType, temp, cable.humidities[index] ?? 0).toFixed(1)+"%",
|
||||
value: ExtractMoisture(props.grainType, temp, cable.humidities[index] ?? 0, customGrain).toFixed(1)+"%",
|
||||
})
|
||||
!showTempHum &&
|
||||
nodeClickData.push({
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ import { getDistanceUnit, or, getTemperatureUnit, getGrainUnit } from "utils";
|
|||
import { makeStyles } from "@mui/styles";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import BinSelector from "./BinSelector";
|
||||
import CustomGrainSelector from "grain/CustomGrainSelector";
|
||||
// import BinSelector from "./BinSelector";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
|
|
@ -205,6 +206,7 @@ export default function BinSettings(props: Props) {
|
|||
const libracartAPI = useLibraCartProxyAPI()
|
||||
const [lcDestination, setlcDestination] = useState<Option | null>()
|
||||
const [lcDestinationOptions, setlcDestinationOptions] = useState<Option[]>([])
|
||||
const [customGrain, setCustomGrain] = useState<pond.GrainSettings>()
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
|
|
@ -236,6 +238,9 @@ export default function BinSettings(props: Props) {
|
|||
initForm.inventory.grainType
|
||||
).bushelsPerTonne;
|
||||
}
|
||||
if (initForm.inventory.customGrain){
|
||||
setCustomGrain(initForm.inventory.customGrain)
|
||||
}
|
||||
//if the target temp is not set (older bins) make it the midpoint of the high and low temps assuming they are set otherwise make it 15
|
||||
if (!initForm.inventory.targetTemperature || initForm.inventory.targetTemperature) {
|
||||
if (initForm.highTemp && initForm.lowTemp) {
|
||||
|
|
@ -444,6 +449,11 @@ export default function BinSettings(props: Props) {
|
|||
form.inventory.inventoryControl = inventoryControl
|
||||
form.inventory.autoThreshold = autoFillThreshold
|
||||
form.inventory.lidarDropCm = getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? lidarDropDistance * 30.48 : lidarDropDistance
|
||||
if(form.inventory.grainType !== pond.Grain.GRAIN_CUSTOM){
|
||||
form.inventory.customGrain = undefined
|
||||
}else{
|
||||
form.inventory.customGrain = customGrain
|
||||
}
|
||||
}
|
||||
|
||||
binAPI
|
||||
|
|
@ -493,6 +503,11 @@ export default function BinSettings(props: Props) {
|
|||
form.inventory.autoThreshold = autoFillThreshold
|
||||
//if the users preferences are in feet convert the distance to cm otherwise it was entered as cm so use that
|
||||
form.inventory.lidarDropCm = getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? lidarDropDistance * 30.48 : lidarDropDistance
|
||||
if(form.inventory.grainType !== pond.Grain.GRAIN_CUSTOM){
|
||||
form.inventory.customGrain = undefined
|
||||
}else{
|
||||
form.inventory.customGrain = customGrain
|
||||
}
|
||||
}
|
||||
binAPI
|
||||
.updateBin(bin.key(), form, as)
|
||||
|
|
@ -1014,7 +1029,37 @@ export default function BinSettings(props: Props) {
|
|||
{isCustom ? "Custom" : "Grain"}
|
||||
</Typography> */}
|
||||
<Box>
|
||||
{!isCustomInventory && (
|
||||
{isCustomInventory ?
|
||||
<React.Fragment>
|
||||
{storageType === pond.BinStorage.BIN_STORAGE_FERTILIZER ?
|
||||
<React.Fragment>
|
||||
<TextField
|
||||
label="Type"
|
||||
value={customTypeName}
|
||||
type="text"
|
||||
onChange={event => {
|
||||
updateForm(
|
||||
"inventory",
|
||||
pond.BinInventory.create({
|
||||
...form.inventory,
|
||||
customTypeName: event.target.value
|
||||
})
|
||||
);
|
||||
updateFormExtension("customTypeName", event.target.value);
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
className={classes.bottomSpacing}
|
||||
/>
|
||||
</React.Fragment>
|
||||
:
|
||||
<Box sx={{border: "1px solid white", borderRadius: 2, padding: 2, marginBottom: 2}}>
|
||||
<CustomGrainSelector initialGrain={customGrain} onGrainSettingsChange={(newGrainSettings) => {setCustomGrain(newGrainSettings)}}/>
|
||||
</Box>
|
||||
}
|
||||
</React.Fragment>
|
||||
:
|
||||
<React.Fragment>
|
||||
<Box className={classes.bottomSpacing}>
|
||||
<SearchSelect
|
||||
label="Type"
|
||||
|
|
@ -1040,49 +1085,6 @@ export default function BinSettings(props: Props) {
|
|||
options={grainOptions}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{isCustomInventory ? (
|
||||
<React.Fragment>
|
||||
<TextField
|
||||
label="Type"
|
||||
value={customTypeName}
|
||||
type="text"
|
||||
onChange={event => {
|
||||
updateForm(
|
||||
"inventory",
|
||||
pond.BinInventory.create({
|
||||
...form.inventory,
|
||||
customTypeName: event.target.value
|
||||
})
|
||||
);
|
||||
updateFormExtension("customTypeName", event.target.value);
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
className={classes.bottomSpacing}
|
||||
/>
|
||||
{storageType !== pond.BinStorage.BIN_STORAGE_FERTILIZER && (
|
||||
<TextField
|
||||
label="Bushels Per Tonne"
|
||||
value={bushPerTonne}
|
||||
type="number"
|
||||
onChange={event => {
|
||||
updateForm(
|
||||
"inventory",
|
||||
pond.BinInventory.create({
|
||||
...form.inventory,
|
||||
bushelsPerTonne: +event.target.value
|
||||
})
|
||||
);
|
||||
updateFormExtension("bushelsPerTonne", event.target.value);
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
className={classes.bottomSpacing}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<TextField
|
||||
label="Grain Variant"
|
||||
value={grainSubtype}
|
||||
|
|
@ -1102,7 +1104,8 @@ export default function BinSettings(props: Props) {
|
|||
disabled={!grainType}
|
||||
className={classes.bottomSpacing}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
}
|
||||
{getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_BUSHELS ||
|
||||
storageType === pond.BinStorage.BIN_STORAGE_FERTILIZER ||
|
||||
formExtension.bushelsPerTonne === "0" ||
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ import Edit from "@mui/icons-material/Edit";
|
|||
import { makeStyles } from "@mui/styles";
|
||||
import ButtonGroup from "common/ButtonGroup";
|
||||
import ModeChangeDialog from "./conditioning/modeChangeDialog";
|
||||
import CustomGrainSelector from "grain/CustomGrainSelector";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
return ({
|
||||
|
|
@ -258,7 +259,7 @@ export default function BinVisualizer(props: Props) {
|
|||
//const [highTempCable, setHighTempCable] = useState<GrainCable>();
|
||||
//const [lowTempCable, setLowTempCable] = useState<GrainCable>();
|
||||
//the switch value to determine what to display in the grain condition box
|
||||
const [valueDisplay, setValueDisplay] = useState<"low" | "avg" | "high">("avg");
|
||||
const [valueDisplay, setValueDisplay] = useState<"low" | "avg" | "high">("high");
|
||||
//the variables to be able to change the grain type through a unique dialog outside of the settings
|
||||
const [grainChangeDialog, setGrainChangeDialog] = useState(false);
|
||||
const grainOptions = GrainOptions();
|
||||
|
|
@ -286,6 +287,7 @@ export default function BinVisualizer(props: Props) {
|
|||
|
||||
const [openModeChange, setOpenModeChange] = useState(false)
|
||||
const [modeChangeInProgress, setModeChangeInProgress] = useState(false)
|
||||
const [newGrainSettings, setNewGrainSettings] = useState<pond.GrainSettings>()
|
||||
|
||||
useEffect(() => {
|
||||
setModeTime(moment(bin.status.lastModeChange));
|
||||
|
|
@ -327,6 +329,7 @@ export default function BinVisualizer(props: Props) {
|
|||
setBushPerTonne(bin.settings.inventory.bushelsPerTonne.toFixed(2));
|
||||
setGrainSubtype(bin.subtype());
|
||||
setNewBinMode(bin.settings.mode);
|
||||
setNewGrainSettings(bin.customGrain());
|
||||
}
|
||||
if (bin.settings) {
|
||||
let t = bin.settings.outdoorTemp;
|
||||
|
|
@ -515,18 +518,13 @@ export default function BinVisualizer(props: Props) {
|
|||
const capacity = bin.settings.specs?.bushelCapacity ?? 0;
|
||||
const grainBushels = bin.bushels();
|
||||
const isEmpty = bin.settings.inventory?.empty === true || !grainBushels || grainBushels <= 0;
|
||||
const grainType = bin.settings.inventory?.grainType;
|
||||
const grainTypeName = isEmpty || !grainType ? "" : GrainDescriber(grainType).name;
|
||||
const customTypeName = bin.settings.inventory?.customTypeName;
|
||||
const grainSubtype = bin.settings.inventory?.grainSubtype;
|
||||
return (
|
||||
<Box>
|
||||
{/* grain display */}
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Typography variant="subtitle2" color="textPrimary" style={{ fontWeight: 800 }}>
|
||||
{bin.storage() === pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN
|
||||
? grainTypeName
|
||||
: customTypeName}
|
||||
{isEmpty ? "" : bin.grainName()}
|
||||
{grainSubtype !== "" ? " - " + grainSubtype : ""}
|
||||
</Typography>
|
||||
<Box
|
||||
|
|
@ -768,6 +766,7 @@ export default function BinVisualizer(props: Props) {
|
|||
setGrainType(bin.settings.inventory.grainType);
|
||||
setGrainOption(ToGrainOption(bin.settings.inventory.grainType));
|
||||
setBushPerTonne(bin.settings.inventory.bushelsPerTonne.toFixed(2));
|
||||
setNewGrainSettings(bin.customGrain())
|
||||
setGrainSubtype(bin.subtype());
|
||||
}
|
||||
setGrainChangeDialog(false);
|
||||
|
|
@ -799,11 +798,13 @@ export default function BinVisualizer(props: Props) {
|
|||
setCustomTypeName("");
|
||||
setGrainOption(ToGrainOption(pond.Grain.GRAIN_NONE));
|
||||
if (checked) {
|
||||
setStorageType(pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN);
|
||||
setGrainType(pond.Grain.GRAIN_CUSTOM);
|
||||
} else {
|
||||
setStorageType(pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN);
|
||||
setGrainType(pond.Grain.GRAIN_CUSTOM);
|
||||
setNewGrainSettings(bin.customGrain())
|
||||
} else {
|
||||
setStorageType(pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN);
|
||||
setGrainType(pond.Grain.GRAIN_NONE);
|
||||
setNewGrainSettings(undefined)
|
||||
}
|
||||
}}
|
||||
name="storage"
|
||||
|
|
@ -834,7 +835,7 @@ export default function BinVisualizer(props: Props) {
|
|||
)}
|
||||
{isCustomInventory ? (
|
||||
<React.Fragment>
|
||||
<TextField
|
||||
{/* <TextField
|
||||
label="Type"
|
||||
value={customTypeName}
|
||||
type="text"
|
||||
|
|
@ -859,7 +860,8 @@ export default function BinVisualizer(props: Props) {
|
|||
variant="outlined"
|
||||
className={classes.bottomSpacing}
|
||||
/>
|
||||
)}
|
||||
)} */}
|
||||
<CustomGrainSelector initialGrain={bin.customGrain()} onGrainSettingsChange={settings => {setNewGrainSettings(settings)}}/>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<TextField
|
||||
|
|
@ -1404,6 +1406,7 @@ export default function BinVisualizer(props: Props) {
|
|||
}
|
||||
}}
|
||||
inventoryControl={bin.inventoryControl()}
|
||||
customGrain={bin.customGrain()}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
@ -1574,6 +1577,12 @@ export default function BinVisualizer(props: Props) {
|
|||
const fanPerformance = () => {
|
||||
let totalCFM = bin.status.fanCfm;
|
||||
let bushelCFM = bin.status.cfmPerBushel;
|
||||
let emc = ExtractMoisture(
|
||||
bin.grain(),
|
||||
activePlenum?.tempHumidity?.temperature ?? 0,
|
||||
activePlenum?.tempHumidity?.humidity ?? 0,
|
||||
bin.customGrain()
|
||||
)
|
||||
return (
|
||||
<Box>
|
||||
<Typography style={{ fontSize: isMobile ? "0.85rem" : "1.0rem", fontWeight: 650 }}>
|
||||
|
|
@ -1617,19 +1626,17 @@ export default function BinVisualizer(props: Props) {
|
|||
</Box>
|
||||
</Grid>
|
||||
<Grid>
|
||||
{emc !== activePlenum?.tempHumidity?.humidity &&
|
||||
<Box className={classes.lightBox}>
|
||||
<Typography align="center" style={{ fontWeight: 650, color: emcColour }}>
|
||||
{ExtractMoisture(
|
||||
bin.grain(),
|
||||
activePlenum?.tempHumidity?.temperature ?? 0,
|
||||
activePlenum?.tempHumidity?.humidity ?? 0
|
||||
).toFixed(2)}
|
||||
{emc.toFixed(2)}
|
||||
%
|
||||
</Typography>
|
||||
<Typography align="center" style={{ fontWeight: 650 }}>
|
||||
Plenum EMC
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
|
@ -1638,6 +1645,12 @@ export default function BinVisualizer(props: Props) {
|
|||
};
|
||||
|
||||
const ambientDisplay = () => {
|
||||
let emc = ExtractMoisture(
|
||||
bin.grain(),
|
||||
ambient?.temperature ?? 0,
|
||||
ambient?.humidity ?? 0,
|
||||
bin.customGrain()
|
||||
)
|
||||
return (
|
||||
<Box>
|
||||
<Typography style={{ fontSize: isMobile ? "0.85rem" : "1.0rem", fontWeight: 650 }}>
|
||||
|
|
@ -1677,19 +1690,17 @@ export default function BinVisualizer(props: Props) {
|
|||
</Box>
|
||||
</Grid>
|
||||
<Grid >
|
||||
{emc !== ambient?.humidity &&
|
||||
<Box className={classes.lightBox}>
|
||||
<Typography align="center" style={{ fontWeight: 650, color: emcColour }}>
|
||||
{ExtractMoisture(
|
||||
bin.grain(),
|
||||
ambient?.temperature ?? 0,
|
||||
ambient?.humidity ?? 0
|
||||
).toFixed(2)}
|
||||
{emc.toFixed(2)}
|
||||
%
|
||||
</Typography>
|
||||
<Typography align="center" style={{ fontWeight: 650 }}>
|
||||
Ambient EMC
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
|
@ -1968,6 +1979,7 @@ export default function BinVisualizer(props: Props) {
|
|||
? 0
|
||||
: parseFloat(bushPerTonne);
|
||||
b.settings.inventory.grainSubtype = grainSubtype;
|
||||
b.settings.inventory.customGrain = newGrainSettings
|
||||
}
|
||||
|
||||
if (b.settings.outdoorTemp !== undefined) {
|
||||
|
|
|
|||
|
|
@ -170,18 +170,18 @@ export default function BinGraphs(props: Props) {
|
|||
setEndDate(newEndDate);
|
||||
};
|
||||
|
||||
const determineGrainColour = () => {
|
||||
let col = "yellow";
|
||||
let binInv = bin.settings.inventory;
|
||||
if (binInv) {
|
||||
if (binInv.grainType === pond.Grain.GRAIN_CUSTOM) {
|
||||
col = stringToMaterialColour(binInv.customTypeName);
|
||||
} else if (binInv.grainType !== pond.Grain.GRAIN_NONE) {
|
||||
col = GrainDescriber(binInv.grainType).colour;
|
||||
}
|
||||
}
|
||||
return col;
|
||||
};
|
||||
// const determineGrainColour = () => {
|
||||
// let col = "yellow";
|
||||
// let binInv = bin.settings.inventory;
|
||||
// if (binInv) {
|
||||
// if (binInv.grainType === pond.Grain.GRAIN_CUSTOM) {
|
||||
// col = stringToMaterialColour(binInv.customTypeName);
|
||||
// } else if (binInv.grainType !== pond.Grain.GRAIN_NONE) {
|
||||
// col = GrainDescriber(binInv.grainType).colour;
|
||||
// }
|
||||
// }
|
||||
// return col;
|
||||
// };
|
||||
|
||||
const inventoryGraph = () => {
|
||||
return (
|
||||
|
|
@ -193,7 +193,7 @@ export default function BinGraphs(props: Props) {
|
|||
endDate={endDate}
|
||||
binLoading={binLoading}
|
||||
bin={bin}
|
||||
colour={determineGrainColour()}
|
||||
colour={bin.grainColour()}
|
||||
fertilizerBin={bin.settings.storage === pond.BinStorage.BIN_STORAGE_FERTILIZER}
|
||||
/>
|
||||
</Grid>
|
||||
|
|
|
|||
|
|
@ -92,7 +92,8 @@ export default function BinGraphsTrending(props: Props) {
|
|||
trend: ExtractMoisture(
|
||||
bin.grain(),
|
||||
val.values[0],
|
||||
plenumHumidity.values[i].values[0]
|
||||
plenumHumidity.values[i].values[0],
|
||||
bin.customGrain()
|
||||
)
|
||||
};
|
||||
trendData.push(trendPoint);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
AccordionSummary,
|
||||
Alert,
|
||||
AlertTitle,
|
||||
Box,
|
||||
Button,
|
||||
Collapse,
|
||||
FormControl,
|
||||
|
|
@ -47,6 +48,7 @@ import { getDistanceUnit } from "utils";
|
|||
import { GrainOptions } from "grain";
|
||||
import CompModes from "component/ComponentMode.json";
|
||||
import FanPicker from "fans/fanPicker";
|
||||
import CustomGrainSelector from "grain/CustomGrainSelector";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
return ({
|
||||
|
|
@ -136,6 +138,7 @@ export default function ComponentForm(props: Props) {
|
|||
sensorDistance: "0",
|
||||
});
|
||||
const [compMode, setCompMode] = useState<any>();
|
||||
const [useCustomGrain, setUseCustomGrain] = useState<boolean>(false)
|
||||
//const [numCalibrations, setNumCalibrations] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -192,6 +195,10 @@ export default function ComponentForm(props: Props) {
|
|||
sensorDistance = sensorDistance / 30.48;
|
||||
}
|
||||
|
||||
if(formComponent.settings.customGrain){
|
||||
setUseCustomGrain(true)
|
||||
}
|
||||
|
||||
setForm({
|
||||
component: formComponent,
|
||||
measure: formComponent.settings.measurementPeriodMs > 0,
|
||||
|
|
@ -383,6 +390,13 @@ export default function ComponentForm(props: Props) {
|
|||
setForm(f);
|
||||
};
|
||||
|
||||
const updateCustomGrain = (grainSettings?: pond.GrainSettings) => {
|
||||
let f = cloneDeep(form)
|
||||
f.component.settings.customGrain = grainSettings
|
||||
f.component.settings.grainType = pond.Grain.GRAIN_CUSTOM
|
||||
setForm(f)
|
||||
}
|
||||
|
||||
const updateGrainType = (option: Option | null) => {
|
||||
let f = cloneDeep(form);
|
||||
|
||||
|
|
@ -622,6 +636,27 @@ export default function ComponentForm(props: Props) {
|
|||
const grainSelect = () => {
|
||||
let selected = findSelectedGrain(grainOptions);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
value={useCustomGrain}
|
||||
checked={useCustomGrain}
|
||||
title="Custom Grain"
|
||||
onClick={() => {
|
||||
setUseCustomGrain(!useCustomGrain);
|
||||
updateCustomGrain()
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Custom Grain"
|
||||
labelPlacement="start"
|
||||
/>
|
||||
{useCustomGrain ?
|
||||
<Box sx={{border: "1px solid white", borderRadius: 2, padding: 2, marginBottom: 2}}>
|
||||
<CustomGrainSelector onGrainSettingsChange={updateCustomGrain} initialGrain={form.component.customGrainProps()}/>
|
||||
</Box>
|
||||
:
|
||||
<SearchSelect
|
||||
selected={selected}
|
||||
changeSelection={updateGrainType}
|
||||
|
|
@ -629,6 +664,8 @@ export default function ComponentForm(props: Props) {
|
|||
options={grainOptions}
|
||||
group
|
||||
/>
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { Pressure } from "models/Pressure";
|
|||
import { GrainCable } from "models/GrainCable";
|
||||
import { extension, Summary } from "pbHelpers/ComponentType";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import { stringToMaterialColour } from "utils";
|
||||
|
||||
interface Props {
|
||||
component: Component | Plenum | Pressure | GrainCable;
|
||||
|
|
@ -100,13 +101,26 @@ export default function UnitMeasurementSummary(props: Props) {
|
|||
);
|
||||
};
|
||||
|
||||
const grainName = () => {
|
||||
if(component.settings.grainType === pond.Grain.GRAIN_CUSTOM && component.settings.customGrain?.name){
|
||||
return component.settings.customGrain.name
|
||||
}
|
||||
return GrainDescriber(component.settings.grainType).name
|
||||
}
|
||||
|
||||
const grainColor = () => {
|
||||
if(component.settings.grainType === pond.Grain.GRAIN_CUSTOM && component.settings.customGrain?.name){
|
||||
return stringToMaterialColour(component.settings.customGrain.name)
|
||||
}
|
||||
return GrainDescriber(component.settings.grainType).colour
|
||||
}
|
||||
|
||||
const getSummaryComponent = (summaries: Summary[]): any => {
|
||||
if (summaries.length < 1) {
|
||||
return noSummary();
|
||||
}
|
||||
|
||||
let overlays: JSX.Element[] = [];
|
||||
let grain = GrainDescriber(component.settings.grainType);
|
||||
return (
|
||||
<Grid container>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
|
|
@ -170,7 +184,7 @@ export default function UnitMeasurementSummary(props: Props) {
|
|||
component.settings.grainType !== pond.Grain.GRAIN_INVALID && (
|
||||
<Typography variant={largeText ? "body1" : "caption"} color="textPrimary">
|
||||
<span key={"grainType"}>
|
||||
Grain Type: <span style={{ color: grain.colour }}>{grain.name}</span>
|
||||
Grain Type: <span style={{ color: grainColor() }}>{grainName()}</span>
|
||||
</span>
|
||||
</Typography>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -56,55 +56,6 @@ export default function GateDevice(props: Props) {
|
|||
const [lastTemps, setLastTemps] = useState({ t1: 0, t2: 0 });
|
||||
const [lastPressures, setLastPressures] = useState({ p1: 0, p2: 0 });
|
||||
|
||||
// const StyledToggleButtonGroup = withStyles(theme => ({
|
||||
// grouped: {
|
||||
// margin: theme.spacing(-0.5),
|
||||
// border: "none",
|
||||
// padding: theme.spacing(1),
|
||||
// "&:not(:first-child):not(:last-child)": {
|
||||
// borderRadius: 24,
|
||||
// marginRight: theme.spacing(0.5),
|
||||
// marginLeft: theme.spacing(0.5)
|
||||
// },
|
||||
// "&:first-child": {
|
||||
// borderRadius: 24,
|
||||
// marginLeft: theme.spacing(0.25)
|
||||
// },
|
||||
// "&:last-child": {
|
||||
// borderRadius: 24,
|
||||
// marginRight: theme.spacing(0.25)
|
||||
// }
|
||||
// },
|
||||
// root: {
|
||||
// backgroundColor: darken(
|
||||
// theme.palette.background.paper,
|
||||
// getThemeType() === "light" ? 0.05 : 0.25
|
||||
// ),
|
||||
// borderRadius: 24,
|
||||
// content: "border-box"
|
||||
// }
|
||||
// }))(ToggleButtonGroup);
|
||||
|
||||
// const StyledToggle = withStyles({
|
||||
// root: {
|
||||
// backgroundColor: "transparent",
|
||||
// overflow: "visible",
|
||||
// content: "content-box",
|
||||
// "&$selected": {
|
||||
// backgroundColor: "gold",
|
||||
// color: "black",
|
||||
// borderRadius: 24,
|
||||
// fontWeight: "bold"
|
||||
// },
|
||||
// "&$selected:hover": {
|
||||
// backgroundColor: "rgb(255, 255, 0)",
|
||||
// color: "black",
|
||||
// borderRadius: 24
|
||||
// }
|
||||
// },
|
||||
// selected: {}
|
||||
// })(ToggleButton);
|
||||
|
||||
useEffect(() => {
|
||||
if (comprehensiveDevice.device) {
|
||||
setDevice(Device.any(comprehensiveDevice.device));
|
||||
|
|
@ -338,11 +289,11 @@ export default function GateDevice(props: Props) {
|
|||
return (
|
||||
<Grid
|
||||
container
|
||||
direction={(isMobile) ? "column" : "row"}
|
||||
// direction={(isMobile) ? "column" : "row"}
|
||||
width="100%"
|
||||
alignItems="center"
|
||||
//alignItems="center"
|
||||
>
|
||||
<Grid size={{ sm: 12, lg: isMobile || drawerView ? 12 : 4}} style={{ padding: 10 }}>
|
||||
<Grid width={"100%"} size={{ sm: 12, lg: isMobile || drawerView ? 12 : 4}} style={{ padding: 10 }}>
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
|
|
@ -363,7 +314,7 @@ export default function GateDevice(props: Props) {
|
|||
]}
|
||||
/>
|
||||
</Box>
|
||||
<Card raised>
|
||||
<Card>
|
||||
<GateSVG
|
||||
finalTemp={
|
||||
gate.gateMutations[device.id().toString()] &&
|
||||
|
|
|
|||
|
|
@ -70,8 +70,9 @@ export default function GateFlowGraph(props: Props) {
|
|||
const classes = useStyles();
|
||||
const [flowEvents, setFlowEvents] = useState<pond.AirFlowEvent[]>([]);
|
||||
const [eventsLoading, setEventsLoading] = useState(false);
|
||||
const eventThreshold = 5;
|
||||
const idleFlow = 2.44;
|
||||
//these two constants could be entered by the user at time of retrieval
|
||||
const eventThreshold = 5; //the threshold that if crossed from one measurement to the next during a flow event will end the current flow event and start a new one
|
||||
const idleFlow = 3.5; //the mass air flow that must be crossed in order to start a flow event, anything below this will not start an event but must go below this to end an event
|
||||
|
||||
useEffect(() => {
|
||||
if (loadingChartData) return;
|
||||
|
|
@ -102,7 +103,6 @@ export default function GateFlowGraph(props: Props) {
|
|||
timestamp: time.valueOf(),
|
||||
value: val.airFlow ?? 0
|
||||
};
|
||||
|
||||
data.push(newPoint);
|
||||
if (!recent || recent.timestamp < newPoint.timestamp) {
|
||||
recent = newPoint;
|
||||
|
|
|
|||
|
|
@ -333,7 +333,7 @@ export default function GateSVG(props: Props) {
|
|||
const deviceCheckIn = () => {
|
||||
return (
|
||||
<g key={"deviceCheckIn"}>
|
||||
<text x={95} y={12} fontSize={7} fontWeight={650} fill={"white"}>
|
||||
<text x={85} y={12} fontSize={7} fontWeight={650} fill={"white"}>
|
||||
Last Checked In: {moment(checkInTime).fromNow()}
|
||||
</text>
|
||||
</g>
|
||||
|
|
@ -342,7 +342,7 @@ export default function GateSVG(props: Props) {
|
|||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Box height={"100%"} maxWidth={isMobile ? 360 : 460} margin="auto">
|
||||
<Box height={"100%"} maxWidth={isMobile ? 400 : 460} margin="auto">
|
||||
<svg
|
||||
width={"100%"}
|
||||
height={"100%"}
|
||||
|
|
|
|||
213
src/grain/CustomGrainForm.tsx
Normal file
213
src/grain/CustomGrainForm.tsx
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
import {MenuItem, TextField } from "@mui/material"
|
||||
import { cloneDeep } from "lodash"
|
||||
import { pond } from "protobuf-ts/pond"
|
||||
import React, { useEffect, useRef, useState } from "react"
|
||||
|
||||
interface Props {
|
||||
grainSettings?: pond.GrainSettings
|
||||
onGrainSettingsChange: (settings: pond.GrainSettings, formValid: boolean) => void
|
||||
}
|
||||
|
||||
export default function CustomGrainForm(props: Props) {
|
||||
const {grainSettings, onGrainSettingsChange} = props
|
||||
const [newGrainSettings, setNewGrainSettings] = useState(pond.GrainSettings.create())
|
||||
const [name, setName] = useState("")
|
||||
const [group, setGroup] = useState("")
|
||||
const [equation, setEquation] = useState<pond.MoistureEquation>(pond.MoistureEquation.MOISTURE_EQUATION_NONE)
|
||||
const [constantA, setConstantA] = useState("0")
|
||||
const [constantB, setConstantB] = useState("0")
|
||||
const [constantC, setConstantC] = useState("0")
|
||||
const [kgPerBushel, setKgPerBushel] = useState("0")
|
||||
const [bushelsPerTonne, setBushelsPerTonne] = useState("0")
|
||||
const valid = useRef(false)
|
||||
|
||||
useEffect(()=>{
|
||||
if(grainSettings){
|
||||
setName(grainSettings.name)
|
||||
setGroup(grainSettings.group)
|
||||
setEquation(grainSettings.equation)
|
||||
setConstantA(grainSettings.a.toString())
|
||||
setConstantB(grainSettings.b.toString())
|
||||
setConstantC(grainSettings.c.toString())
|
||||
setKgPerBushel(grainSettings.kgPerBushel.toString())
|
||||
setBushelsPerTonne(grainSettings.bushelsPerTonne.toString())
|
||||
setNewGrainSettings(grainSettings)
|
||||
}
|
||||
},[grainSettings])
|
||||
|
||||
const validate = (name: string, group: string, constA: string, constB: string, constC: string, kgPerBushel: string, bushelsPerTonne: string) => {
|
||||
if (name === "") return false
|
||||
if (group === "") return false
|
||||
if (isNaN(parseFloat(constA))) return false
|
||||
if (isNaN(parseFloat(constB))) return false
|
||||
if (isNaN(parseFloat(constC))) return false
|
||||
if (isNaN(parseFloat(kgPerBushel))) return false
|
||||
if (isNaN(parseFloat(bushelsPerTonne))) return false
|
||||
return true
|
||||
}
|
||||
|
||||
const settingsChanged = (newSettings: pond.GrainSettings) => {
|
||||
setNewGrainSettings(newSettings)
|
||||
onGrainSettingsChange(newSettings, valid.current)
|
||||
}
|
||||
|
||||
const grainForm = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TextField
|
||||
sx={{marginBottom: 2}}
|
||||
fullWidth
|
||||
label="Name*"
|
||||
value={name}
|
||||
onChange={(e) => {
|
||||
let name = e.target.value
|
||||
setName(name)
|
||||
let settings = cloneDeep(newGrainSettings)
|
||||
settings.name = name
|
||||
valid.current = validate(name, group, constantA, constantB, constantC, kgPerBushel, bushelsPerTonne)
|
||||
settingsChanged(settings)
|
||||
}}/>
|
||||
<TextField
|
||||
sx={{marginBottom: 2}}
|
||||
fullWidth
|
||||
label="Group*"
|
||||
value={group}
|
||||
onChange={(e) => {
|
||||
let group = e.target.value
|
||||
setGroup(group)
|
||||
let settings = cloneDeep(newGrainSettings)
|
||||
settings.group = group
|
||||
valid.current = validate(name, group, constantA, constantB, constantC, kgPerBushel, bushelsPerTonne)
|
||||
settingsChanged(settings)
|
||||
}}/>
|
||||
<TextField
|
||||
sx={{marginBottom: 2}}
|
||||
fullWidth
|
||||
value={equation}
|
||||
helperText={"Not selecting an EMC equation to use will return the humidity instead of EMC"}
|
||||
label="EMC Equation"
|
||||
onChange={(e) => {
|
||||
let enumVal = parseFloat(e.target.value)
|
||||
setEquation(enumVal)
|
||||
let settings = cloneDeep(newGrainSettings)
|
||||
settings.equation = enumVal
|
||||
valid.current = validate(name, group, constantA, constantB, constantC, kgPerBushel, bushelsPerTonne)
|
||||
settingsChanged(settings)
|
||||
}}
|
||||
select>
|
||||
<MenuItem key={pond.MoistureEquation.MOISTURE_EQUATION_NONE} value={pond.MoistureEquation.MOISTURE_EQUATION_NONE}>
|
||||
None
|
||||
</MenuItem>
|
||||
<MenuItem key={pond.MoistureEquation.MOISTURE_EQUATION_CHUNG_PFOST} value={pond.MoistureEquation.MOISTURE_EQUATION_CHUNG_PFOST}>
|
||||
Chung-Pfost
|
||||
</MenuItem>
|
||||
<MenuItem key={pond.MoistureEquation.MOISTURE_EQUATION_HENDERSON} value={pond.MoistureEquation.MOISTURE_EQUATION_HENDERSON}>
|
||||
Henderson
|
||||
</MenuItem>
|
||||
<MenuItem key={pond.MoistureEquation.MOISTURE_EQUATION_HALSEY} value={pond.MoistureEquation.MOISTURE_EQUATION_HALSEY}>
|
||||
Halsey
|
||||
</MenuItem>
|
||||
<MenuItem key={pond.MoistureEquation.MOISTURE_EQUATION_OSWIN} value={pond.MoistureEquation.MOISTURE_EQUATION_OSWIN}>
|
||||
Oswin
|
||||
</MenuItem>
|
||||
</TextField>
|
||||
<TextField
|
||||
sx={{marginBottom: 2}}
|
||||
fullWidth
|
||||
type="number"
|
||||
value={constantA}
|
||||
error={isNaN(parseFloat(constantA))}
|
||||
helperText={isNaN(parseFloat(constantA)) ? "Must be a valid number" : ""}
|
||||
onChange={(e) => {
|
||||
let val = e.target.value
|
||||
setConstantA(val)
|
||||
valid.current = validate(name, group, val, constantB, constantC, kgPerBushel, bushelsPerTonne)
|
||||
let settings = cloneDeep(newGrainSettings)
|
||||
if(!isNaN(parseFloat(val))){
|
||||
settings.a = parseFloat(val)
|
||||
}
|
||||
settingsChanged(settings)
|
||||
}}
|
||||
label="Constant A"/>
|
||||
<TextField
|
||||
sx={{marginBottom: 2}}
|
||||
fullWidth
|
||||
type="number"
|
||||
value={constantB}
|
||||
error={isNaN(parseFloat(constantB))}
|
||||
helperText={isNaN(parseFloat(constantB)) ? "Must be a valid number" : ""}
|
||||
onChange={(e) => {
|
||||
let val = e.target.value
|
||||
setConstantB(val)
|
||||
valid.current = validate(name, group, constantA, val, constantC, kgPerBushel, bushelsPerTonne)
|
||||
let settings = cloneDeep(newGrainSettings)
|
||||
if(!isNaN(parseFloat(val))){
|
||||
settings.b = parseFloat(val)
|
||||
}
|
||||
settingsChanged(settings)
|
||||
}}
|
||||
label="Constant B"/>
|
||||
<TextField
|
||||
sx={{marginBottom: 2}}
|
||||
fullWidth
|
||||
type="number"
|
||||
value={constantC}
|
||||
error={isNaN(parseFloat(constantC))}
|
||||
helperText={isNaN(parseFloat(constantC)) ? "Must be a valid number" : ""}
|
||||
onChange={(e) => {
|
||||
let val = e.target.value
|
||||
setConstantC(val)
|
||||
valid.current = validate(name, group, constantA, constantB, val, kgPerBushel, bushelsPerTonne)
|
||||
let settings = cloneDeep(newGrainSettings)
|
||||
if(!isNaN(parseFloat(val))){
|
||||
settings.c = parseFloat(val)
|
||||
}
|
||||
settingsChanged(settings)
|
||||
}}
|
||||
label="Constant C"/>
|
||||
<TextField
|
||||
sx={{marginBottom: 2}}
|
||||
fullWidth
|
||||
type="number"
|
||||
value={kgPerBushel}
|
||||
error={isNaN(parseFloat(kgPerBushel))}
|
||||
helperText={isNaN(parseFloat(kgPerBushel)) ? "Must be a valid number" : ""}
|
||||
onChange={(e) => {
|
||||
let val = e.target.value
|
||||
setKgPerBushel(val)
|
||||
valid.current = validate(name, group, constantA, constantB, constantC, val, bushelsPerTonne)
|
||||
let settings = cloneDeep(newGrainSettings)
|
||||
if(!isNaN(parseFloat(val))){
|
||||
settings.kgPerBushel = parseFloat(val)
|
||||
}
|
||||
settingsChanged(settings)
|
||||
}}
|
||||
label="kg per Bushel"/>
|
||||
<TextField
|
||||
sx={{marginBottom: 2}}
|
||||
fullWidth
|
||||
type="number"
|
||||
value={bushelsPerTonne}
|
||||
error={isNaN(parseFloat(bushelsPerTonne))}
|
||||
helperText={isNaN(parseFloat(bushelsPerTonne)) ? "Must be a valid number" : ""}
|
||||
onChange={(e) => {
|
||||
let val = e.target.value
|
||||
setBushelsPerTonne(val)
|
||||
valid.current = validate(name, group, constantA, constantB, constantC, kgPerBushel, val)
|
||||
let settings = cloneDeep(newGrainSettings)
|
||||
if(!isNaN(parseFloat(val))){
|
||||
settings.bushelsPerTonne = parseFloat(val)
|
||||
}
|
||||
settingsChanged(settings)
|
||||
}}
|
||||
label="Bushels per Tonne"/>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{grainForm()}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
155
src/grain/CustomGrainSelector.tsx
Normal file
155
src/grain/CustomGrainSelector.tsx
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import { ExpandMore } from "@mui/icons-material"
|
||||
import { Accordion, AccordionDetails, AccordionSummary, Box, Button, Typography } from "@mui/material"
|
||||
import SearchSelect, { Option } from "common/SearchSelect"
|
||||
import { pond } from "protobuf-ts/pond"
|
||||
import { useGlobalState } from "providers"
|
||||
import { useGrainAPI } from "providers/pond/grainAPI"
|
||||
import React, { useEffect, useState } from "react"
|
||||
import CustomGrainForm from "./CustomGrainForm"
|
||||
|
||||
interface Props {
|
||||
initialGrain?: pond.GrainSettings
|
||||
onGrainSettingsChange: (settings: pond.GrainSettings) => void
|
||||
}
|
||||
|
||||
export default function CustomGrainSelector(props: Props) {
|
||||
const {initialGrain, onGrainSettingsChange} = props
|
||||
const grainAPI = useGrainAPI()
|
||||
const [{as}] = useGlobalState()
|
||||
const [newGrainSettings, setNewGrainSettings] = useState(initialGrain ?? pond.GrainSettings.create())
|
||||
const [formValid, setFormValid] = useState(true)
|
||||
const [grainMap, setGrainMap] = useState<Map<string, pond.GrainObject>>(
|
||||
initialGrain ?
|
||||
new Map([
|
||||
["Active", pond.GrainObject.create({key: "Active", name: initialGrain.name, settings: initialGrain})]
|
||||
])
|
||||
:
|
||||
new Map())
|
||||
const [grainOptions, setGrainOptions] = useState<Option[]>(
|
||||
initialGrain ?
|
||||
[
|
||||
{
|
||||
label: initialGrain.name,
|
||||
value: "Active",
|
||||
group: "Active"
|
||||
}
|
||||
]
|
||||
:
|
||||
[]
|
||||
)
|
||||
const [isNew, setIsNew] = useState(false)
|
||||
const [selectedOption, setSelectedOption] = useState<Option | null>(initialGrain ? {
|
||||
label: initialGrain.name,
|
||||
value: "Active",
|
||||
group: "Active"
|
||||
} : null)
|
||||
|
||||
useEffect(()=>{
|
||||
grainAPI.listGrains(0, 0, "asc", "name", undefined, undefined, undefined, as).then(resp => {
|
||||
let options: Option[] = grainOptions
|
||||
let map: Map<string, pond.GrainObject> = grainMap
|
||||
if (resp.data.grains){
|
||||
resp.data.grains.forEach(grain => {
|
||||
map.set(grain.key, grain)
|
||||
options.push({
|
||||
label: grain.name,
|
||||
value: grain.key,
|
||||
group: grain.settings?.group,
|
||||
})
|
||||
})
|
||||
}
|
||||
setGrainOptions([...options])
|
||||
setGrainMap(map)
|
||||
})
|
||||
},[grainAPI])
|
||||
|
||||
const saveGrain = () => {
|
||||
// console.log(newGrainSettings)
|
||||
grainAPI.addGrain(newGrainSettings, as).then(resp => {
|
||||
console.log("grain added")
|
||||
}).catch(err => {
|
||||
console.log("err")
|
||||
})
|
||||
}
|
||||
|
||||
const settingsChanged = (newSettings: pond.GrainSettings, formValid: boolean) => {
|
||||
setNewGrainSettings(newSettings)
|
||||
setIsNew(true)
|
||||
setFormValid(formValid)
|
||||
onGrainSettingsChange(newSettings)
|
||||
}
|
||||
|
||||
const grainAccordion = () => {
|
||||
return (
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMore />}>Custom Grain Properties</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<CustomGrainForm grainSettings={newGrainSettings} onGrainSettingsChange={settingsChanged}/>
|
||||
{isNew &&
|
||||
<Box display='flex' justifyContent="flex-end">
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={!formValid}
|
||||
onClick={() => {
|
||||
setIsNew(false)
|
||||
saveGrain()
|
||||
}}
|
||||
color="primary">
|
||||
Save Custom Grain
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{grainOptions.length === 0 ?
|
||||
<Box>
|
||||
<Typography sx={{marginBottom: 1}}>Custom Grain Properties</Typography>
|
||||
<CustomGrainForm grainSettings={newGrainSettings} onGrainSettingsChange={settingsChanged}/>
|
||||
{isNew &&
|
||||
<Box display='flex' justifyContent="flex-end">
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={!formValid}
|
||||
onClick={() => {
|
||||
setIsNew(false)
|
||||
}}
|
||||
color="primary">
|
||||
Save Custom Grain
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
</Box>
|
||||
:
|
||||
<Box>
|
||||
<Box sx={{marginBottom: 2}}>
|
||||
<SearchSelect
|
||||
label="My Grains"
|
||||
selected={selectedOption}
|
||||
changeSelection={option => {
|
||||
setIsNew(false)
|
||||
setSelectedOption(option)
|
||||
//when an option is selected set all of the state variables controlling the form entries
|
||||
let grain = grainMap.get(option?.value)
|
||||
if(grain && grain.settings){
|
||||
//the the new grain settings object
|
||||
setNewGrainSettings(grain.settings)
|
||||
//pass that settings object back up
|
||||
settingsChanged(grain.settings, true)
|
||||
}
|
||||
}}
|
||||
group
|
||||
options={grainOptions}
|
||||
/>
|
||||
</Box>
|
||||
{grainAccordion()}
|
||||
</Box>
|
||||
|
||||
}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
|
@ -641,6 +641,16 @@ export function GetGrainExtensionMap(): Map<pond.Grain, GrainExtension> {
|
|||
return cloneDeep(GrainExtensions);
|
||||
}
|
||||
|
||||
export function GetValidGrainExtensions(): GrainExtension[] {
|
||||
let validGrains: GrainExtension[] = []
|
||||
GrainExtensions.forEach((ext, key) => {
|
||||
if(key !== pond.Grain.GRAIN_NONE && key !== pond.Grain.GRAIN_INVALID && key !== pond.Grain.GRAIN_CUSTOM){
|
||||
validGrains.push(ext)
|
||||
}
|
||||
})
|
||||
return validGrains
|
||||
}
|
||||
|
||||
export default function GrainDescriber(type: pond.Grain): GrainExtension {
|
||||
let describer = GrainExtensions.get(type);
|
||||
//console.log(describer)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ const toERH = (humidity: number): number => {
|
|||
export function ExtractMoisture(
|
||||
type: pond.Grain | undefined,
|
||||
celsius: number,
|
||||
humidity: number
|
||||
humidity: number,
|
||||
customGrain?: pond.GrainSettings
|
||||
): number {
|
||||
if (humidity <= 0) {
|
||||
return 0;
|
||||
|
|
@ -26,20 +27,43 @@ export function ExtractMoisture(
|
|||
type === undefined ||
|
||||
type === pond.Grain.GRAIN_NONE ||
|
||||
type === pond.Grain.GRAIN_INVALID ||
|
||||
type === pond.Grain.GRAIN_CUSTOM
|
||||
(type === pond.Grain.GRAIN_CUSTOM && customGrain === undefined)
|
||||
) {
|
||||
return humidity;
|
||||
}
|
||||
|
||||
let dry = humidity;
|
||||
let eq: pond.MoistureEquation
|
||||
let constA: number
|
||||
let constB: number
|
||||
let constC: number
|
||||
if(type === pond.Grain.GRAIN_CUSTOM && customGrain){
|
||||
if(customGrain.equation === pond.MoistureEquation.MOISTURE_EQUATION_NONE) return humidity
|
||||
eq = customGrain.equation
|
||||
constA = customGrain.a
|
||||
constB = customGrain.b
|
||||
constC = customGrain.c
|
||||
}else{
|
||||
let ctx = GrainDescriber(type);
|
||||
dry = toDryMoisture(ctx.equation, celsius, humidity, ctx.a, ctx.b, ctx.c);
|
||||
eq = ctx.equation
|
||||
constA = ctx.a
|
||||
constB = ctx.b
|
||||
constC = ctx.c
|
||||
}
|
||||
|
||||
dry = toDryMoisture(eq, celsius, humidity, constA, constB, constC);
|
||||
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 WaterContent(type: pond.Grain, bushels: number, moistureContent: number, customGrain?: pond.GrainSettings): number {
|
||||
//kg per bushel conversion
|
||||
let conversion = 0
|
||||
if(type === pond.Grain.GRAIN_CUSTOM && customGrain){
|
||||
conversion = customGrain.kgPerBushel
|
||||
}else{
|
||||
conversion = GrainDescriber(type).weightConversionKg;
|
||||
}
|
||||
return bushels * conversion * moistureContent;
|
||||
}
|
||||
|
||||
export function MoistureToHumidity(
|
||||
|
|
@ -69,7 +93,7 @@ function wetToDry(wetMC: number): number {
|
|||
}
|
||||
|
||||
function toDryMoisture(
|
||||
eq: Equation,
|
||||
eq: pond.MoistureEquation,
|
||||
T: number,
|
||||
RH: number,
|
||||
a: number,
|
||||
|
|
@ -78,13 +102,13 @@ function toDryMoisture(
|
|||
): number {
|
||||
const ERH: number = toERH(RH);
|
||||
switch (eq) {
|
||||
case Equation.chungPfost:
|
||||
case pond.MoistureEquation.MOISTURE_EQUATION_CHUNG_PFOST:
|
||||
return chungPfost(T, ERH, a, b, c);
|
||||
case Equation.halsey:
|
||||
case pond.MoistureEquation.MOISTURE_EQUATION_HALSEY:
|
||||
return halsey(T, ERH, a, b, c);
|
||||
case Equation.henderson:
|
||||
case pond.MoistureEquation.MOISTURE_EQUATION_HENDERSON:
|
||||
return henderson(T, ERH, a, b, c);
|
||||
case Equation.oswin:
|
||||
case pond.MoistureEquation.MOISTURE_EQUATION_OSWIN:
|
||||
return oswin(T, ERH, a, b, c);
|
||||
default:
|
||||
return RH;
|
||||
|
|
@ -92,7 +116,7 @@ function toDryMoisture(
|
|||
}
|
||||
|
||||
function dryMoistureToHumidity(
|
||||
eq: Equation,
|
||||
eq: pond.MoistureEquation,
|
||||
celsius: number,
|
||||
dryMC: number,
|
||||
a: number,
|
||||
|
|
@ -101,16 +125,16 @@ function dryMoistureToHumidity(
|
|||
): number {
|
||||
let ERH = 0;
|
||||
switch (eq) {
|
||||
case Equation.chungPfost:
|
||||
case pond.MoistureEquation.MOISTURE_EQUATION_CHUNG_PFOST:
|
||||
ERH = chungPfostInverse(celsius, dryMC, a, b, c);
|
||||
break;
|
||||
case Equation.halsey:
|
||||
case pond.MoistureEquation.MOISTURE_EQUATION_HALSEY:
|
||||
ERH = halseyInverse(celsius, dryMC, a, b, c);
|
||||
break;
|
||||
case Equation.henderson:
|
||||
case pond.MoistureEquation.MOISTURE_EQUATION_HENDERSON:
|
||||
ERH = hendersonInverse(celsius, dryMC, a, b, c);
|
||||
break;
|
||||
case Equation.oswin:
|
||||
case pond.MoistureEquation.MOISTURE_EQUATION_OSWIN:
|
||||
ERH = oswinInverse(celsius, dryMC, a, b, c);
|
||||
break;
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ export default function GrainTransaction(props: Props) {
|
|||
.then(resp => {
|
||||
//let sourceOps: Option[] = sourceOptions
|
||||
let binOps: Option[] = [];
|
||||
if(resp.data.bins){
|
||||
resp.data.bins.forEach(bin => {
|
||||
let b = Bin.create(bin);
|
||||
if (mainObject.key() !== b.key()) {
|
||||
|
|
@ -133,9 +134,10 @@ export default function GrainTransaction(props: Props) {
|
|||
value: b,
|
||||
group: "Bins"
|
||||
};
|
||||
(!restrictMatching || grainTypesMatch(op.value, mainObject)) && binOps.push(op);
|
||||
(!restrictMatching || grainTypesMatch(op.value, mainObject) || op.value.grainName() === mainObject.grainName()) && binOps.push(op);
|
||||
}
|
||||
});
|
||||
}
|
||||
setBinOptions([...binOps]);
|
||||
})
|
||||
.finally(() => {
|
||||
|
|
@ -151,6 +153,7 @@ export default function GrainTransaction(props: Props) {
|
|||
.then(resp => {
|
||||
//let sourceOps: Option[] = sourceOptions
|
||||
let bagOps: Option[] = [];
|
||||
if(resp.data.grainBags){
|
||||
resp.data.grainBags.forEach(bag => {
|
||||
let b = GrainBag.create(bag);
|
||||
if (mainObject.key() !== b.key()) {
|
||||
|
|
@ -159,9 +162,10 @@ export default function GrainTransaction(props: Props) {
|
|||
value: b,
|
||||
group: "Grain Bags"
|
||||
};
|
||||
(!restrictMatching || grainTypesMatch(op.value, mainObject)) && bagOps.push(op);
|
||||
(!restrictMatching || grainTypesMatch(op.value, mainObject) || op.value.grainName() === mainObject.grainName()) && bagOps.push(op);
|
||||
}
|
||||
});
|
||||
}
|
||||
setBagOptions([...bagOps]);
|
||||
})
|
||||
.finally(() => {
|
||||
|
|
@ -184,7 +188,7 @@ export default function GrainTransaction(props: Props) {
|
|||
value: f,
|
||||
group: "Fields"
|
||||
};
|
||||
(!restrictMatching || grainTypesMatch(op.value, mainObject)) && fieldOps.push(op);
|
||||
(!restrictMatching || grainTypesMatch(op.value, mainObject) || op.value.grainName() === mainObject.grainName()) && fieldOps.push(op);
|
||||
});
|
||||
setFieldOptions([...fieldOps]);
|
||||
})
|
||||
|
|
@ -356,13 +360,6 @@ export default function GrainTransaction(props: Props) {
|
|||
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;
|
||||
|
|
@ -373,7 +370,7 @@ export default function GrainTransaction(props: Props) {
|
|||
if (grainTypesMatch(selectedSource.value, selectedDestination.value)) {
|
||||
updateInventory();
|
||||
} else {
|
||||
//open dialog saying that the destinations grain type will change from this action
|
||||
//open dialog saying that the grain types do not match or because it is a custom type
|
||||
setGrainChangeDialog(true);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -381,6 +378,36 @@ export default function GrainTransaction(props: Props) {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* the function is used to determine what text to display to the user when the grain types dont match
|
||||
* or when it sees any of the objects involved as having a custom grain type
|
||||
* @param source the source of the grain
|
||||
* @param destination the destination of the grain
|
||||
* @returns the text describing the situation
|
||||
*/
|
||||
const confirmationText = () => {
|
||||
if(selectedSource && selectedDestination){
|
||||
|
||||
let source = selectedSource?.value
|
||||
let destination = selectedDestination?.value
|
||||
//both are custom
|
||||
if(source.storage() === pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN && destination.storage() === pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN){
|
||||
return "Both source and destination are using custom grain types, they may not match. Would you like to continue with the transaction?"
|
||||
}
|
||||
//source is custom destination is not
|
||||
if(source.storage() === pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN){
|
||||
return "The source is using a custom grain type and may not match the destination. Would you like to continue with the transaction?"
|
||||
}
|
||||
//destination is custom source is not
|
||||
if(destination.storage() === pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN){
|
||||
return "The destination is using a custom grain type and may not match the source. Would you like to continue with the transaction?"
|
||||
}
|
||||
}
|
||||
|
||||
//neither are custom
|
||||
return "The Grain types do not match. Are you sure you would like to perform this transaction?"
|
||||
}
|
||||
|
||||
const grainChange = () => {
|
||||
return (
|
||||
<ResponsiveDialog
|
||||
|
|
@ -389,7 +416,7 @@ export default function GrainTransaction(props: Props) {
|
|||
setGrainChangeDialog(false);
|
||||
}}>
|
||||
<DialogContent>
|
||||
The grain type of the destination may change as a result of this action.
|
||||
{confirmationText()}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -90,11 +90,19 @@ export class Bin {
|
|||
public customType(): string {
|
||||
let c = "";
|
||||
if (this.settings.inventory) {
|
||||
if (this.settings.inventory.customGrain){
|
||||
c = this.settings.inventory.customGrain.name
|
||||
}else{
|
||||
c = this.settings.inventory.customTypeName;
|
||||
}
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
public customGrain(): pond.GrainSettings | undefined {
|
||||
return this.settings.inventory?.customGrain ?? undefined
|
||||
}
|
||||
|
||||
public empty(): boolean {
|
||||
return this.settings.inventory?.empty || (this.settings.inventory !== undefined && this.settings.inventory !== null && this.bushels() < 5);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -142,4 +142,9 @@ export class Component {
|
|||
return getFriendlyAddressTypeName(this.settings.addressType);
|
||||
}
|
||||
};
|
||||
|
||||
public customGrainProps(): pond.GrainSettings | undefined {
|
||||
if(this.settings.customGrain) return this.settings.customGrain
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ const JohnDeere = lazy(() => import("pages/JohnDeere"));
|
|||
const CNHi = lazy(() => import("pages/CNHi"));
|
||||
const LibraCart = lazy(() => import("pages/LibraCart"));
|
||||
const FieldPage = lazy(()=>import("pages/Field"));
|
||||
const GrainsPage = lazy(()=>import("pages/Grains"));
|
||||
|
||||
export const appendToUrl = (appendage: number | string) => {
|
||||
const basePath = location.pathname.replace(/\/$/, "");
|
||||
|
|
@ -360,6 +361,7 @@ export default function Router() {
|
|||
{user.hasFeature("libra-cart") &&
|
||||
<Route path="libracart" element={<LibraCart />} />
|
||||
}
|
||||
<Route path="grains" element={<GrainsPage />} />
|
||||
{/* Map routes */}
|
||||
<Route path="visualFarm" element={<FieldMap />} />
|
||||
<Route path="aviationMap" element={<AviationMap />} />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ChevronRight, Code, Memory, People, Person, SyncAlt, TapAndPlay } from "@mui/icons-material";
|
||||
import { ChevronRight, Code, Grain, Memory, People, Person, SyncAlt, TapAndPlay } from "@mui/icons-material";
|
||||
import ChevronLeft from "@mui/icons-material/ChevronLeft";
|
||||
import {
|
||||
darken,
|
||||
|
|
@ -374,6 +374,20 @@ export default function SideNavigator(props: Props) {
|
|||
</ListItemButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{(isAg || isStreamline || user.hasFeature("admin")) && (
|
||||
<Tooltip title="Grain Types" placement="right">
|
||||
<ListItemButton
|
||||
id="tour-grain-types"
|
||||
onClick={() => goTo("/grains")}
|
||||
classes={getClasses("/grains")}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Grain />
|
||||
</ListItemIcon>
|
||||
{open && <ListItemText primary="Grain Types" />}
|
||||
</ListItemButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{(isAdCon || user.hasFeature("admin")) && (
|
||||
<Tooltip title="Jobsites" placement="right">
|
||||
|
|
|
|||
|
|
@ -819,6 +819,7 @@ export default function Bin(props: Props) {
|
|||
fans={fans}
|
||||
heaters={heaters}
|
||||
grain={bin.grain()}
|
||||
customGrain={bin.customGrain()}
|
||||
/>
|
||||
)}
|
||||
<Box marginTop={2}>
|
||||
|
|
@ -968,6 +969,7 @@ export default function Bin(props: Props) {
|
|||
fans={fans}
|
||||
heaters={heaters}
|
||||
grain={bin.grain()}
|
||||
customGrain={bin.customGrain()}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -196,9 +196,10 @@ export default function Bins(props: Props) {
|
|||
const { openSnack } = useSnackbar();
|
||||
// const history = useHistory();
|
||||
const navigate = useNavigate()
|
||||
const [cardValDisplay, setCardValDisplay] = useState<"high" | "low" | "average">("average");
|
||||
const [cardValDisplay, setCardValDisplay] = useState<"high" | "low" | "average">("high");
|
||||
|
||||
const [teamsDialog, setTeamsDialog] = useState(false)
|
||||
const snackbar = useSnackbar()
|
||||
|
||||
// const [scrollTranslations, setScrollTranslations] = useState({
|
||||
// bins: 0,
|
||||
|
|
@ -261,10 +262,12 @@ export default function Bins(props: Props) {
|
|||
if (doneLoadingPage && grainBins.length < 1 && grainBags.length < 1) {
|
||||
teamAPI.listTeams(1, 0).then(resp => {
|
||||
// console.log(resp.data.teams)
|
||||
if (resp.data.teams.length > 0) {
|
||||
if (resp.data?.teams?.length > 0) {
|
||||
// console.log("should?")
|
||||
setTeamsDialog(true)
|
||||
}
|
||||
}).catch(err => {
|
||||
snackbar.error("error listing teams: "+err)
|
||||
})
|
||||
}
|
||||
}, [doneLoadingPage, grainBins, grainBags])
|
||||
|
|
|
|||
|
|
@ -158,9 +158,9 @@ export default function DevicePage() {
|
|||
if (length > 0) {
|
||||
warningString = warningString + " through " + getContextTypes()[length - 1];
|
||||
}
|
||||
snackbar.warning(warningString);
|
||||
snackbar.warning("page data warning: "+warningString);
|
||||
} else {
|
||||
snackbar.error(err);
|
||||
snackbar.error("page data error: "+err);
|
||||
}
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
|
|
@ -174,7 +174,7 @@ export default function DevicePage() {
|
|||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
console.log("load port scan: "+err)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
474
src/pages/Grains.tsx
Normal file
474
src/pages/Grains.tsx
Normal file
|
|
@ -0,0 +1,474 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import PageContainer from "./PageContainer";
|
||||
import CustomGrainForm from "grain/CustomGrainForm";
|
||||
import { Accordion, AccordionDetails, AccordionSummary, Box, Button, DialogActions, DialogContent, DialogTitle, Divider, Grid2, IconButton, Typography } from "@mui/material";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import ButtonGroup from "common/ButtonGroup";
|
||||
import { useMobile, useSnackbar, useUserAPI } from "hooks";
|
||||
import { useGrainAPI } from "providers/pond/grainAPI";
|
||||
import ResponsiveTable from "common/ResponsiveTable";
|
||||
import { GetGrainExtensionMap, GetValidGrainExtensions, GrainExtension } from "grain";
|
||||
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||
import { useGlobalState } from "providers";
|
||||
import { Scope } from "models";
|
||||
import React from "react";
|
||||
import { AddCircle, ExpandMore } from "@mui/icons-material";
|
||||
|
||||
export default function Grains() {
|
||||
const [currentCustomGrain, setCurrentCustomGrain] = useState<pond.GrainSettings>()
|
||||
const [formValid, setFormValid] = useState(false)
|
||||
const [selectedGrain, setSelectedGrain] = useState<pond.GrainObject | undefined>()
|
||||
const [displayCustom, setDisplayCustom] = useState(false)
|
||||
const { openSnack } = useSnackbar()
|
||||
const [tableSize, setTableSize] = useState(10)
|
||||
const [tablePage, setTablePage] = useState(0)
|
||||
const [tableTotal, setTableTotal] = useState(0)
|
||||
const grainAPI = useGrainAPI()
|
||||
const userAPI = useUserAPI()
|
||||
const [{as, user}] = useGlobalState();
|
||||
const [permissions, setPermissions] = useState<pond.Permission[]>([])
|
||||
const [openRemoveConfirmation, setOpenRemoveConfirmation] = useState(false)
|
||||
const [openUpdateConfirmation, setOpenUpdateConfirmation] = useState(false)
|
||||
const [grainObjectTableData, setGrainObjectTableData] = useState<pond.GrainObject[]>([])
|
||||
const [supportedGrainTableData, setSupportedGrainTableData] = useState<GrainExtension[]>([])
|
||||
// const [selectedGrainExtension, setSelectedGrainExtension] = useState<GrainExtension | undefined>()
|
||||
const [accordionKey, setAccordionKey] = useState("allClosed")
|
||||
const [openNewGrainDialog, setOpenNewGrainDialog] = useState(false)
|
||||
const isMobile = useMobile()
|
||||
const extraLoad = 10 //the amount of more to load on the mobile view when load more is clicked
|
||||
|
||||
//function to load the first set of custom grains
|
||||
const loadCustomGrain = useCallback(() => {
|
||||
grainAPI.listGrains(tableSize, tablePage * tableSize, "asc", "name").then(resp => {
|
||||
setGrainObjectTableData(resp.data.grains)
|
||||
setTableTotal(resp.data.total)
|
||||
}).catch(err => {
|
||||
openSnack("There was an issue loading your custom grains")
|
||||
})
|
||||
},[tableSize, tablePage])
|
||||
|
||||
useEffect(() => {
|
||||
//load the custom grains
|
||||
if(displayCustom){
|
||||
loadCustomGrain()
|
||||
}else{
|
||||
let displayedGrains: GrainExtension[] = []
|
||||
let offset = tablePage * tableSize
|
||||
let limit = tableSize
|
||||
let grainList = GetValidGrainExtensions()
|
||||
let sortedGrain = grainList.sort((a, b) => (a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1))
|
||||
sortedGrain.forEach((ext, i) => {
|
||||
if(i >= offset && displayedGrains.length < limit){
|
||||
displayedGrains.push(ext)
|
||||
}
|
||||
})
|
||||
setSupportedGrainTableData(displayedGrains)
|
||||
setTableTotal(sortedGrain.length)
|
||||
}
|
||||
},[loadCustomGrain, tableSize, tablePage, displayCustom, as])
|
||||
|
||||
useEffect(()=>{
|
||||
//if the user is viewing as a team get their permissions to the team, if they are not then we will get their permission to the grain type individually when a type is selected
|
||||
if(as){
|
||||
userAPI.getUser(user.id(), { key: as, kind: "team" } as Scope).then(resp => {
|
||||
setPermissions(resp.permissions);
|
||||
});
|
||||
}
|
||||
},[as, userAPI])
|
||||
|
||||
const equationName = (enumVal: number) => {
|
||||
switch(enumVal){
|
||||
case pond.MoistureEquation.MOISTURE_EQUATION_CHUNG_PFOST:
|
||||
return "Chung-Pfost"
|
||||
case pond.MoistureEquation.MOISTURE_EQUATION_HALSEY:
|
||||
return "Halsey"
|
||||
case pond.MoistureEquation.MOISTURE_EQUATION_HENDERSON:
|
||||
return "Henderson"
|
||||
case pond.MoistureEquation.MOISTURE_EQUATION_OSWIN:
|
||||
return "Oswin"
|
||||
default:
|
||||
return "None"
|
||||
}
|
||||
}
|
||||
|
||||
//function to update
|
||||
|
||||
//table that will display the custom grains created by the user
|
||||
const grainObjectTable = () => {
|
||||
return (
|
||||
<ResponsiveTable<pond.GrainObject>
|
||||
rows={grainObjectTableData}
|
||||
noDataMessage="No Grain Types Found. Add a new custom grain to see them here"
|
||||
loadMore={() => {
|
||||
setTableSize(tableSize + extraLoad)
|
||||
}}
|
||||
renderMobile={(row) => <Accordion expanded={accordionKey === row.key} onChange={() => {
|
||||
//close it if it is already open
|
||||
if(accordionKey === row.key){
|
||||
setAccordionKey("allClosed")
|
||||
}else{
|
||||
setAccordionKey(row.key)
|
||||
}
|
||||
setCurrentCustomGrain(row.settings ?? pond.GrainSettings.create())
|
||||
}}>
|
||||
<AccordionSummary expandIcon={<ExpandMore />}>{row.name}</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{customGrainDisplay()}
|
||||
</AccordionDetails>
|
||||
</Accordion>}
|
||||
onRowClick={(grainObj) => {
|
||||
setSelectedGrain(grainObj)
|
||||
setFormValid(true)
|
||||
/** if they are viewin as a team leave the permissions alone, if not then use the users permissions to the grain type,
|
||||
* for the moment grain types are not shareable so this will be unused, but this will cover our bases if we want to implement sharing in the future
|
||||
* */
|
||||
if (!as) {
|
||||
userAPI.getUser(user.id(), { key: grainObj.key, kind: "grain" } as Scope).then(resp => {
|
||||
setPermissions(resp.permissions);
|
||||
});
|
||||
}
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
title: "Name",
|
||||
render: row => <Box padding={2}>
|
||||
<Typography>
|
||||
{row.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
},
|
||||
{
|
||||
title: "Group",
|
||||
render: row => <Box padding={2}>
|
||||
<Typography>
|
||||
{row.settings?.group}
|
||||
</Typography>
|
||||
</Box>
|
||||
},
|
||||
{
|
||||
title: "Equation",
|
||||
render: row => <Box padding={2}>
|
||||
<Typography>
|
||||
{equationName(row.settings?.equation ?? 0)}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
]}
|
||||
page={tablePage}
|
||||
pageSize={tableSize}
|
||||
handleRowsPerPageChange={(e) => {
|
||||
setTableSize(e.target.value)
|
||||
}}
|
||||
setPage={(page) => {
|
||||
setTablePage(page)
|
||||
}}
|
||||
total={tableTotal}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
//table that will display the grain extensions provided by us
|
||||
const supportedGrainTable = () => {
|
||||
return (
|
||||
<ResponsiveTable<GrainExtension>
|
||||
rows={supportedGrainTableData}
|
||||
page={tablePage}
|
||||
// onRowClick={(ext) => {setSelectedGrainExtension(ext)}}
|
||||
loadMore={() => {
|
||||
setTableSize(tableSize + extraLoad)
|
||||
}}
|
||||
renderMobile={(row) => <Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMore />}>{row.name}</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{supportedDisplay(row)}
|
||||
</AccordionDetails>
|
||||
</Accordion>}
|
||||
pageSize={tableSize}
|
||||
columns={[
|
||||
{
|
||||
title: "Name",
|
||||
render: row => <Box padding={2}>
|
||||
<Typography>
|
||||
{row.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
},
|
||||
{
|
||||
title: "Group",
|
||||
render: row => <Box padding={2}>
|
||||
<Typography>
|
||||
{row.group}
|
||||
</Typography>
|
||||
</Box>
|
||||
},
|
||||
{
|
||||
title: "Equation",
|
||||
render: row => <Box padding={2}>
|
||||
<Typography>
|
||||
{equationName(row.equation ?? 0)}
|
||||
</Typography>
|
||||
</Box>
|
||||
},
|
||||
{
|
||||
title: "Kg Per Bushel",
|
||||
render: row => <Box padding={2}>
|
||||
<Typography>
|
||||
{row.weightConversionKg.toFixed(2)}
|
||||
</Typography>
|
||||
</Box>
|
||||
},
|
||||
{
|
||||
title: "Bushels Per Tonne",
|
||||
render: row => <Box padding={2}>
|
||||
<Typography>
|
||||
{row.bushelsPerTonne.toFixed(2)}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
]}
|
||||
handleRowsPerPageChange={(e) => {
|
||||
setTableSize(e.target.value)
|
||||
}}
|
||||
setPage={(page) => {
|
||||
setTablePage(page)
|
||||
}}
|
||||
total={tableTotal}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
//the grain display for supported grain type to show the rest of the data such as what constants are used in the formula
|
||||
const supportedDisplay = (grain?: GrainExtension) => {
|
||||
return (
|
||||
<Box>
|
||||
{grain ?
|
||||
<React.Fragment>
|
||||
<Typography>
|
||||
Info
|
||||
</Typography>
|
||||
<Divider />
|
||||
<Box padding={2}>
|
||||
<Typography>Name: {grain.name}</Typography>
|
||||
<Typography>Group: {grain.group}</Typography>
|
||||
</Box>
|
||||
<Typography>
|
||||
Formula
|
||||
</Typography>
|
||||
<Divider />
|
||||
<Box padding={2}>
|
||||
<Typography>Equation: {equationName(grain.equation)}</Typography>
|
||||
{/* <Typography>Constant A: {grain.a}</Typography>
|
||||
<Typography>Constant B: {grain.b}</Typography>
|
||||
<Typography>Constant C: {grain.c}</Typography> */}
|
||||
</Box>
|
||||
<Typography>
|
||||
Conversions
|
||||
</Typography>
|
||||
<Divider />
|
||||
<Box padding={2}>
|
||||
<Typography>Kg per Bushel: {grain.weightConversionKg.toFixed(2)}</Typography>
|
||||
<Typography>Bushels per Tonne: {grain.bushelsPerTonne.toFixed(2)}</Typography>
|
||||
</Box>
|
||||
</React.Fragment>
|
||||
:
|
||||
<Typography>Select a Grain from the table to view its details</Typography>
|
||||
}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const updateGrain = () => {
|
||||
if(selectedGrain && currentCustomGrain){
|
||||
grainAPI.updateGrain(selectedGrain.key, currentCustomGrain).then(resp => {
|
||||
openSnack("Grain type updated")
|
||||
}).catch(err => {
|
||||
openSnack("There was an issue updating the selected grain type")
|
||||
}).finally(() => {
|
||||
setOpenUpdateConfirmation(false)
|
||||
loadCustomGrain()
|
||||
})
|
||||
}
|
||||
}
|
||||
const updateGrainConfirmation = () => {
|
||||
return (
|
||||
<ResponsiveDialog open={openUpdateConfirmation} onClose={() => {setOpenUpdateConfirmation(false)}}>
|
||||
<DialogTitle>Update Custom Grain</DialogTitle>
|
||||
<DialogContent>
|
||||
Update existing custom grain type?
|
||||
<br/>
|
||||
Note that updating the grain here will not change any bins or components this type was used for, those will retain the old settings.
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" color="primary" onClick={()=>{setOpenUpdateConfirmation(false)}}>Cancel</Button>
|
||||
<Button variant="contained" color="primary" onClick={()=>{updateGrain()}}>Confirm</Button>
|
||||
</DialogActions>
|
||||
</ResponsiveDialog>
|
||||
)
|
||||
}
|
||||
|
||||
//because the form is embedded on desktop this dialog is specifically for mobile view
|
||||
const newGrainDialog = () => {
|
||||
return (
|
||||
<ResponsiveDialog open={openNewGrainDialog} onClose={() => setOpenNewGrainDialog(false)}>
|
||||
<DialogTitle>Add New Grain</DialogTitle>
|
||||
<DialogActions>
|
||||
{customGrainDisplay()}
|
||||
|
||||
</DialogActions>
|
||||
</ResponsiveDialog>
|
||||
)
|
||||
}
|
||||
|
||||
const addNewGrain = () => {
|
||||
if(currentCustomGrain){
|
||||
grainAPI.addGrain(currentCustomGrain).then(resp => {
|
||||
//new grain added
|
||||
openSnack("New grain type added")
|
||||
}).catch(err => {
|
||||
openSnack("There was a problem creating the new grain type")
|
||||
}).finally(() => {
|
||||
loadCustomGrain()
|
||||
setOpenNewGrainDialog(false)//only for the mobile view since the desktop has it embedded
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const removeGrain = () => {
|
||||
if(selectedGrain){
|
||||
grainAPI.removeGrain(selectedGrain.key).then(resp => {
|
||||
openSnack("Grain type removed")
|
||||
}).catch(err => {
|
||||
openSnack("There was a problem removing the selected grain type")
|
||||
}).finally(() => {
|
||||
setOpenRemoveConfirmation(false)
|
||||
loadCustomGrain()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const removeConfirmation = () => {
|
||||
return (
|
||||
<ResponsiveDialog open={openRemoveConfirmation} onClose={() => {setOpenRemoveConfirmation(false)}}>
|
||||
<DialogTitle>Remove Custom Grain</DialogTitle>
|
||||
<DialogContent>
|
||||
Remove custom grain type?
|
||||
<br />
|
||||
Note that removing the grain here will not change any bins or components this type was used for, those will retain the old settings.
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" color="primary" onClick={()=>{setOpenRemoveConfirmation(false)}}>Cancel</Button>
|
||||
<Button variant="contained" color="primary" onClick={()=>{removeGrain()}}>Confirm</Button>
|
||||
</DialogActions>
|
||||
</ResponsiveDialog>
|
||||
)
|
||||
}
|
||||
|
||||
const customGrainActions = () => {
|
||||
return (
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Box>
|
||||
{selectedGrain &&
|
||||
<Button disabled={!permissions.includes(pond.Permission.PERMISSION_WRITE)} variant="contained" color="error" onClick={() => {setOpenRemoveConfirmation(true)}}>Remove Grain</Button>
|
||||
}
|
||||
</Box>
|
||||
<Box>
|
||||
{selectedGrain &&
|
||||
<Button sx={{marginRight: 1}} onClick={() => setOpenUpdateConfirmation(true)} disabled={!formValid || !permissions.includes(pond.Permission.PERMISSION_WRITE)} variant="contained" color="primary">Update</Button>
|
||||
}
|
||||
{isMobile && !selectedGrain && <Button onClick={() => {setOpenNewGrainDialog(false)}}>Cancel</Button>}
|
||||
<Button onClick={() => addNewGrain()} disabled={!formValid || (as !== "" && !permissions.includes(pond.Permission.PERMISSION_WRITE))} variant="contained" color="primary">Save New</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
//the grain display for custom grain the user has defined
|
||||
const customGrainDisplay = () => {
|
||||
return (
|
||||
<Box>
|
||||
<CustomGrainForm
|
||||
grainSettings={selectedGrain?.settings || undefined}
|
||||
onGrainSettingsChange={(newSettings, formValid)=>{
|
||||
setCurrentCustomGrain(newSettings)
|
||||
setFormValid(formValid)
|
||||
}}
|
||||
/>
|
||||
{customGrainActions()}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const desktopView = () => {
|
||||
if(displayCustom){
|
||||
return (
|
||||
<Grid2 container>
|
||||
<Grid2 size={7} padding={2}>
|
||||
{grainObjectTable()}
|
||||
</Grid2>
|
||||
<Grid2 size={5} padding={2}>
|
||||
{customGrainDisplay()}
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
)
|
||||
}else{
|
||||
return (
|
||||
<Box marginTop={2}>
|
||||
{supportedGrainTable()}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const mobileView = () => {
|
||||
return (
|
||||
<Box>
|
||||
{displayCustom ? grainObjectTable() : supportedGrainTable()}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<Box padding={2}>
|
||||
<Typography variant="h4">
|
||||
Adaptive Grains
|
||||
</Typography>
|
||||
<Box display="flex" marginTop={2} justifyContent="space-between">
|
||||
<ButtonGroup
|
||||
toggle
|
||||
buttons={[
|
||||
{
|
||||
title: "Supported",
|
||||
function: () => {
|
||||
setDisplayCustom(false)
|
||||
setTablePage(0)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Custom",
|
||||
function: () => {
|
||||
setDisplayCustom(true)
|
||||
setTablePage(0)
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
{isMobile && displayCustom &&
|
||||
<IconButton sx={{padding: 0}} color="primary" onClick={()=>{
|
||||
setSelectedGrain(undefined)
|
||||
setCurrentCustomGrain(undefined)
|
||||
setOpenNewGrainDialog(true)
|
||||
}}>
|
||||
<AddCircle />
|
||||
</IconButton>
|
||||
}
|
||||
</Box>
|
||||
{!isMobile ? desktopView() : mobileView()}
|
||||
</Box>
|
||||
{updateGrainConfirmation()}
|
||||
{removeConfirmation()}
|
||||
{newGrainDialog()}
|
||||
</PageContainer>
|
||||
)
|
||||
}
|
||||
|
|
@ -181,7 +181,8 @@ export default function Terminals(props: Props) {
|
|||
const load = useCallback(() => {
|
||||
if(loading) return
|
||||
setLoading(true)
|
||||
terminalAPI.listTerminals(200, 0, "asc", "name", as).then(resp => {
|
||||
terminalAPI.listTerminals(200, 0, "asc", "name", undefined, as).then(resp => {
|
||||
console.log(resp.data)
|
||||
if(resp.data.terminals){
|
||||
setTerminals(resp.data.terminals.map(a => Terminal.any(a)));
|
||||
}
|
||||
|
|
|
|||
137
src/providers/pond/grainAPI.tsx
Normal file
137
src/providers/pond/grainAPI.tsx
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import { useHTTP } from "hooks";
|
||||
import { createContext, PropsWithChildren, useContext } from "react";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { pondURL } from "./pond";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { useGlobalState } from "providers";
|
||||
import { or } from "utils";
|
||||
|
||||
export interface IGrainInterface {
|
||||
addGrain: (
|
||||
settings: pond.GrainSettings,
|
||||
otherTeam?: string
|
||||
) => Promise<AxiosResponse<pond.AddGrainResponse>>;
|
||||
getGrain: (key: string, otherTeam?: string) => Promise<AxiosResponse<pond.GetGrainResponse>>
|
||||
listGrains: (
|
||||
limit: number,
|
||||
offset: number,
|
||||
order?: "asc" | "desc",
|
||||
orderBy?: string,
|
||||
search?: string,
|
||||
keys?: string[],
|
||||
types?: string[],
|
||||
otherTeam?: string,
|
||||
) => Promise<AxiosResponse<pond.ListGrainsResponse>>;
|
||||
updateGrain: (key: string, settings: pond.GrainSettings, otherTeam?: string) => Promise<AxiosResponse<pond.UpdateGrainResponse>>
|
||||
removeGrain: (key: string, otherTeam?: string) => Promise<AxiosResponse<pond.RemoveGrainResponse>>;
|
||||
}
|
||||
|
||||
export const GrainAPIcontext = createContext<IGrainInterface>({} as IGrainInterface);
|
||||
|
||||
interface Props {}
|
||||
|
||||
export default function GrainProvider(props: PropsWithChildren<Props>) {
|
||||
const { children } = props;
|
||||
const { get, del, post, put } = useHTTP();
|
||||
const [{ as }] = useGlobalState();
|
||||
|
||||
//add
|
||||
const addGrain = (settings: pond.GrainSettings, otherTeam?: string) => {
|
||||
const view = otherTeam ? otherTeam : as
|
||||
if (view) {
|
||||
return post<pond.AddGrainResponse>(
|
||||
pondURL("/grains?&as=" + view),
|
||||
settings
|
||||
);
|
||||
}
|
||||
return post<pond.AddGrainResponse>(pondURL("/grains"), settings);
|
||||
};
|
||||
|
||||
//get
|
||||
const getGrain = (key: string, otherTeam?: string) => {
|
||||
const view = otherTeam ? otherTeam : as
|
||||
const url = "/grains/" + key + (view ? "?as=" + view : "")
|
||||
return new Promise<AxiosResponse<pond.GetGrainResponse>>((resolve, reject)=>{
|
||||
get<pond.GetGrainResponse>(pondURL(url))
|
||||
.then(resp => {
|
||||
if (resp.data.grain){
|
||||
resp.data.grain = pond.GrainObject.fromObject(resp.data.grain)
|
||||
}
|
||||
resolve(resp)
|
||||
})
|
||||
.catch(err => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
//list
|
||||
const listGrains = (
|
||||
limit: number,
|
||||
offset: number,
|
||||
order?: "asc" | "desc",
|
||||
orderBy?: string,
|
||||
search?: string,
|
||||
keys?: string[],
|
||||
types?: string[],
|
||||
otherTeam?: string
|
||||
) => {
|
||||
return new Promise<AxiosResponse<pond.ListGrainsResponse>>((resolve, reject)=>{
|
||||
const view = otherTeam ? otherTeam : as
|
||||
get<pond.ListGrainsResponse>(
|
||||
pondURL(
|
||||
"/grains?limit=" +
|
||||
limit +
|
||||
"&offset=" +
|
||||
offset +
|
||||
("&order=" + or(order, "asc")) +
|
||||
("&by=" + or(orderBy, "key")) +
|
||||
(search ? "&search=" + search : "") +
|
||||
(keys ? "&keys=" + keys.join(",") : "") +
|
||||
(types ? "&types=" + types.join(",") : "") +
|
||||
(view ? "&as=" + view : "")
|
||||
)
|
||||
).then(resp => {
|
||||
resp.data = pond.ListGrainsResponse.fromObject(resp.data);
|
||||
return resolve(resp)
|
||||
}).catch(err => {
|
||||
return reject(err)
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
//update
|
||||
const updateGrain = (key: string, settings: pond.GrainSettings, otherTeam?: string) => {
|
||||
const view = otherTeam ? otherTeam : as
|
||||
if (view) {
|
||||
return put<pond.UpdateGrainResponse>(
|
||||
pondURL("/grains/"+ key + "?&as=" + view),
|
||||
settings
|
||||
);
|
||||
}
|
||||
return put<pond.UpdateGrainResponse>(pondURL("/grains/" + key), settings);
|
||||
}
|
||||
|
||||
//remove
|
||||
const removeGrain = (key: string, otherTeam?: string) => {
|
||||
const view = otherTeam ? otherTeam : as
|
||||
if (view) {
|
||||
return del<pond.RemoveGrainResponse>(pondURL("/grains/" + key + "?as=" + view));
|
||||
}
|
||||
return del<pond.RemoveGrainResponse>(pondURL("/grains/" + key));
|
||||
};
|
||||
|
||||
return (
|
||||
<GrainAPIcontext.Provider
|
||||
value={{
|
||||
addGrain,
|
||||
getGrain,
|
||||
listGrains,
|
||||
updateGrain,
|
||||
removeGrain
|
||||
}}>
|
||||
{children}
|
||||
</GrainAPIcontext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useGrainAPI = () => useContext(GrainAPIcontext);
|
||||
|
|
@ -38,6 +38,7 @@ import JohnDeereProvider, { useJohnDeereProxyAPI } from "./johnDeereProxyAPI";
|
|||
import LibraCartProvider, { useLibraCartProxyAPI } from "./libracartProxyAPI";
|
||||
import CNHiProvider, { useCNHiProxyAPI } from "./cnhiProxyAPI";
|
||||
import DevicePresetProvider, { useDevicePresetAPI } from "./devicePresetAPI";
|
||||
import GrainProvider, { useGrainAPI } from "./grainAPI";
|
||||
// import NoteProvider from "providers/noteAPI";
|
||||
|
||||
export const pondURL = (partial: string, demo: boolean = false): string => {
|
||||
|
|
@ -93,7 +94,9 @@ export default function PondProvider(props: PropsWithChildren<any>) {
|
|||
<KeyManagerProvider>
|
||||
<DevicePresetProvider>
|
||||
<ImagekitProvider>
|
||||
<GrainProvider>
|
||||
{children}
|
||||
</GrainProvider>
|
||||
</ImagekitProvider>
|
||||
</DevicePresetProvider>
|
||||
</KeyManagerProvider>
|
||||
|
|
@ -173,5 +176,6 @@ export {
|
|||
useJohnDeereProxyAPI,
|
||||
useCNHiProxyAPI,
|
||||
useLibraCartProxyAPI,
|
||||
useDevicePresetAPI
|
||||
useDevicePresetAPI,
|
||||
useGrainAPI
|
||||
};
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ export default function UserMenu() {
|
|||
);
|
||||
};
|
||||
|
||||
if (user.id().length < 1) return unauthenticatedUserMenu()
|
||||
if (user.id()?.length < 1) return unauthenticatedUserMenu()
|
||||
|
||||
const hasAdmin = user.hasFeature ? user.hasFeature("admin") : false;
|
||||
|
||||
|
|
@ -182,7 +182,7 @@ export default function UserMenu() {
|
|||
<UserTeamName user={user} team={team} />
|
||||
</Box>
|
||||
<Box style={{ marginLeft: theme.spacing(1) }}>
|
||||
{picture && hasTeams && team.id().length > 0 ? (
|
||||
{picture && hasTeams && team.id()?.length > 0 ? (
|
||||
<div>
|
||||
<UserAvatar
|
||||
user={user}
|
||||
|
|
@ -238,7 +238,7 @@ export default function UserMenu() {
|
|||
{hasTeams && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
if (as.length > 1) {
|
||||
if (as?.length > 1) {
|
||||
user.settings.useTeam = false;
|
||||
dispatch({ key: "as", value: "" });
|
||||
userAPI.updateUser(user.id(), user.protobuf()).then(_resp => {
|
||||
|
|
@ -252,13 +252,13 @@ export default function UserMenu() {
|
|||
});
|
||||
}
|
||||
}}
|
||||
disabled={user.settings.defaultTeam.length < 1}
|
||||
disabled={user.settings.defaultTeam?.length < 1}
|
||||
aria-label="Open Team Menu"
|
||||
dense>
|
||||
<ListItemIcon>
|
||||
<Checkbox
|
||||
disabled={user.settings.defaultTeam.length < 1}
|
||||
checked={as.length > 1}
|
||||
disabled={user.settings.defaultTeam?.length < 1}
|
||||
checked={as?.length > 1}
|
||||
style={{ margin: 0, padding: 0 }}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue