Merge branch 'staging_environment'

This commit is contained in:
Carter 2026-03-10 15:13:17 -06:00
commit 596599cd2b
36 changed files with 1195 additions and 337 deletions

4
package-lock.json generated
View file

@ -43,7 +43,7 @@
"mui-tel-input": "^7.0.0",
"notistack": "^3.0.1",
"openweathermap-ts": "^1.2.10",
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#master",
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging",
"query-string": "^9.2.1",
"react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1",
@ -11967,7 +11967,7 @@
},
"node_modules/protobuf-ts": {
"version": "1.0.0",
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#ab9e8d4fd38f9af7802398403d38fddda5eb01da",
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#1c00e059fe16126e85c50581786b7510759920d7",
"dependencies": {
"protobufjs": "^6.8.8"
}

View file

@ -55,7 +55,7 @@
"mui-tel-input": "^7.0.0",
"notistack": "^3.0.1",
"openweathermap-ts": "^1.2.10",
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#master",
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging",
"query-string": "^9.2.1",
"react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1",

View file

@ -443,14 +443,24 @@ export default function BinCard(props: Props) {
" L"
);
}
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && bin.bushelsPerTonne() > 1) {
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE && bin.bushelsPerTonne() > 1) {
return (
bin.grainTonnes().toLocaleString() +
bin.grainInventory().toLocaleString() +
" mT " +
bin.fillPercent() +
"%"
);
} else if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON && bin.bushelsPerTonne() > 1) {
return (
bin.grainInventory().toLocaleString() +
" t " +
bin.fillPercent() +
"%"
);
}
return (
current.toLocaleString() +
"/" +

View file

@ -0,0 +1,168 @@
import { Component } from "models"
import React, { useEffect, useState } from "react"
import { makeStyles } from "@mui/styles";
import { Box, Button, DialogActions, DialogContent, DialogTitle, Typography } from "@mui/material";
import AerationFanIcon from "products/AgIcons/AerationFanIcon";
import { quack } from "protobuf-ts/quack";
import { useComponentAPI, useMobile, useSnackbar } from "hooks";
import ButtonGroup from "common/ButtonGroup";
import ObjectHeaterIcon from "products/Construction/ObjectHeaterIcon";
import ResponsiveDialog from "common/ResponsiveDialog";
interface Props {
deviceID: number
component: Component
currentState?: boolean
}
const useStyles = makeStyles(() => {
return ({
controllerDisplay: {
display: "flex",
justifyContent: "space-between",
paddingLeft: 5,
paddingRight: 5
},
})
})
export default function BinControllerDisplay(props: Props) {
const {component, deviceID, currentState} = props
const [controllerState, setControllerState] = useState(0)
const [openDialog, setOpenDialog] = useState(false)
const [newOutputState, setNewOutputState] = useState(0)
const {openSnack} = useSnackbar()
const componentAPI = useComponentAPI()
const isMobile = useMobile()
const classes = useStyles()
useEffect(()=>{
setControllerState(component.settings.defaultOutputState)
},[component])
const controllerIcon = () => {
if(component.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER){
return <ObjectHeaterIcon />
}else{
return <AerationFanIcon />
}
}
const setController = () => {
let newSettings = component.settings
newSettings.defaultOutputState = newOutputState
componentAPI.update(deviceID, newSettings).then(resp => {
openSnack("Updated controllers output state")
setControllerState(newOutputState)
}).catch(err => {
openSnack("There was a problem updating the controller")
}).finally(() => {
setOpenDialog(false)
})
}
const controllerDialog = () => {
return (
<ResponsiveDialog open={openDialog} onClose={() => {setOpenDialog(false)}}>
<DialogTitle>Set Controller State</DialogTitle>
<DialogContent>
This Action will change the output state of the controller
</DialogContent>
<DialogActions>
<Button onClick={()=>{setOpenDialog(false)}}>Cancel</Button>
<Button onClick={()=>{setController()}} variant="contained" color="primary">Confirm</Button>
</DialogActions>
</ResponsiveDialog>
)
}
const componentOn = () => {
if(currentState){
return currentState
}else{
let state = false
component.status.lastGoodMeasurement.forEach(um => {
if(um.type === quack.MeasurementType.MEASUREMENT_TYPE_BOOLEAN){
if(um.values.length > 0){
let readings = um.values
if(readings[readings.length-1].values.length > 0){
let nodes = readings[readings.length-1].values
if (nodes[nodes.length -1] === 1){
state = true
}
}
}
}
})
return state
}
}
return (
<React.Fragment>
{controllerDialog()}
<Box className={classes.controllerDisplay} key={component.key()} marginTop={1} marginBottom={1}>
<Box display="flex" marginY="auto">
<Box margin="auto">
{controllerIcon()}
</Box>
<Typography
align="center"
style={{
marginBottom: "auto",
marginTop: "auto",
paddingLeft: 5,
fontSize: isMobile ? "0.85rem" : "1.0rem",
fontWeight: 650
}}>
{component.name()}
</Typography>
</Box>
<Box display="flex">
<ButtonGroup
buttons={[
{
title: "Auto",
function: () => {
setNewOutputState(0)
setOpenDialog(true)
}
},
{
title: "On",
function: () => {
setNewOutputState(1)
setOpenDialog(true)
}
},
{
title: "Off",
function: () => {
setNewOutputState(2)
setOpenDialog(true)
}
}
]}
toggledButtons={[controllerState]}
toggle
/>
<Typography
style={{
minWidth: 30,
marginLeft: "10px",
marginBottom: "auto",
marginTop: "auto",
fontSize: isMobile ? "0.85rem" : "1.0rem",
fontWeight: 650,
color: componentOn() ? "green" : "orange"
}}>
{componentOn() ? "ON" : "OFF"}
</Typography>
</Box>
</Box>
</React.Fragment>
)
}

View file

@ -126,14 +126,13 @@ interface BinFormExtension {
hopperHeight: string;
diameter: string;
grainBushels: string;
grainTonnes: string;
grainWeight: string;
grainType: Option | null;
grainUse: Option | null;
grainSubtype: string;
customTypeName: string;
highTemp: number;
lowTemp: number;
bushelsPerTonne: string;
tempTarget: number;
}
@ -160,14 +159,13 @@ export default function BinSettings(props: Props) {
hopperHeight: "",
diameter: "",
grainBushels: "",
grainTonnes: "",
grainWeight: "",
grainType: null,
grainUse: null,
grainSubtype: "",
customTypeName: "",
highTemp: 20,
lowTemp: 10,
bushelsPerTonne: "",
tempTarget: 15
});
const [initialized, setInitialized] = useState(false);
@ -194,6 +192,7 @@ export default function BinSettings(props: Props) {
const [isCustomBin, setIsCustomBin] = useState(false);
const [binModelOps, setBinModelOptions] = useState<Option[]>([]);
const [selectedBinModel, setSelectedBinModel] = useState<Option | null>(null);
const [bushelConversion, setBushelConversion] = useState(0) //the number used to convert the bushels to either tonne or ton based on user preference
const [modelID, setModelID] = useState(0);
const [autoTopNode, setAutoTopNode] = useState(false);
const [inventoryControl, setInventoryControl] = useState<pond.BinInventoryControl>(
@ -232,14 +231,27 @@ export default function BinSettings(props: Props) {
if (initForm.inventory) {
setInventoryControl(initForm.inventory.inventoryControl);
//make sure bushels per tonne is set so bins that don't have it in the settings yet can still do it before they get updated properly
if (!initForm.inventory.bushelsPerTonne) {
initForm.inventory.bushelsPerTonne = GrainDescriber(
//make sure bushels per ton/ne is set so bins that don't have it in the settings yet can still do it before they get updated properly
const grain = GrainDescriber(
initForm.inventory.grainType
).bushelsPerTonne;
)
if (!initForm.inventory.bushelsPerTonne) {
initForm.inventory.bushelsPerTonne = grain.bushelsPerTonne;
}
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE){
setBushelConversion(initForm.inventory.bushelsPerTonne)
}else if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
setBushelConversion(initForm.inventory.bushelsPerTonne * 0.907)
}
if (initForm.inventory.customGrain){
setCustomGrain(initForm.inventory.customGrain)
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE){
setBushelConversion(initForm.inventory.customGrain.bushelsPerTonne)
}else if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
setBushelConversion(initForm.inventory.customGrain.bushelsPerTonne * 0.907)
}
}
//if the target temp is not set (older bins) make it the midpoint of the high and low temps assuming they are set otherwise make it 15
if (!initForm.inventory.targetTemperature || initForm.inventory.targetTemperature) {
@ -290,6 +302,21 @@ export default function BinSettings(props: Props) {
? initForm.inventory.grainBushels * 35.239
: 0
: initForm.inventory?.grainBushels ?? 0;
let weight = ""
if(initForm.inventory){
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE){
weight = (gb/initForm.inventory.bushelsPerTonne).toFixed(2)
}else if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
weight = (gb/(initForm.inventory.bushelsPerTonne * 0.907)).toFixed(2)
}
if(initForm.inventory.customGrain){
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE){
weight = (gb/initForm.inventory.customGrain.bushelsPerTonne).toFixed(2)
}else if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
weight = (gb/(initForm.inventory.customGrain.bushelsPerTonne * 0.907)).toFixed(2)
}
}
}
setForm(initForm);
setHighTempC(initForm.highTemp ?? 20);
@ -299,9 +326,10 @@ export default function BinSettings(props: Props) {
height: h,
diameter: d,
grainBushels: gb.toFixed(2),
grainTonnes: initForm.inventory?.bushelsPerTonne
? (gb / initForm.inventory.bushelsPerTonne).toFixed(2)
: "",
grainWeight: weight,
// grainTons: initForm.inventory?.bushelsPerTon
// ? (gb / initForm.inventory.bushelsPerTon).toFixed(2)
// : "",
grainType: initForm.inventory?.grainType
? ToGrainOption(initForm.inventory.grainType)
: null,
@ -315,8 +343,7 @@ export default function BinSettings(props: Props) {
tempTarget: target,
hopperHeight: hopperHeight,
topConeHeight: topconeHeight,
sidewallHeight: sidewallHeight,
bushelsPerTonne: initForm.inventory?.bushelsPerTonne.toString() ?? "0"
sidewallHeight: sidewallHeight
});
setInMoistureString(initForm.inventory?.initialMoisture.toString() ?? "0");
setTMoistureString(initForm.inventory?.targetMoisture.toString() ?? "0");
@ -771,12 +798,103 @@ export default function BinSettings(props: Props) {
}
};
const inventoryAmount = () => {
const grainWeight = formExtension.grainWeight;
const binQuantity = formExtension.grainBushels;
//as long as the storage type is not fertilizer it is some sort of grain, whether it is supported or custom is not important in this instance so we do not need to check for unknown
if(storageType !== pond.BinStorage.BIN_STORAGE_FERTILIZER && ((getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE || getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON) && bushelConversion !== 0 && grainWeight !== "")){
return (
<TextField
label="Amount"
value={grainWeight}
type="number"
onChange={event => {
let newWeight = event.target.value;
//get the bushel amount
if (!isNaN(+newWeight)) {
let newBushels = Math.round(+newWeight * bushelConversion * 100) / 100;
updateForm(
"inventory",
pond.BinInventory.create({
...form.inventory,
grainBushels: newBushels
})
);
//updateFormExtension("grainBushels", newBushels.toString())
updateFormExtension("grainWeight", newWeight);
}
}}
fullWidth
variant="outlined"
disabled={!canEdit}
InputProps={{
endAdornment: <InputAdornment position="end">{getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE ? "mT" : "t"}</InputAdornment>
}}
className={classes.bottomSpacing}
/>
)
}else{
return (
<TextField
label="Amount"
value={binQuantity}
type="number"
onChange={event => {
let capacity =
bin?.settings.storage === pond.BinStorage.BIN_STORAGE_FERTILIZER
? +inputCapacity * 35.239
: inputCapacity;
let value = event?.target.value;
let v =
value === ""
? 0
: isNaN(parseFloat(value))
? 0
: parseFloat(value) >= +capacity
? +capacity
: parseFloat(value) <= 0
? 0
: parseFloat(value);
if (bin?.storage() === pond.BinStorage.BIN_STORAGE_FERTILIZER && v) {
v = v / 35.239;
}
//setNewGrainQuantity(v);
updateForm(
"inventory",
pond.BinInventory.create({
...form.inventory,
grainBushels: v
})
);
updateFormExtension("grainBushels", value);
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{storageType === pond.BinStorage.BIN_STORAGE_FERTILIZER
? "Litres"
: "Bushels"}
</InputAdornment>
)
}}
fullWidth
variant="outlined"
disabled={
!canEdit ||
inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC
}
className={classes.bottomSpacing}
/>
)
}
}
const inventoryForm = () => {
const inventory = form.inventory;
const empty = inventory ? inventory.empty : false;
const bushPerTonne = formExtension.bushelsPerTonne;
const binQuantity = formExtension.grainBushels;
const grainTonnes = formExtension.grainTonnes;
const grainType = formExtension.grainType;
const grainSubtype = formExtension.grainSubtype;
const grainUse = formExtension.grainUse;
@ -805,11 +923,11 @@ export default function BinSettings(props: Props) {
pond.BinInventory.create({
...form.inventory,
grainType: pond.Grain.GRAIN_CUSTOM,
bushelsPerTonne: 0
bushelsPerTonne: 0,
})
);
updateFormExtension("grainType", null);
updateFormExtension("bushelsPerTonne", "");
setBushelConversion(0)
} else {
setStorageType(pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN);
updateForm(
@ -859,7 +977,8 @@ export default function BinSettings(props: Props) {
let storageType = parseFloat(value) as pond.BinStorage;
setStorageType(storageType);
if (storageType === pond.BinStorage.BIN_STORAGE_FERTILIZER) {
formExtension.bushelsPerTonne = "";
// formExtension.bushelsPerTonne = "";
setBushelConversion(0)
formExtension.grainBushels = (
parseFloat(formExtension.grainBushels) * 35.239
).toFixed(0);
@ -1054,7 +1173,29 @@ export default function BinSettings(props: Props) {
</React.Fragment>
:
<Box sx={{border: "1px solid white", borderRadius: 2, padding: 2, marginBottom: 2}}>
<CustomGrainSelector initialGrain={customGrain} onGrainSettingsChange={(newGrainSettings) => {setCustomGrain(newGrainSettings)}}/>
<CustomGrainSelector
initialGrain={customGrain}
onGrainSettingsChange={(newGrainSettings) => {
setCustomGrain(newGrainSettings)
let conversion = 0
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE){
conversion = newGrainSettings.bushelsPerTonne
}else if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
conversion = newGrainSettings.bushelsPerTonne * 0.907
}
updateForm(
"inventory",
pond.BinInventory.create({
...form.inventory,
bushelsPerTonne: newGrainSettings.bushelsPerTonne,
})
);
setBushelConversion(conversion)
if(conversion !== 0 && !isNaN(+binQuantity)){
updateFormExtension("grainWeight", (+binQuantity/conversion).toFixed(2))
}
}}/>
</Box>
}
</React.Fragment>
@ -1073,11 +1214,21 @@ export default function BinSettings(props: Props) {
pond.BinInventory.create({
...form.inventory,
grainType: newGrainType,
bushelsPerTonne: GrainDescriber(newGrainType).bushelsPerTonne
bushelsPerTonne: GrainDescriber(newGrainType).bushelsPerTonne,
})
);
//doesn't need to update the form extension to display the bushels per tonne as it is not changeable for the supported grain
//and is reset whenever the user switches between custom or not
let conversion = 0
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE){
conversion = GrainDescriber(newGrainType).bushelsPerTonne
}else if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
conversion = GrainDescriber(newGrainType).bushelsPerTon
}
setBushelConversion(conversion)
if(conversion !== 0 && !isNaN(+binQuantity)){
//updateFormExtension("grainWeight", (+binQuantity/conversion).toFixed(2))
let newWeight = (+binQuantity/conversion).toFixed(2)
formExtension.grainWeight = newWeight
}
updateFormExtension("grainType", option);
}}
group
@ -1106,7 +1257,8 @@ export default function BinSettings(props: Props) {
/>
</React.Fragment>
}
{getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_BUSHELS ||
{inventoryAmount()}
{/* {getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_BUSHELS ||
storageType === pond.BinStorage.BIN_STORAGE_FERTILIZER ||
formExtension.bushelsPerTonne === "0" ||
formExtension.bushelsPerTonne === "" ? (
@ -1163,7 +1315,7 @@ export default function BinSettings(props: Props) {
) : (
<TextField
label="Amount"
value={grainTonnes}
value={grainWeight}
type="number"
onChange={event => {
let newWeight = event.target.value;
@ -1180,7 +1332,7 @@ export default function BinSettings(props: Props) {
})
);
//updateFormExtension("grainBushels", newBushels.toString())
updateFormExtension("grainTonnes", newWeight);
updateFormExtension("grainWeight", newWeight);
}
}}
fullWidth
@ -1191,7 +1343,7 @@ export default function BinSettings(props: Props) {
}}
className={classes.bottomSpacing}
/>
)}
)} */}
{!isCustomInventory && (
<SearchSelect
label="Use"

View file

@ -196,14 +196,27 @@ export default function BinTransactions(props: Props){
}
}
const grainQuantityDisplay = (bushels: number) => {
const grainTransaction = selectedTransaction?.transaction.transaction?.grainTransaction ?? pond.GrainTransaction.create()
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE && grainTransaction.bushelsPerTonne > 1){
let tonneWeight = bushels / grainTransaction.bushelsPerTonne
return tonneWeight + " mT"
}
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON && grainTransaction.bushelsPerTonne > 1){
let tonneWeight = bushels / (grainTransaction.bushelsPerTonne * 0.907)
return tonneWeight + " t"
}
return bushels + " bushels"
}
// dialog for approving a transaction
const approvalDialog = () => {
// get the display information for the transaction
const grainTransaction = selectedTransaction?.transaction.transaction?.grainTransaction ?? pond.GrainTransaction.create()
const bushels = grainTransaction.bushels ?? 0
const weight = bushels / grainTransaction.bushelsPerTonne
//const weight = bushels / grainTransaction.bushelsPerTonne
let graintype = grainTransaction.grainType ?? pond.Grain.GRAIN_INVALID
const grainDisplay = graintype === pond.Grain.GRAIN_CUSTOM
const grainTypeDisplay = graintype === pond.Grain.GRAIN_CUSTOM
? grainTransaction.customGrain
: GrainDescriber(grainTransaction.grainType).name
@ -215,7 +228,7 @@ export default function BinTransactions(props: Props){
<DialogTitle>Accept Transaction</DialogTitle>
<DialogContent>
<Box>
<Typography>{getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT ? weight + " mT" : bushels + " bushels"} of {grainTransaction.subtype} {grainDisplay} moved</Typography>
<Typography>{grainQuantityDisplay(bushels)} of {grainTransaction.subtype} {grainTypeDisplay} moved</Typography>
<Typography margin={1} fontWeight={650}>From:</Typography>
{/*Object type selection*/}
<Autocomplete
@ -378,9 +391,9 @@ export default function BinTransactions(props: Props){
// get the display information for the transaction
const grainTransaction = t.transaction.transaction?.grainTransaction ?? pond.GrainTransaction.create()
const bushels = grainTransaction.bushels ?? 0
const weight = bushels / grainTransaction.bushelsPerTonne
// const weight = bushels / grainTransaction.bushelsPerTonne
let graintype = grainTransaction.grainType ?? pond.Grain.GRAIN_INVALID
const grainDisplay = graintype === pond.Grain.GRAIN_CUSTOM
const grainTypeDisplay = graintype === pond.Grain.GRAIN_CUSTOM
? grainTransaction.customGrain
: GrainDescriber(grainTransaction.grainType).name
return (
@ -394,7 +407,7 @@ export default function BinTransactions(props: Props){
</Box>
</Box>
<Box display="flex" justifyContent="space-between">
<Typography sx={{margin: "auto", marginLeft: 0}}>{getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT ? weight + " mT" : bushels + " bushels"} of {grainTransaction.subtype} {grainDisplay} moved</Typography>
<Typography sx={{margin: "auto", marginLeft: 0}}>{grainQuantityDisplay(bushels)} of {grainTransaction.subtype} {grainTypeDisplay} moved</Typography>
<Button
variant="contained"
disabled={selectedMatch?.transaction.key === t.transaction.key}

View file

@ -31,7 +31,7 @@ import { round } from "lodash";
import { Bin, Component, Device } from "models";
import { GetComponentIcon } from "pbHelpers/ComponentType";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { useMobile } from "hooks";
import { useComponentAPI, useMobile } from "hooks";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
import { useGlobalState, useSnackbar } from "providers";
@ -71,6 +71,7 @@ import { makeStyles } from "@mui/styles";
import ButtonGroup from "common/ButtonGroup";
import ModeChangeDialog from "./conditioning/modeChangeDialog";
import CustomGrainSelector from "grain/CustomGrainSelector";
import BinControllerDisplay from "./BinControllerDisplay";
const useStyles = makeStyles((theme: Theme) => {
return ({
@ -180,7 +181,7 @@ interface Props {
bin: Bin;
loading: boolean;
components: Map<string, Component>;
componentDevices?: Map<string, number>;
componentDevices: Map<string, number>;
devices: Device[];
plenums?: Plenum[];
ambient?: Ambient;
@ -270,7 +271,7 @@ export default function BinVisualizer(props: Props) {
const [storageType, setStorageType] = useState<pond.BinStorage>(
pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN
);
const [bushPerTonne, setBushPerTonne] = useState("0");
//const [bushPerTonne, setBushPerTonne] = useState("0");
const [grainSubtype, setGrainSubtype] = useState("");
const [loadingTrend, setLoadingTrend] = useState(false);
@ -289,6 +290,12 @@ export default function BinVisualizer(props: Props) {
const [modeChangeInProgress, setModeChangeInProgress] = useState(false)
const [newGrainSettings, setNewGrainSettings] = useState<pond.GrainSettings>()
const componentAPI = useComponentAPI()
const [openControllerDIalog, setOpenControllerDialog] = useState(false)
const [controllerOutputStates, setControllerOutputStates] = useState<Map<string, number>>(new Map<string, number>())
const [selectedController, setSelectedController] = useState<Component>()
const [newOutputState, setNewOutputState] = useState()
useEffect(() => {
setModeTime(moment(bin.status.lastModeChange));
}, [bin.status.lastModeChange]);
@ -326,7 +333,7 @@ export default function BinVisualizer(props: Props) {
setCustomTypeName(bin.settings.inventory.customTypeName);
setGrainType(bin.settings.inventory.grainType);
setGrainOption(ToGrainOption(bin.settings.inventory.grainType));
setBushPerTonne(bin.settings.inventory.bushelsPerTonne.toFixed(2));
//setBushPerTonne(bin.settings.inventory.bushelsPerTonne.toFixed(2));
setGrainSubtype(bin.subtype());
setNewBinMode(bin.settings.mode);
setNewGrainSettings(bin.customGrain());
@ -495,8 +502,10 @@ export default function BinVisualizer(props: Props) {
if (bin.storage() === pond.BinStorage.BIN_STORAGE_FERTILIZER) {
return Math.round(val * 35.239 * 100) / 100;
} else {
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && bin.bushelsPerTonne() > 1) {
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE && bin.bushelsPerTonne() > 1) {
return Math.round((val / bin.bushelsPerTonne()) * 100) / 100;
} else if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON && bin.bushelsPerTonne() > 1) {
return Math.round((val / (bin.bushelsPerTonne()*0.907)) * 100) / 100;
}
}
return val;
@ -511,17 +520,19 @@ export default function BinVisualizer(props: Props) {
if (bin.storage() === pond.BinStorage.BIN_STORAGE_FERTILIZER) {
return " / " + capacity.toLocaleString() + " L";
}
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && bin.bushelsPerTonne() > 1) {
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE && bin.bushelsPerTonne() > 1) {
return "mT (" + bin.fillPercent() + "%)";
} else if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON && bin.bushelsPerTonne() > 1) {
return "t (" + bin.fillPercent() + "%)";
} else {
return " / " + capacity.toLocaleString() + " bu";
}
};
const grainErrorCheck = () => {
if (isNaN(parseFloat(bushPerTonne))) return true;
return false;
};
// const grainErrorCheck = () => {
// if (isNaN(parseFloat(bushPerTonne))) return true;
// return false;
// };
const inventoryOverview = () => {
const capacity = bin.settings.specs?.bushelCapacity ?? 0;
@ -774,7 +785,7 @@ export default function BinVisualizer(props: Props) {
setCustomTypeName(bin.settings.inventory.customTypeName);
setGrainType(bin.settings.inventory.grainType);
setGrainOption(ToGrainOption(bin.settings.inventory.grainType));
setBushPerTonne(bin.settings.inventory.bushelsPerTonne.toFixed(2));
//setBushPerTonne(bin.settings.inventory.bushelsPerTonne.toFixed(2));
setNewGrainSettings(bin.customGrain())
setGrainSubtype(bin.subtype());
}
@ -802,7 +813,7 @@ export default function BinVisualizer(props: Props) {
checked={isCustomInventory}
onChange={(_, checked) => {
setIsCustomInventory(checked);
setBushPerTonne("0");
//setBushPerTonne("0");
setGrainSubtype("");
setCustomTypeName("");
setGrainOption(ToGrainOption(pond.Grain.GRAIN_NONE));
@ -834,7 +845,7 @@ export default function BinVisualizer(props: Props) {
: pond.Grain.GRAIN_INVALID;
setGrainOption(ToGrainOption(newGrainType));
setGrainType(newGrainType);
setBushPerTonne(GrainDescriber(newGrainType).bushelsPerTonne.toFixed(2));
//setBushPerTonne(GrainDescriber(newGrainType).bushelsPerTonne.toFixed(2));
}}
group
disabled={!canEdit}
@ -897,7 +908,7 @@ export default function BinVisualizer(props: Props) {
<Button
variant="contained"
color="primary"
disabled={grainErrorCheck()}
// disabled={grainErrorCheck()}
onClick={() => {
updateBin();
setGrainChangeDialog(false);
@ -1321,13 +1332,20 @@ export default function BinVisualizer(props: Props) {
}
}
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && bin.bushelsPerTonne() > 1) {
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE && bin.bushelsPerTonne() > 1) {
diffDisplay = diffDisplay / bin.bushelsPerTonne();
if (pendingDisplay) {
pendingDisplay = pendingDisplay / bin.bushelsPerTonne();
}
}
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON && bin.bushelsPerTonne() > 1) {
diffDisplay = diffDisplay / (bin.bushelsPerTonne()*0.907);
if (pendingDisplay) {
pendingDisplay = pendingDisplay / (bin.bushelsPerTonne()*0.907);
}
}
return (
<Box display="flex" width={1} justifyContent="flex-end">
<FullScreen handle={fullScreenHandler}>
@ -1533,54 +1551,24 @@ export default function BinVisualizer(props: Props) {
Controllers
</Typography>
<Box className={classes.displayBox}>
{bin.status.fans.map(fan => (
<Box className={classes.controllerDisplay} key={fan.key}>
<Box display="flex">
<AerationFanIcon />
<Typography
align="center"
style={{
paddingLeft: 5,
fontSize: isMobile ? "0.85rem" : "1.0rem",
fontWeight: 650
}}>
{fan.name}
</Typography>
</Box>
<Typography
style={{
fontSize: isMobile ? "0.85rem" : "1.0rem",
fontWeight: 650,
color: fan.state ? "green" : "orange"
}}>
{fan.state ? "ON" : "OFF"}
</Typography>
</Box>
))}
{bin.status.heaters.map(heater => (
<Box className={classes.controllerDisplay} key={heater.key}>
<Box display="flex">
<ObjectHeaterIcon />
<Typography
align="center"
style={{
paddingLeft: 5,
fontSize: isMobile ? "0.85rem" : "1.0rem",
fontWeight: 650
}}>
{heater.name}
</Typography>
</Box>
<Typography
style={{
fontSize: isMobile ? "0.85rem" : "1.0rem",
fontWeight: 650,
color: heater.state ? "green" : "orange"
}}>
{heater.state ? "ON" : "OFF"}
</Typography>
</Box>
))}
{bin.status.fans.map(fan => {
let fanComp = components.get(fan.key)
let device = componentDevices.get(fan.key)
if(fanComp && device){
return (
<BinControllerDisplay key={fanComp.key()} component={fanComp} deviceID={device} currentState/>
)
}
})}
{bin.status.heaters.map(heater => {
let heaterComp = components.get(heater.key)
let device = componentDevices.get(heater.key)
if(heaterComp && device){
return (
<BinControllerDisplay key={heaterComp.key()} component={heaterComp} deviceID={device} currentState={heater.state}/>
)
}
})}
</Box>
</Box>
);
@ -1987,9 +1975,10 @@ export default function BinVisualizer(props: Props) {
//update all the grain stuff
b.settings.inventory.grainType = grainType;
b.settings.inventory.customTypeName = customTypeName;
b.settings.inventory.bushelsPerTonne = isNaN(parseFloat(bushPerTonne))
? 0
: parseFloat(bushPerTonne);
b.settings.inventory.bushelsPerTonne = GrainDescriber(grainType).bushelsPerTonne
// b.settings.inventory.bushelsPerTonne = isNaN(parseFloat(bushPerTonne))
// ? 0
// : parseFloat(bushPerTonne);
b.settings.inventory.grainSubtype = grainSubtype;
b.settings.inventory.customGrain = newGrainSettings
}
@ -2010,7 +1999,6 @@ export default function BinVisualizer(props: Props) {
}
b.settings.mode = newBinMode;
}
console.log(b.settings)
binAPI.updateBin(bin.key(), b.settings, as).then(resp => {
refresh();
});

View file

@ -652,9 +652,47 @@ export default function BinyardDisplay(props: Props) {
);
};
const grainUnitDisplay = (custom?: pond.CustomInventory) => {
// If custom and not convertible, always bushels
if (custom && custom.bushelsPerTonne <= 1) {
return " bu";
}
switch (getGrainUnit()) {
case pond.GrainUnit.GRAIN_UNIT_TONNE:
return " mT";
case pond.GrainUnit.GRAIN_UNIT_TON:
return " t";
default:
return " bu";
}
};
const customQuantityDisplay = (customInventory: pond.CustomInventory, bushels: number) => {
let amount = bushels
if(customInventory.bushelsPerTonne > 1){
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE){
amount = bushels/customInventory.bushelsPerTonne
}else if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
amount = bushels/(customInventory.bushelsPerTonne*0.907)
}
}
return Math.round(amount*100)/100
}
const supportedGrainDisplay = (grain: pond.Grain, bushels: number) => {
const describer = GrainDescriber(grain)
let amount = bushels
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE){
amount = bushels/describer.bushelsPerTonne
}else if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
amount = bushels/describer.bushelsPerTon
}
return Math.round(amount*100)/100
}
const binUtilizationList = () => {
const hasInventory = binMetrics && binMetrics.grainInventory.length > 0;
const useWeight = getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT;
return (
<Accordion
className={classes.accordion}
@ -695,17 +733,9 @@ export default function BinyardDisplay(props: Props) {
<GridListTile key={key}>
<BinUtilizationChart
grain={inv.grainType}
customUnit={useWeight ? " mT" : " bu"}
bushelAmount={
useWeight
? inv.bushelAmount / GrainDescriber(inv.grainType).bushelsPerTonne
: inv.bushelAmount
}
bushelCapacity={
useWeight
? inv.bushelCapacity / GrainDescriber(inv.grainType).bushelsPerTonne
: inv.bushelCapacity
}
customUnit={grainUnitDisplay()}
bushelAmount={supportedGrainDisplay(inv.grainType, inv.bushelAmount)}
bushelCapacity={supportedGrainDisplay(inv.grainType, inv.bushelCapacity)}
onClick={() => {
setContentFilter(pond.Grain[inv.grainType]);
loadMoreBins(pond.Grain[inv.grainType]);
@ -724,11 +754,10 @@ export default function BinyardDisplay(props: Props) {
amount = amount * 35.239;
cap = cap * 35.239;
unit = " L";
}
if (useWeight && inv.bushelsPerTonne > 1) {
amount = amount / inv.bushelsPerTonne;
cap = cap / inv.bushelsPerTonne;
unit = " mT";
}else{
amount = customQuantityDisplay(inv, amount)
cap = customQuantityDisplay(inv, cap)
unit = grainUnitDisplay(inv)
}
return (

View file

@ -69,9 +69,12 @@ export default function BinLevelOverTime(props: Props) {
if (fertilizerBin && cap) {
cap = cap * 35.239;
}
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && cap) {
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE && cap) {
cap = cap / bin.bushelsPerTonne();
}
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON && cap) {
cap = cap / (bin.bushelsPerTonne()*0.907);
}
setCapacity(cap);
binAPI
.listHistoryBetween(bin.key(), 500, startDate.toISOString(), endDate.toISOString())
@ -85,13 +88,18 @@ export default function BinLevelOverTime(props: Props) {
if (hist.settings?.inventory) {
let bushels = hist.settings.inventory.grainBushels ?? 0;
if (bushels !== lastBushels) {
let grainDisplay = bushels
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE && bin.bushelsPerTonne() > 1){
grainDisplay = Math.round((bushels / bin.bushelsPerTonne()) * 100) / 100;
}
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON && bin.bushelsPerTonne() > 1){
grainDisplay = Math.round((bushels / (bin.bushelsPerTonne()*0.907)) * 100) / 100;
}
let newData: InventoryAt = {
timestamp: moment(hist.timestamp).valueOf(),
value: fertilizerBin
? Math.round(bushels * 35.239)
: getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT
? Math.round((bushels / bin.bushelsPerTonne()) * 100) / 100
: bushels
: grainDisplay
};
data.push(newData);
lastBushels = bushels;
@ -112,12 +120,17 @@ export default function BinLevelOverTime(props: Props) {
let currentTime = moment().valueOf();
if (data.length === 0) {
let bushels = bin.bushels();
let grainDisplay = bushels
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE && bin.bushelsPerTonne() > 1){
grainDisplay = Math.round((bushels / bin.bushelsPerTonne()) * 100) / 100;
}
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON && bin.bushelsPerTonne() > 1){
grainDisplay = Math.round((bushels / (bin.bushelsPerTonne()*0.907)) * 100) / 100;
}
data.push({
value: fertilizerBin
? Math.round(bushels * 35.239)
: getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT
? Math.round((bushels / bin.bushelsPerTonne()) * 100) / 100
: bushels,
: grainDisplay,
timestamp: currentTime
});
}
@ -143,9 +156,12 @@ export default function BinLevelOverTime(props: Props) {
if (fertilizerBin && cap) {
cap = cap * 35.239;
}
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && cap) {
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE && cap) {
cap = cap / bin.bushelsPerTonne();
}
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON && cap) {
cap = cap / (bin.bushelsPerTonne()*0.907);
}
setCapacity(cap);
let autoBarData: BarData[] = [];
@ -163,8 +179,15 @@ export default function BinLevelOverTime(props: Props) {
m.values.forEach((val, i) => {
if (val.values[0] && m.timestamps[i]) {
if (lastBushels !== val.values[0]) {
let grainDisplay = val.values[0]
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE && bin.bushelsPerTonne() > 1){
grainDisplay = Math.round((grainDisplay / bin.bushelsPerTonne()) * 100) / 100;
}
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON && bin.bushelsPerTonne() > 1){
grainDisplay = Math.round((grainDisplay / (bin.bushelsPerTonne()*0.907)) * 100) / 100;
}
autoBarData.push({
value: val.values[0],
value: grainDisplay,
timestamp: moment(m.timestamps[i]).valueOf(),
});
lastBushels = val.values[0];
@ -175,12 +198,17 @@ export default function BinLevelOverTime(props: Props) {
if (autoBarData.length === 0) {
let bushels = bin.bushels();
let currentTime = moment().valueOf();
let grainDisplay = bushels
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE && bin.bushelsPerTonne() > 1){
grainDisplay = Math.round((bushels / bin.bushelsPerTonne()) * 100) / 100;
}
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON && bin.bushelsPerTonne() > 1){
grainDisplay = Math.round((bushels / (bin.bushelsPerTonne()*0.907)) * 100) / 100;
}
autoBarData.push({
value: fertilizerBin
? Math.round(bushels * 35.239)
: getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT
? Math.round((bushels / bin.bushelsPerTonne()) * 100) / 100
: bushels,
: grainDisplay,
timestamp: currentTime
});
}

View file

@ -49,6 +49,10 @@ interface Props {
* @default undefined
*/
toggledButtons?: number[]
/**
* This can be used to set which button starts toggled by default, and from there it will be controlled internally, toggledButtons will override this
*/
defaultToggle?: number
/**
* Numerical value for the size of the text for buttons that dont use the icon
*/
@ -97,7 +101,7 @@ const useStyles = makeStyles((theme: Theme) => {
})
export default function ButtonGroup(props: Props){
const { buttons, toggle, toggledButtons, multiToggle, textSize, disableAll } = props
const { buttons, toggle, toggledButtons, defaultToggle, multiToggle, textSize, disableAll } = props
const classes = useStyles()
const [currentToggle, setCurrentToggle] = useState<number[]>([])
@ -107,6 +111,8 @@ export default function ButtonGroup(props: Props){
let toggled: number [] = [0]
if(toggledButtons){
toggled = toggledButtons
} else if(defaultToggle){
toggled = [defaultToggle]
}
setCurrentToggle(toggled)
}

View file

@ -92,9 +92,14 @@ export default function ContractSettings(props: Props) {
setContractValue(contract.settings.totalValue.toString());
setCommodity(contract.settings.commodity);
setContractDate(contract.settings.contractDate);
setConversionValue(
contract.settings.conversionValue > 0 ? contract.settings.conversionValue.toString() : "1"
);
let conv = 1
if(contract.settings.conversionValue > 1){
conv = contract.settings.conversionValue
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
conv = conv * 0.907
}
}
setConversionValue(conv.toFixed(2));
setUseCustom(contract.isCustom());
}
}, [contract]);
@ -102,12 +107,21 @@ export default function ContractSettings(props: Props) {
useEffect(() => {
switch (contractType) {
case pond.ContractType.CONTRACT_TYPE_GRAIN:
let conversionLabel = "Bushels Per Tonne"
let adornment = "bu"
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE){
adornment = "mT"
}
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
conversionLabel = "Bushels Per Ton"
adornment = "t"
}
setLabels({
sizeLabel: "Total Grain on Contract",
commodityLabel: "Select Grain Type",
conversionLabel: "Bushels Per Tonne",
conversionLabel: conversionLabel,
deliveryLabel: "Grain Delivered",
adornment: getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT ? "mT" : "bu"
adornment: adornment
});
break;
default:
@ -148,13 +162,22 @@ export default function ContractSettings(props: Props) {
};
const submit = () => {
const conVal = isNaN(parseFloat(conversionValue)) ? 1 : parseFloat(conversionValue);
let conVal = isNaN(parseFloat(conversionValue)) ? 1 : parseFloat(conversionValue);
//the toStoredUnit will convert what they entered for amount and size using the conversion that they entered
//we will assume that both of those entries are using the same unit ie both US ton or both metric tonne
const deliveredVal = isNaN(parseFloat(deliveredAmount))
? 0
: Math.round(Contract.toStoredUnit(parseFloat(deliveredAmount), contractType, conVal));
const sizeVal = isNaN(parseFloat(contractSize))
? 0
: Math.round(Contract.toStoredUnit(parseFloat(contractSize), contractType, conVal));
//then after the conversion to bushels change the conversion value to metric to be stored in the contract
if(contractType === pond.ContractType.CONTRACT_TYPE_GRAIN){
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
conVal = conVal / 0.907 //convert the ton they entered to mT to be stored in the contracts conversion value
}
}
if (contract) {
let settings: pond.ContractSettings = contract.settings;
settings.commodity = commodity;
@ -201,7 +224,6 @@ export default function ContractSettings(props: Props) {
settings.type = contractType;
settings.conversionValue = conVal;
settings.contractDate = contractDate;
contractAPI
.addContract(name, settings, as)
.then(resp => {
@ -490,7 +512,9 @@ export default function ContractSettings(props: Props) {
InputLabelProps={{ shrink: true }}
error={isNaN(parseFloat(conversionValue))}
helperText={isNaN(parseFloat(conversionValue)) ? "Must be a valid number" : ""}
onChange={e => setConversionValue(e.target.value)}
onChange={e => {
setConversionValue(e.target.value)
}}
/>
</React.Fragment>
)}

View file

@ -27,11 +27,17 @@ export default function ContractTransactionGraph(props: Props) {
if (transaction?.grainTransaction) {
value = transaction.grainTransaction.bushels;
if (
getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT &&
getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE &&
transaction.grainTransaction.bushelsPerTonne > 1
) {
value = value / transaction.grainTransaction.bushelsPerTonne;
}
if (
getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON &&
transaction.grainTransaction.bushelsPerTonne > 1
) {
value = value / (transaction.grainTransaction.bushelsPerTonne * 0.907);
}
}
d.push({
timestamp: time.valueOf(),

View file

@ -1,5 +1,5 @@
import { Chip, Grid2 as Grid, Theme, Tooltip, Skeleton } from "@mui/material";
import { DataUsage, SimCard, Launch } from "@mui/icons-material";
import { DataUsage, SimCard, Launch, DeviceHub } from "@mui/icons-material";
import StatusChip from "common/StatusChip";
import DeviceTags from "device/DeviceTags";
import VersionChip from "device/VersionChip";
@ -233,6 +233,17 @@ export default function DeviceOverview(props: Props) {
<StatusChip status="pending" />
</Grid>
)}
{device.settings.needsBusControl && (
<Grid>
<Tooltip title="Device needs the Bus Control component to power the I2C port">
<Chip
variant="outlined"
label={"I2C Bus"}
icon={<DeviceHub />}
/>
</Tooltip>
</Grid>
)}
{<DeviceTags device={device} disableAdd={disableAddTag} />}
</Grid>
);

View file

@ -235,6 +235,12 @@ export default function DeviceSettings(props: Props) {
setDeviceForm(updatedForm);
};
const toggleBusRequired = (event: any) => {
let updatedForm = Device.clone(deviceForm);
updatedForm.settings.needsBusControl = event.target.checked;
setDeviceForm(updatedForm);
};
const toggleSleeps = (event: any) => {
let updatedForm = Device.clone(deviceForm);
let updatedSleeps = event.target.checked;
@ -500,7 +506,8 @@ export default function DeviceSettings(props: Props) {
</Grid>
)}
{user.hasAdmin() && (
<Grid size={{ xs: 12, sm: 12 }}>
<React.Fragment>
<Grid size={{ xs: 12, sm: 6 }}>
<FormControlLabel
control={
<Switch
@ -516,6 +523,23 @@ export default function DeviceSettings(props: Props) {
labelPlacement="end"
/>
</Grid>
<Grid size={{ xs: 12, sm: 6 }}>
<FormControlLabel
control={
<Switch
checked={deviceForm.settings.needsBusControl}
onChange={toggleBusRequired}
name="needsI2CBus"
aria-label="needsI2CBus"
color="secondary"
/>
}
disabled={!canEdit}
label="I2C Bus Required"
labelPlacement="end"
/>
</Grid>
</React.Fragment>
)}
</Grid>
</React.Fragment>

View file

@ -88,6 +88,7 @@ export default function FieldSettings(props: Props) {
settings.grainSubtype = grainSubtype;
settings.landLocation = landLocation;
settings.acres = acres;
settings.bushelsPerTonne = isNaN(parseFloat(bushelsPerTonne)) ? 1 : parseFloat(bushelsPerTonne);
fieldAPI
.updateField(selectedField.key(), settings, undefined, as)
@ -121,6 +122,7 @@ export default function FieldSettings(props: Props) {
geo.shapes = borders;
}
settings.fieldGeoData = geo;
settings.bushelsPerTonne = isNaN(parseFloat(bushelsPerTonne)) ? 1 : parseFloat(bushelsPerTonne);
fieldAPI
.addField(settings, as)
.then(resp => {
@ -226,6 +228,7 @@ export default function FieldSettings(props: Props) {
if (option) {
let grainType = pond.Grain[option.value as keyof typeof pond.Grain];
setCropType(grainType);
console.log("set bpt here")
setBushelsPerTonne(GrainDescriber(grainType).bushelsPerTonne.toString());
}
}}

View file

@ -126,9 +126,24 @@ export default function GateDevice(props: Props) {
}
}, [comprehensiveDevice, gate, linkedCompList, user]);
const gateComponentUpdate = (componentKey: string, gateCompType: pond.GateComponentType) => {
const gateComponentUpdate = (oldKey: string, componentKey: string, gateCompType: pond.GateComponentType) => {
let promises: any[] = []
if (oldKey !== "") {
//the promise for removing the pref from the old component
promises.push(gateAPI
.updatePrefs(
gate.key,
"component",
device.id() + ":" + oldKey,
pond.GateComponentType.GATE_COMPONENT_TYPE_UNKNOWN,
[gate.key],
["gate"],
as
))
}
if (componentKey !== "") {
gateAPI
//the promise for adding the pref to the new component
promises.push(gateAPI
.updatePrefs(
gate.key,
"component",
@ -137,11 +152,17 @@ export default function GateDevice(props: Props) {
[gate.key],
["gate"],
as
)
.then(resp => {
openSnack("Component Prefence Updated");
});
))
}
Promise.all(promises).then(resp => {
openSnack("Component Prefence Updated");
let clone = cloneDeep(gate.preferences)
clone[oldKey] = pond.GateComponentType.GATE_COMPONENT_TYPE_UNKNOWN
clone[componentKey] = gateCompType
gate.preferences = clone
}).catch(
);
};
useEffect(() => {
@ -205,7 +226,7 @@ export default function GateDevice(props: Props) {
style={{ fontSize: 8 }}
onChange={e => {
var key = e.target.value as string;
gateComponentUpdate(key, pond.GateComponentType.GATE_COMPONENT_TYPE_AMBIENT);
gateComponentUpdate(ambientKey, key, pond.GateComponentType.GATE_COMPONENT_TYPE_AMBIENT);
setAmbientKey(key);
if (tempKey === key) {
setTempKey("");
@ -238,6 +259,7 @@ export default function GateDevice(props: Props) {
style={{ fontSize: 8 }}
onChange={e => {
gateComponentUpdate(
tempKey,
e.target.value as string,
pond.GateComponentType.GATE_COMPONENT_TYPE_TEMP
);
@ -273,6 +295,7 @@ export default function GateDevice(props: Props) {
style={{ fontSize: 8 }}
onChange={e => {
gateComponentUpdate(
pressureKey,
e.target.value as string,
pond.GateComponentType.GATE_COMPONENT_TYPE_PRESSURE
);

View file

@ -93,15 +93,14 @@ export default function GateDeviceInteraction(props: Props) {
compDevice.components.forEach(comp => {
let component = Component.any(comp);
//checks the address for the LED components to get the red and green LED's
if (component.locationString() === redAddr && component.subType() === 1) {
setRedComponent(component);
} else if (component.locationString() === greenAddr && component.subType() === 1) {
setGreenComponent(component);
} else if (component.type() === quack.ComponentType.COMPONENT_TYPE_PRESSURE_CABLE) {
if (
} else if (
gate.preferences[component.key()] === pond.GateComponentType.GATE_COMPONENT_TYPE_PRESSURE
) {
console.log(component)
setPressureComponent(component);
setPressureSource(
quack.ComponentID.create({
@ -111,13 +110,13 @@ export default function GateDeviceInteraction(props: Props) {
})
)
}
}
});
if(compDevice.device){
setDevice(Device.create(compDevice.device))
}
}, [gate, densityTemp, user, compDevice]);
}, [gate, densityTemp, user, compDevice, gate.preferences]);
const buttonDisabled = () => {
if (greenComponent === undefined) return true
@ -129,13 +128,13 @@ export default function GateDeviceInteraction(props: Props) {
const createInteractions = async () => {
//the interactions to be made
if (
greenComponent !== undefined &&
redComponent !== undefined &&
pressureComponent !== undefined
) {
//get the number of nodes in the pressure components
let nodes = pressureComponent.numNodes()
let lightInteractions: pond.InteractionSettings[] = []
//making variables for the parameters of the interactions
const redSink = quack.ComponentID.create({
@ -163,19 +162,18 @@ export default function GateDeviceInteraction(props: Props) {
source: pressureSource,
conditions: [
pond.InteractionCondition.create({
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
value: -highDelta,
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
value: highDelta,
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
}),
pond.InteractionCondition.create({
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
value: -lowDelta,
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
value: lowDelta,
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
})
],
nodeOne: 2,
nodeTwo: 1,
subtype: 18,
nodeOne: nodes,
subtype: nodes+1,
schedule: lightSchedule,
result: pond.InteractionResult.create({
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE,
@ -201,14 +199,13 @@ export default function GateDeviceInteraction(props: Props) {
source: pressureSource,
conditions: [
pond.InteractionCondition.create({
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
value: -highDelta,
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
value: highDelta,
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
})
],
nodeOne: 2,
nodeTwo: 1,
subtype: 18,
nodeOne: nodes,
subtype: nodes+1,
schedule: lightSchedule,
result: pond.InteractionResult.create({
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET,
@ -221,19 +218,18 @@ export default function GateDeviceInteraction(props: Props) {
source: pressureSource,
conditions: [
pond.InteractionCondition.create({
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
value: -highDelta,
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
value: highDelta,
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
}),
pond.InteractionCondition.create({
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
value: -lowDelta,
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
value: lowDelta,
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
})
],
nodeOne: 2,
nodeTwo: 1,
subtype: 18,
nodeOne: nodes,
subtype: nodes+1,
schedule: lightSchedule,
result: pond.InteractionResult.create({
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET,
@ -246,19 +242,18 @@ export default function GateDeviceInteraction(props: Props) {
source: pressureSource,
conditions: [
pond.InteractionCondition.create({
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
value: -lowDelta,
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
value: lowDelta,
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
}),
pond.InteractionCondition.create({
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
value: -thresholdPascals,
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
value: thresholdPascals,
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
}),
],
nodeOne: 2,
nodeTwo: 1,
subtype: 18,
nodeOne: nodes,
subtype: nodes+1,
schedule: lightSchedule,
result: pond.InteractionResult.create({
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET,
@ -276,7 +271,8 @@ export default function GateDeviceInteraction(props: Props) {
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
}),
],
subtype: 1,
nodeOne: nodes,
subtype: nodes+1,
schedule: lightSchedule,
result: pond.InteractionResult.create({
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET,
@ -291,19 +287,18 @@ export default function GateDeviceInteraction(props: Props) {
source: pressureSource,
conditions: [
pond.InteractionCondition.create({
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
value: -highDelta,
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
value: highDelta,
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
}),
pond.InteractionCondition.create({
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
value: -lowDelta,
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
value: lowDelta,
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
})
],
nodeOne: 2,
nodeTwo: 1,
subtype: 18,
nodeOne: nodes,
subtype: nodes+1,
schedule: lightSchedule,
result: pond.InteractionResult.create({
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE,
@ -393,7 +388,6 @@ export default function GateDeviceInteraction(props: Props) {
color="primary"
onClick={() => {
//TODO: Once we figure out how to fix the backend to handle rapid addition/removal of interactions
//removeCurrentInteractions();
setAdding(true);
interactionsAPI.clearInteractions(device.id(), [pressureSource]).then(resp => {
createInteractions();
@ -401,7 +395,8 @@ export default function GateDeviceInteraction(props: Props) {
console.error(err)
})
}}
disabled={buttonDisabled()}>
disabled={buttonDisabled()}
>
{adding ? "Adding Interaction" : "Create Interaction"}
</Button>
</DialogActions>

View file

@ -2,6 +2,7 @@ import {MenuItem, TextField } from "@mui/material"
import { cloneDeep } from "lodash"
import { pond } from "protobuf-ts/pond"
import React, { useEffect, useRef, useState } from "react"
import { getGrainUnit } from "utils"
interface Props {
grainSettings?: pond.GrainSettings
@ -18,7 +19,7 @@ export default function CustomGrainForm(props: Props) {
const [constantB, setConstantB] = useState("0")
const [constantC, setConstantC] = useState("0")
const [kgPerBushel, setKgPerBushel] = useState("0")
const [bushelsPerTonne, setBushelsPerTonne] = useState("0")
const [bushelsConversion, setBushelConversion] = useState("0")//this is either per tonne or per US ton depending on user preferences
const valid = useRef(false)
useEffect(()=>{
@ -30,12 +31,19 @@ export default function CustomGrainForm(props: Props) {
setConstantB(grainSettings.b.toString())
setConstantC(grainSettings.c.toString())
setKgPerBushel(grainSettings.kgPerBushel.toString())
setBushelsPerTonne(grainSettings.bushelsPerTonne.toString())
setBushelConversion(grainSettings.bushelsPerTonne.toString())
setNewGrainSettings(grainSettings)
}
},[grainSettings])
const validate = (name: string, group: string, constA: string, constB: string, constC: string, kgPerBushel: string, bushelsPerTonne: string) => {
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
@ -64,7 +72,7 @@ export default function CustomGrainForm(props: Props) {
setName(name)
let settings = cloneDeep(newGrainSettings)
settings.name = name
valid.current = validate(name, group, constantA, constantB, constantC, kgPerBushel, bushelsPerTonne)
valid.current = validate(name, group, constantA, constantB, constantC, kgPerBushel, bushelsConversion)
settingsChanged(settings)
}}/>
<TextField
@ -77,7 +85,7 @@ export default function CustomGrainForm(props: Props) {
setGroup(group)
let settings = cloneDeep(newGrainSettings)
settings.group = group
valid.current = validate(name, group, constantA, constantB, constantC, kgPerBushel, bushelsPerTonne)
valid.current = validate(name, group, constantA, constantB, constantC, kgPerBushel, bushelsConversion)
settingsChanged(settings)
}}/>
<TextField
@ -91,7 +99,7 @@ export default function CustomGrainForm(props: Props) {
setEquation(enumVal)
let settings = cloneDeep(newGrainSettings)
settings.equation = enumVal
valid.current = validate(name, group, constantA, constantB, constantC, kgPerBushel, bushelsPerTonne)
valid.current = validate(name, group, constantA, constantB, constantC, kgPerBushel, bushelsConversion)
settingsChanged(settings)
}}
select>
@ -121,7 +129,7 @@ export default function CustomGrainForm(props: Props) {
onChange={(e) => {
let val = e.target.value
setConstantA(val)
valid.current = validate(name, group, val, constantB, constantC, kgPerBushel, bushelsPerTonne)
valid.current = validate(name, group, val, constantB, constantC, kgPerBushel, bushelsConversion)
let settings = cloneDeep(newGrainSettings)
if(!isNaN(parseFloat(val))){
settings.a = parseFloat(val)
@ -139,7 +147,7 @@ export default function CustomGrainForm(props: Props) {
onChange={(e) => {
let val = e.target.value
setConstantB(val)
valid.current = validate(name, group, constantA, val, constantC, kgPerBushel, bushelsPerTonne)
valid.current = validate(name, group, constantA, val, constantC, kgPerBushel, bushelsConversion)
let settings = cloneDeep(newGrainSettings)
if(!isNaN(parseFloat(val))){
settings.b = parseFloat(val)
@ -157,7 +165,7 @@ export default function CustomGrainForm(props: Props) {
onChange={(e) => {
let val = e.target.value
setConstantC(val)
valid.current = validate(name, group, constantA, constantB, val, kgPerBushel, bushelsPerTonne)
valid.current = validate(name, group, constantA, constantB, val, kgPerBushel, bushelsConversion)
let settings = cloneDeep(newGrainSettings)
if(!isNaN(parseFloat(val))){
settings.c = parseFloat(val)
@ -175,7 +183,7 @@ export default function CustomGrainForm(props: Props) {
onChange={(e) => {
let val = e.target.value
setKgPerBushel(val)
valid.current = validate(name, group, constantA, constantB, constantC, val, bushelsPerTonne)
valid.current = validate(name, group, constantA, constantB, constantC, val, bushelsConversion)
let settings = cloneDeep(newGrainSettings)
if(!isNaN(parseFloat(val))){
settings.kgPerBushel = parseFloat(val)
@ -187,20 +195,24 @@ export default function CustomGrainForm(props: Props) {
sx={{marginBottom: 2}}
fullWidth
type="number"
value={bushelsPerTonne}
error={isNaN(parseFloat(bushelsPerTonne))}
helperText={isNaN(parseFloat(bushelsPerTonne)) ? "Must be a valid number" : ""}
value={bushelsConversion}
error={isNaN(parseFloat(bushelsConversion))}
helperText={isNaN(parseFloat(bushelsConversion)) ? "Must be a valid number" : ""}
onChange={(e) => {
let val = e.target.value
setBushelsPerTonne(val)
setBushelConversion(val)
valid.current = validate(name, group, constantA, constantB, constantC, kgPerBushel, val)
let settings = cloneDeep(newGrainSettings)
if(!isNaN(parseFloat(val))){
settings.bushelsPerTonne = parseFloat(val)
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
settings.bushelsPerTonne = parseFloat(val)/0.907
}else{
settings.bushelsPerTonne = parseFloat(val)
}
}
settingsChanged(settings)
}}
label="Bushels per Tonne"/>
label={"Bushels per " + (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE ? " Tonne" : "Ton")}/>
</React.Fragment>
)
}

View file

@ -30,6 +30,7 @@ export interface GrainExtension {
colour: string;
weightConversionKg: number;
bushelsPerTonne: number;
bushelsPerTon: number; //NOTE: this is US tons (2000lB/t)
}
const defaultSetTemp = 30.0;
@ -45,7 +46,8 @@ const defaultGrain: GrainExtension = {
targetMC: 15.0,
colour: "#424242",
weightConversionKg: 0,
bushelsPerTonne: 0
bushelsPerTonne: 0,
bushelsPerTon: 0
};
export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
@ -64,7 +66,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
targetMC: 15.0,
colour: "yellow",
weightConversionKg: 0,
bushelsPerTonne: 0
bushelsPerTonne: 0,
bushelsPerTon: 0
}
],
[
@ -81,7 +84,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
img: BarleyImg,
colour: "#27632a",
weightConversionKg: 21.772657171369,
bushelsPerTonne: 45.93
bushelsPerTonne: 45.93,
bushelsPerTon: 41.66
}
],
[
@ -98,7 +102,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
targetMC: 16.1,
colour: "#46b298",
weightConversionKg: 1,
bushelsPerTonne: 45.93
bushelsPerTonne: 45.93,
bushelsPerTon: 50.63
}
],
[
@ -116,7 +121,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
// colour: "#cddc39",
colour: "#ffff00",
weightConversionKg: 22.67961461,
bushelsPerTonne: 44.092
bushelsPerTonne: 44.092,
bushelsPerTon: 39.99
}
],
[
@ -133,7 +139,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
img: CanolaImg,
colour: "#cddc39",
weightConversionKg: 27.215537532,
bushelsPerTonne: 44.092
bushelsPerTonne: 44.092,
bushelsPerTon: 39.9
}
],
[
@ -150,7 +157,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
img: CornImg,
colour: "#ffef62",
weightConversionKg: 25.4014333665971,
bushelsPerTonne: 39.368
bushelsPerTonne: 39.368,
bushelsPerTon: 35.71
}
],
[
@ -167,7 +175,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
img: CornImg,
colour: "#ffef62",
weightConversionKg: 25.4014333665971,
bushelsPerTonne: 39.368
bushelsPerTonne: 39.368,
bushelsPerTon: 35.71
}
],
[
@ -184,7 +193,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
img: CornImg,
colour: "#ffef62",
weightConversionKg: 25.4014333665971,
bushelsPerTonne: 39.368
bushelsPerTonne: 39.368,
bushelsPerTon: 35.71
}
],
[
@ -201,7 +211,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
img: CornImg,
colour: "#ffef62",
weightConversionKg: 25.4014333665971,
bushelsPerTonne: 39.368
bushelsPerTonne: 39.368,
bushelsPerTon: 35.71
}
],
[
@ -218,7 +229,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
img: CornImg,
colour: "#ffef62",
weightConversionKg: 25.4014333665971,
bushelsPerTonne: 39.368
bushelsPerTonne: 39.368,
bushelsPerTon: 35.71
}
],
[
@ -235,7 +247,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
img: OatImg,
colour: "#79955a",
weightConversionKg: 15.4222988297197,
bushelsPerTonne: 64.842
bushelsPerTonne: 64.842,
bushelsPerTon: 58.81
}
],
[
@ -252,7 +265,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
img: OatImg,
colour: "#79955a",
weightConversionKg: 15.4222988297197,
bushelsPerTonne: 64.842
bushelsPerTonne: 64.842,
bushelsPerTon: 58.81
}
],
[
@ -269,7 +283,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
img: OatImg,
colour: "#79955a",
weightConversionKg: 15.4222988297197,
bushelsPerTonne: 64.842
bushelsPerTonne: 64.842,
bushelsPerTon: 58.81
}
],
[
@ -286,7 +301,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
img: PeanutImg,
colour: "#b2a058",
weightConversionKg: 23.3124,
bushelsPerTonne: 105.263
bushelsPerTonne: 105.263,
bushelsPerTon: 95.47
}
],
[
@ -303,7 +319,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
img: RiceImg,
colour: "#7c9c99",
weightConversionKg: 28.439,
bushelsPerTonne: 49.02
bushelsPerTonne: 49.02,
bushelsPerTon: 44.46
}
],
[
@ -320,7 +337,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
img: RiceImg,
colour: "#7c9c99",
weightConversionKg: 29.979,
bushelsPerTonne: 49.02
bushelsPerTonne: 49.02,
bushelsPerTon: 44.46
}
],
[
@ -337,7 +355,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
img: RiceImg,
colour: "#7c9c99",
weightConversionKg: 30.744,
bushelsPerTonne: 49.02
bushelsPerTonne: 49.02,
bushelsPerTon: 44.46
}
],
[
@ -354,7 +373,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
img: SorghumImg,
colour: "#829baf",
weightConversionKg: 25.4014333665971,
bushelsPerTonne: 39.368
bushelsPerTonne: 39.368,
bushelsPerTon: 35.71
}
],
[
@ -371,7 +391,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
img: SoybeanImg,
colour: "#7d6d99",
weightConversionKg: 27.2158214642112,
bushelsPerTonne: 36.744
bushelsPerTonne: 36.744,
bushelsPerTon: 33.33
}
],
[
@ -388,7 +409,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
img: SunflowerImg,
colour: "#ffab40",
weightConversionKg: 13.6079107321056,
bushelsPerTonne: 73.487
bushelsPerTonne: 73.487,
bushelsPerTon: 66.65
}
],
[
@ -405,7 +427,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
img: WheatImg,
colour: "#46b298",
weightConversionKg: 27.2158214642112,
bushelsPerTonne: 36.744
bushelsPerTonne: 36.744,
bushelsPerTon: 33.33
}
],
[
@ -422,7 +445,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
img: WheatImg,
colour: "#46b298",
weightConversionKg: 27.2158214642112,
bushelsPerTonne: 36.744
bushelsPerTonne: 36.744,
bushelsPerTon: 33.33
}
],
[
@ -439,7 +463,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
img: WheatImg,
colour: "#46b298",
weightConversionKg: 27.2158214642112,
bushelsPerTonne: 36.744
bushelsPerTonne: 36.744,
bushelsPerTon: 33.33
}
],
[
@ -456,7 +481,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
img: FlaxImg,
colour: "#6e6d19",
weightConversionKg: 25.4014333665971,
bushelsPerTonne: 39.368
bushelsPerTonne: 39.368,
bushelsPerTon: 35.71
}
],
[
@ -473,7 +499,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
img: FlaxImg,
colour: "#6e6d19",
weightConversionKg: 25.4014333665971,
bushelsPerTonne: 39.368
bushelsPerTonne: 39.368,
bushelsPerTon: 35.71
}
],
[
@ -490,7 +517,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
img: PeaImg,
colour: "#ffe100",
weightConversionKg: 27.2158214642112,
bushelsPerTonne: 36.744
bushelsPerTonne: 36.744,
bushelsPerTon: 33.33
}
],
[
@ -507,7 +535,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
img: LentilImg,
colour: "#cb5e3c",
weightConversionKg: 27.2158214642112,
bushelsPerTonne: 36.744
bushelsPerTonne: 36.744,
bushelsPerTon: 33.33
}
],
[
@ -524,7 +553,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
img: LentilImg,
colour: "#cb5e3c",
weightConversionKg: 27.2158214642112,
bushelsPerTonne: 36.744
bushelsPerTonne: 36.744,
bushelsPerTon: 33.33
}
],
[
@ -541,7 +571,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
img: LentilImg,
colour: "#cb5e3c",
weightConversionKg: 27.2158214642112,
bushelsPerTonne: 36.744
bushelsPerTonne: 36.744,
bushelsPerTon: 33.33
}
],
[
@ -557,7 +588,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
targetMC: 15.0,
colour: "red",
weightConversionKg: 27.2155,
bushelsPerTonne: 36.744
bushelsPerTonne: 36.744,
bushelsPerTon: 33.33
}
],[
pond.Grain.GRAIN_DRY_BEANS_BLACK,
@ -572,7 +604,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
targetMC: 15.0,
colour: "grey",
weightConversionKg: 27.2155,
bushelsPerTonne: 36.744
bushelsPerTonne: 36.744,
bushelsPerTon: 33.33
}
],[
pond.Grain.GRAIN_RYE_A,
@ -587,7 +620,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
targetMC: 15.0,
colour: "grey",
weightConversionKg: 25.4,
bushelsPerTonne: 39.368
bushelsPerTonne: 39.368,
bushelsPerTon: 35.71
}
],[
pond.Grain.GRAIN_RYE_B,
@ -602,7 +636,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
targetMC: 15.0,
colour: "grey",
weightConversionKg: 25.4,
bushelsPerTonne: 39.368
bushelsPerTonne: 39.368,
bushelsPerTon: 35.71
},
],[
pond.Grain.GRAIN_RYE_C,
@ -617,7 +652,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
targetMC: 15.0,
colour: "grey",
weightConversionKg: 25.4,
bushelsPerTonne: 39.368
bushelsPerTonne: 39.368,
bushelsPerTon: 35.71
},
],[
pond.Grain.GRAIN_RYE_D,
@ -632,7 +668,8 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
targetMC: 15.0,
colour: "grey",
weightConversionKg: 25.4,
bushelsPerTonne: 39.368
bushelsPerTonne: 39.368,
bushelsPerTon: 35.71
}
]
]);

View file

@ -225,12 +225,13 @@ export default function GrainTransaction(props: Props) {
const createTransaction = (finalVal: number) => {
let dest = selectedDestination;
let source = selectedSource;
let bpt = 1;
let bPerTonne = 1;
let bPerTon = 1;
//use the sources bushel per tonne if one was selected otherwise use the destination
if (source) {
bpt = source.value.bushelsPerTonne();
bPerTonne = source.value.bushelsPerTonne();
} else if (dest) {
bpt = dest.value.bushelsPerTonne();
bPerTonne = dest.value.bushelsPerTonne();
}
let newTransaction = pond.Transaction.create({
@ -239,7 +240,7 @@ export default function GrainTransaction(props: Props) {
grainTransaction: pond.GrainTransaction.create({
bushels: finalVal,
message: grainMessage,
bushelsPerTonne: bpt
bushelsPerTonne: bPerTonne,
})
})
});
@ -449,6 +450,17 @@ export default function GrainTransaction(props: Props) {
);
};
const grainUnitDisplay = () => {
switch (getGrainUnit()){
case pond.GrainUnit.GRAIN_UNIT_TONNE:
return "mT"
case pond.GrainUnit.GRAIN_UNIT_TON:
return "t"
default:
return "bu"
}
}
return (
<ResponsiveDialog
open={open}
@ -506,14 +518,14 @@ export default function GrainTransaction(props: Props) {
InputProps={{
endAdornment: (
<InputAdornment position="end">
{getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT ? "mT" : "bu"}
{grainUnitDisplay()}
</InputAdornment>
)
}}
onChange={e => {
//if the user is viewing the grain in weight
let grainVal = e.target.value;
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT) {
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE) {
//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))) {
@ -523,6 +535,18 @@ export default function GrainTransaction(props: Props) {
grainVal = (+grainVal * selectedDestination.value.bushelsPerTonne()).toFixed(2);
}
}
}else if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON) {
if (!isNaN(parseFloat(e.target.value))) {
if (selectedSource) {
if(selectedSource.value.bushelsPerTonne){
grainVal = (+grainVal * (selectedSource.value.bushelsPerTonne() * 0.907)).toFixed(2);
}
} else if (selectedDestination) {
if(selectedDestination.value.bushelsPerTonne){
grainVal = (+grainVal * (selectedDestination.value.bushelsPerTonne() * 0.907)).toFixed(2);
}
}
}
}
setGrainEntry(e.target.value);
setGrainChangeVal(grainVal);

View file

@ -36,11 +36,14 @@ export default function GrainBagInventoryGraph(props: Props) {
//let time = hist.timestamp;
let val = hist.object.grainBagSettings.currentBushels ?? 0;
if (
getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT &&
getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE &&
grainBag.bushelsPerTonne() > 1
) {
val = Math.round((val / grainBag.bushelsPerTonne()) * 100) / 100;
}
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON && grainBag.bushelsPerTonne() > 1){
val = Math.round((val / (grainBag.bushelsPerTonne() * 0.907)) * 100) / 100;
}
if (val !== lastBushels) {
lastBushels = val;
barData.push({
@ -51,13 +54,16 @@ export default function GrainBagInventoryGraph(props: Props) {
}
});
if (barData.length === 0) {
let val = grainBag.bushels()
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE && grainBag.bushelsPerTonne() > 1){
val = grainBag.bushels() / grainBag.bushelsPerTonne()
}
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON && grainBag.bushelsPerTonne() > 1){
val = grainBag.bushels() / (grainBag.bushelsPerTonne() * 0.907)
}
barData.push({
timestamp: moment.now().valueOf(),
value:
getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT &&
grainBag.bushelsPerTonne() > 1
? Math.round((grainBag.bushels() / grainBag.bushelsPerTonne()) * 100) / 100
: grainBag.bushels()
value: Math.round(val*100)/100
});
}
setData(barData);
@ -71,6 +77,17 @@ export default function GrainBagInventoryGraph(props: Props) {
}
}, [grainBagAPI, grainBag, startDate, endDate, openSnack]); //eslint-disable-line react-hooks/exhaustive-deps
const maxYAxis = () => {
let val = grainBag.capacity()
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE){
val = Math.round((val / grainBag.bushelsPerTonne()) * 100) / 100;
}
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON && grainBag.bushelsPerTonne() > 1){
val = Math.round((val / (grainBag.bushelsPerTonne() * 0.907)) * 100) / 100;
}
return val
}
return (
<Card raised>
<Box padding={2}>
@ -87,11 +104,7 @@ export default function GrainBagInventoryGraph(props: Props) {
}
data={data}
yMin={0}
yMax={
getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT
? grainBag.capacity() / grainBag.bushelsPerTonne()
: grainBag.capacity()
}
yMax={maxYAxis()}
customHeight={customHeight}
useGradient
labels

View file

@ -66,7 +66,7 @@ export default function GrainBagSettings(props: Props) {
const theme = useTheme();
const [grainUpdate, setGrainUpdate] = useState(false);
const [grainDiff, setGrainDiff] = useState(0);
const [bushelsPerTonne, setBushelsPerTonne] = useState(0);
const [bushelConversion, setBushelConversion] = useState(0);
useEffect(() => {
if (grainBag) {
@ -84,8 +84,10 @@ export default function GrainBagSettings(props: Props) {
: grainBag.settings.length
);
let grainVal = grainBag.bushels();
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT) {
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE) {
grainVal = grainBag.bushels() / grainBag.bushelsPerTonne();
}else if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON) {
grainVal = grainBag.bushels() / (grainBag.bushelsPerTonne() * 0.907)
}
setGrainFill(grainVal.toFixed(2));
setBagName(grainBag.name());
@ -117,6 +119,10 @@ export default function GrainBagSettings(props: Props) {
const submit = () => {
//if a bag was passed in do an update
let tonneConversion = bushelConversion
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
tonneConversion = bushelConversion / 0.907
}
if (grainBag) {
grainBag.title = bagName;
grainBag.settings.initialMoisture = initialMoisture;
@ -127,7 +133,7 @@ export default function GrainBagSettings(props: Props) {
grainBag.settings.bushelCapacity = grainCapacity;
grainBag.settings.grainSubtype = grainSubtype;
grainBag.settings.fillDate = fillDate;
grainBag.settings.bushelsPerTonne = bushelsPerTonne;
grainBag.settings.bushelsPerTonne = tonneConversion;
grainBag.settings.storageType = isCustom
? pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN
: pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN;
@ -161,6 +167,7 @@ export default function GrainBagSettings(props: Props) {
settings.currentBushels = grainBushels;
settings.grainSubtype = grainSubtype;
settings.fillDate = fillDate;
settings.bushelsPerTonne = tonneConversion;
settings.storageType = isCustom
? pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN
: pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN;
@ -205,10 +212,10 @@ export default function GrainBagSettings(props: Props) {
/>
<TextField
label="Bushels Per Tonne"
value={bushelsPerTonne}
value={bushelConversion}
type="number"
onChange={event => {
setBushelsPerTonne(+event.target.value);
setBushelConversion(+event.target.value);
}}
fullWidth
className={classes.bottomSpacing}
@ -227,7 +234,7 @@ export default function GrainBagSettings(props: Props) {
? pond.Grain[option.value as keyof typeof pond.Grain]
: pond.Grain.GRAIN_INVALID;
setSupportedGrainType(newGrainType);
setBushelsPerTonne(GrainDescriber(newGrainType).bushelsPerTonne);
setBushelConversion(GrainDescriber(newGrainType).bushelsPerTonne);
setSelectedGrainOp(option);
}}
group
@ -246,6 +253,18 @@ export default function GrainBagSettings(props: Props) {
return invalid;
};
const grainUnitDisplay = () => {
if(bushelConversion > 1){
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE){
return "mT"
}else if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
return "t"
}
}else{
return "bu"
}
}
return (
<React.Fragment>
<GrainTransaction
@ -389,30 +408,24 @@ export default function GrainBagSettings(props: Props) {
className={classes.bottomSpacing}
/>
<TextField
label={
"Current " +
(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && bushelsPerTonne > 0
? "Weight"
: "Bushels")
}
label={"Current Inventory"}
fullWidth
type="number"
variant="outlined"
InputProps={{
endAdornment: (
<InputAdornment position="end">
{getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && bushelsPerTonne > 0
? "mT"
: "bu"}
{grainUnitDisplay()}
</InputAdornment>
)
}}
value={grainFill}
onChange={e => {
let bushelVal = +e.target.value;
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT) {
//convert the number as a weight into the bushel value
bushelVal = +e.target.value * (bushelsPerTonne > 0 ? bushelsPerTonne : 1);
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE || getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON) {
//convert the number as a weight into the bushel value, no changing or conversions necessary
// since these are both user entered fields and should be the same unit (ton or tonne)
bushelVal = +e.target.value * (bushelConversion > 0 ? bushelConversion : 1);
}
if (bushelVal > grainCapacity) {
bushelVal = grainCapacity;

View file

@ -83,12 +83,19 @@ export default function GrainBagVisualizer(props: Props) {
const grainOverlay = () => {
let displayPending = pendingGrainAmount;
let displayDiff = grainDiff;
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && grainBag.bushelsPerTonne() > 1) {
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE && grainBag.bushelsPerTonne() > 1) {
if (displayPending && displayDiff) {
displayPending = Math.round((displayPending / grainBag.bushelsPerTonne()) * 100) / 100;
displayDiff = Math.round((displayDiff / grainBag.bushelsPerTonne()) * 100) / 100;
}
}
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON && grainBag.bushelsPerTonne() > 1) {
if (displayPending && displayDiff) {
displayPending = Math.round((displayPending / (grainBag.bushelsPerTonne() * 0.907)) * 100) / 100;
displayDiff = Math.round((displayDiff / (grainBag.bushelsPerTonne()* 0.907)) * 100) / 100;
}
}
return (
<Box>
{grainDiff !== undefined && grainDiff > 0 && (
@ -141,7 +148,7 @@ export default function GrainBagVisualizer(props: Props) {
};
const grainAmountDisplay = () => {
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && grainBag.bushelsPerTonne() > 1) {
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE && grainBag.bushelsPerTonne() > 1) {
return (
<div style={{ display: "flex", flexDirection: "row" }}>
<Typography variant="body2" style={{ fontWeight: "bold", marginRight: "4px" }}>
@ -150,6 +157,15 @@ export default function GrainBagVisualizer(props: Props) {
<Typography variant="body2">({grainBag.fillPercent()}%)</Typography>
</div>
);
} else if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON && grainBag.bushelsPerTonne() > 1) {
return (
<div style={{ display: "flex", flexDirection: "row" }}>
<Typography variant="body2" style={{ fontWeight: "bold", marginRight: "4px" }}>
{(grainBag.bushels() / (grainBag.bushelsPerTonne()*0.907)).toFixed(2) + " t"}
</Typography>
<Typography variant="body2">({grainBag.fillPercent()}%)</Typography>
</div>
);
} else {
return (
<div style={{ display: "flex", flexDirection: "row" }}>

123
src/hooks/useWebSocket.ts Normal file
View file

@ -0,0 +1,123 @@
import { useEffect, useRef, useCallback } from "react";
/**
* Derives the WebSocket base URL from VITE_APP_API_URL.
* e.g. "https://api.brandxtech.ca" -> "wss://api.brandxtech.ca"
* Falls back to current page host for local dev.
*/
function getWsBaseUrl(): string {
const apiUrl = import.meta.env.VITE_APP_API_URL;
if (apiUrl) {
return apiUrl.replace(/^http/, "ws");
}
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
return `${protocol}//${window.location.host}`;
}
interface UseWebSocketOptions<T> {
/** The API path, e.g. "/v1/live/devices/123/components" */
path: string;
/** Transform the raw MessageEvent into your domain type */
parse: (event: MessageEvent) => T;
/** Called whenever a new parsed message arrives */
onMessage: (data: T) => void;
/** Auth token passed as ?token= query param */
token?: string;
/** Minimum seconds between updates, passed as ?rate= to the backend */
rate?: number;
/** Whether the connection should be active. Default true. */
enabled?: boolean;
}
/**
* useWebSocket manages a WebSocket connection with automatic reconnection.
*
* Usage:
* useWebSocket({
* path: `/v1/live/devices/${deviceID}/components`,
* parse: (e) => Component.any(JSON.parse(e.data)),
* onMessage: (component) => handleUpdate(component),
* token: authToken,
* });
*/
export function useWebSocket<T>(options: UseWebSocketOptions<T>) {
const { path, parse, onMessage, token, rate = 0, enabled = true } = options;
// Keep latest callbacks in refs so reconnects don't use stale closures
const onMessageRef = useRef(onMessage);
onMessageRef.current = onMessage;
const parseRef = useRef(parse);
parseRef.current = parse;
const wsRef = useRef<WebSocket | null>(null);
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
const retriesRef = useRef(0);
const connect = useCallback(() => {
if (!token) return;
const params = new URLSearchParams();
params.set("token", token);
if (rate > 0) params.set("rate", rate.toString());
const url = `${getWsBaseUrl()}${path}?${params.toString()}`;
const ws = new WebSocket(url);
wsRef.current = ws;
ws.onopen = () => {
console.debug(`[ws] connected: ${path}`);
retriesRef.current = 0;
};
ws.onmessage = (event) => {
try {
const parsed = parseRef.current(event);
onMessageRef.current(parsed);
} catch (err) {
console.warn(`[ws] parse error on ${path}:`, err);
}
};
ws.onerror = (err) => {
console.warn(`[ws] error on ${path}:`, err);
};
ws.onclose = (event) => {
console.debug(`[ws] closed: ${path} (code: ${event.code})`);
wsRef.current = null;
// Don't reconnect on clean close or auth rejection
if (event.code === 1000 || event.code === 4001 || event.code === 4003) {
return;
}
// Exponential backoff: 1s, 2s, 4s, 8s, ... capped at 30s
const delay = Math.min(1000 * Math.pow(2, retriesRef.current), 30000);
retriesRef.current += 1;
console.debug(`[ws] reconnecting in ${delay}ms...`);
reconnectTimeoutRef.current = setTimeout(connect, delay);
};
}, [path, token, rate]);
useEffect(() => {
if (!enabled || !token) {
if (wsRef.current) {
wsRef.current.close(1000, "disabled");
wsRef.current = null;
}
return;
}
connect();
return () => {
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
}
if (wsRef.current) {
wsRef.current.close(1000, "cleanup");
wsRef.current = null;
}
};
}, [connect, enabled, token]);
}

View file

@ -2,6 +2,7 @@ import GrainDescriber from "grain/GrainDescriber";
import { cloneDeep } from "lodash";
import { MarkerData } from "maps/mapMarkers/Markers";
import { pond } from "protobuf-ts/pond";
import { getGrainUnit } from "utils";
import { stringToMaterialColour } from "utils/strings";
import { or } from "utils/types";
@ -235,17 +236,14 @@ export class Bin {
return bpt;
}
/**
* gets the weight in tonnes for the grain inventory provided the bushels per tonne is set
* if it is not set it will divide by 1 effectively returning the bushels
* @returns grain weight
*/
public grainTonnes(): number {
let weight = 0;
if (this.settings.inventory) {
weight = this.settings.inventory.grainBushels / this.bushelsPerTonne();
public grainInventory(): number {
let grain = this.bushels()
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE && this.bushelsPerTonne() > 1){
grain = this.bushels() / this.bushelsPerTonne()
}else if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON && this.bushelsPerTonne() > 1){
grain = this.bushels() / (this.bushelsPerTonne() * 0.907)
}
return Math.round(weight * 100) / 100;
return Math.round(grain*100)/100
}
/**

View file

@ -29,7 +29,13 @@ export class Contract {
private measurementUnit(): string {
if (this.settings.type === pond.ContractType.CONTRACT_TYPE_GRAIN) {
return getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT ? "mT" : "bu";
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE && this.conversionValue() > 1){
return "mT"
}
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON && this.conversionValue() > 1){
return "t"
}
return "bu";
}
return "";
}
@ -202,9 +208,12 @@ export class Contract {
let size = this.settings.size;
switch (this.settings.type) {
case pond.ContractType.CONTRACT_TYPE_GRAIN:
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && this.conversionValue() > 1) {
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE && this.conversionValue() > 1) {
size = size / this.conversionValue();
}
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON && this.conversionValue() > 1){
size = size / (this.conversionValue() * 0.907); //the conversion for grain will be stored in metric tonnes, this will convert it to US tons
}
}
return Math.round(size * 100) / 100;
}
@ -213,26 +222,33 @@ export class Contract {
let del = this.settings.delivered;
switch (this.settings.type) {
case pond.ContractType.CONTRACT_TYPE_GRAIN:
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && this.conversionValue() > 1) {
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE && this.conversionValue() > 1) {
del = del / this.conversionValue();
}
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON && this.conversionValue() > 1){
del = del / (this.conversionValue() * 0.907); //the conversion for grain will be stored in metric tonnes, this will convert it to US tons
}
}
return Math.round(del * 100) / 100;
}
public static toStoredUnit(
secondaryUnitVal: number,
contractType: pond.ContractType,
conversionValue: number
): number {
let storedunitVal = secondaryUnitVal;
let storedUnitVal = secondaryUnitVal;
switch (contractType) {
//use the value and conversion they entered directly, it is safe to assume they are both the same unit (ton vs tonne) so dont need to convert anything
//before converting to bushels
case pond.ContractType.CONTRACT_TYPE_GRAIN:
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && conversionValue > 1) {
storedunitVal = secondaryUnitVal * conversionValue;
if ((getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE || getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON) && conversionValue > 1) {
storedUnitVal = secondaryUnitVal * conversionValue;
}
}
return storedunitVal;
return storedUnitVal;
}
public grainName(): string {

View file

@ -928,9 +928,47 @@ export default function Bins(props: Props) {
);
};
const grainUnitDisplay = (custom?: pond.CustomInventory) => {
// If custom and not convertible, always bushels
if (custom && custom.bushelsPerTonne <= 1) {
return " bu";
}
switch (getGrainUnit()) {
case pond.GrainUnit.GRAIN_UNIT_TONNE:
return " mT";
case pond.GrainUnit.GRAIN_UNIT_TON:
return " t";
default:
return " bu";
}
};
const customQuantityDisplay = (customInventory: pond.CustomInventory, bushels: number) => {
let amount = bushels
if(customInventory.bushelsPerTonne > 1){
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE){
amount = bushels/customInventory.bushelsPerTonne
}else if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
amount = bushels/(customInventory.bushelsPerTonne*0.907)
}
}
return Math.round(amount*100)/100
}
const supportedGrainDisplay = (grain: pond.Grain, bushels: number) => {
const describer = GrainDescriber(grain)
let amount = bushels
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE){
amount = bushels/describer.bushelsPerTonne
}else if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
amount = bushels/describer.bushelsPerTon
}
return Math.round(amount*100)/100
}
const binUtilizationList = () => {
const hasInventory = binMetrics && binMetrics.grainInventory.length > 0;
const useWeight = getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT;
return (
<Accordion
className={classes.accordion}
@ -972,17 +1010,9 @@ export default function Bins(props: Props) {
<ImageListItem key={key}>
<BinUtilizationChart
grain={inv.grainType}
customUnit={useWeight ? " mT" : " bu"}
bushelAmount={
useWeight
? inv.bushelAmount / GrainDescriber(inv.grainType).bushelsPerTonne
: inv.bushelAmount
}
bushelCapacity={
useWeight
? inv.bushelCapacity / GrainDescriber(inv.grainType).bushelsPerTonne
: inv.bushelCapacity
}
customUnit={grainUnitDisplay()}
bushelAmount={supportedGrainDisplay(inv.grainType, inv.bushelAmount)}
bushelCapacity={supportedGrainDisplay(inv.grainType, inv.bushelCapacity)}
onClick={() => {
setContentFilter(pond.Grain[inv.grainType]);
loadMoreBins(pond.Grain[inv.grainType]);
@ -1001,11 +1031,10 @@ export default function Bins(props: Props) {
amount = amount * 35.239;
cap = cap * 35.239;
unit = " L";
}
if (useWeight && inv.bushelsPerTonne > 1) {
amount = amount / inv.bushelsPerTonne;
cap = cap / inv.bushelsPerTonne;
unit = " mT";
} else {
amount = customQuantityDisplay(inv, amount)
cap = customQuantityDisplay(inv, cap)
unit = grainUnitDisplay(inv);
}
return (

View file

@ -6,7 +6,7 @@ import { useDeviceAPI, useGlobalState, useSnackbar } from "providers";
import { useEffect, useState } from "react";
import { useLocation, useParams } from "react-router-dom";
import PageContainer from "./PageContainer";
import { useMobile } from "hooks";
import { useHTTP, useMobile } from "hooks";
import LoadingScreen from "app/LoadingScreen";
import SmartBreadcrumb from "common/SmartBreadcrumb";
import DeviceActions from "device/DeviceActions";
@ -21,6 +21,7 @@ import { or } from "utils";
import ComponentDiagnostics from "component/ComponentDiagnostics";
import DeviceWizard from "device/DeviceWizard";
import DeviceScannedComponents from "device/autoDetect/DeviceScannedComponents";
import { useWebSocket } from "hooks/useWebSocket";
export interface DevicePageData {
device: Device;
@ -65,6 +66,8 @@ export default function DevicePage() {
// const [devicePageData, setDevicePageData] = useState(pond.GetDevicePageDataResponse.create())
const { token } = useHTTP();
const loadDevice = () => {
if (loading) return
if (state?.devicePageData) {
@ -165,6 +168,65 @@ export default function DevicePage() {
.finally(() => setLoading(false));
}
// --- Real-time component updates ---
// Streams all component changes for this device.
// When the backend receives a new reading and updates a component,
// this fires and updates the component in state without a page refresh.
useWebSocket<{ key: string; component: Component } | null>({
path: `/live/devices/${deviceID}/components`,
parse: (e) => {
try {
const raw = JSON.parse(e.data);
const comp = Component.any(raw);
if (!comp?.settings) {
console.warn("[ws] received component without settings:", raw);
return null;
}
return { key: comp.key(), component: comp };
} catch (err) {
console.warn("[ws] failed to parse component:", err);
return null;
}
},
onMessage: (data) => {
if (!data) return;
const { key, component } = data;
setComponents((prev) => {
const updated = new Map(prev);
updated.set(key, component);
return updated;
});
},
token,
enabled: !!deviceID,
});
// --- Real-time device updates (optional) ---
// Updates device-level info (name, status, lastActive, etc.)
useWebSocket<Device | null>({
path: `/live/devices/${deviceID}`,
parse: (e) => {
try {
const raw = JSON.parse(e.data);
const dev = Device.any(raw);
if (!dev?.settings) {
console.warn("[ws] received device without settings:", raw);
return null;
}
return dev;
} catch (err) {
console.warn("[ws] failed to parse device:", err);
return null;
}
},
onMessage: (updatedDevice) => {
if (!updatedDevice) return;
setDevice(updatedDevice);
},
token,
enabled: !!deviceID,
});
const loadPortScan = () => {
deviceAPI.listFoundComponents(parseInt(deviceID))
.then(resp => {

View file

@ -168,7 +168,8 @@ export default function SignupCallback () {
variant="outlined"
InputLabelProps={{ shrink: true }}>
<MenuItem value={pond.GrainUnit.GRAIN_UNIT_BUSHELS}>Bushels (bu)</MenuItem>
<MenuItem value={pond.GrainUnit.GRAIN_UNIT_WEIGHT}>Tonnes (mT)</MenuItem>
<MenuItem value={pond.GrainUnit.GRAIN_UNIT_TONNE}>Tonnes (mT)</MenuItem>
<MenuItem value={pond.GrainUnit.GRAIN_UNIT_TON}>US Tons (t)</MenuItem>
</TextField>
</Grid2>
)}

View file

@ -59,7 +59,6 @@ export default function GrainBag(props: Props) {
}
console.log
userAPI.getUser(user.id(), { key: key, kind: kind } as Scope).then(resp => {
console.log(resp)
setPermissions(resp.permissions);
});
}, [as, bagID, userAPI, user]);

View file

@ -19,6 +19,7 @@ interface IHTTPContext {
) => Promise<AxiosResponse<T>>;
del: <T>(url: string, spreadOptions?: AxiosRequestConfig) => Promise<AxiosResponse<T>>;
options: (demo?: boolean) => AxiosRequestConfig;
token: string;
}
interface Props extends PropsWithChildren<any>{
@ -104,7 +105,8 @@ export default function HTTPProvider(props: Props) {
put,
post,
del,
options: defaultOptions
options: defaultOptions,
token
}}>
{/* <BillingProvider> */}
<PondProvider>

View file

@ -147,7 +147,7 @@ export default function GrainBagProvider(props: PropsWithChildren<Props>) {
(end && "&end=" + end)
)
if (view) {
url = url + "?as=" + view
url = url + "&as=" + view
}
return get<pond.ListGrainBagHistoryResponse>(url);
};

View file

@ -336,9 +336,9 @@ export function getWhitelabel(): WhiteLabel {
if (window.location.origin.includes("bxt-dev")) {
return BXT_WHITE_LABEL;
}
// if (window.location.origin.includes("localhost")) {
// return STAGING_WHITELABEL;
// }
if (window.location.origin.includes("localhost")) {
return STREAMLINE_WHITE_LABEL;
}
if (window.location.origin.includes("staging") || import.meta.env.VITE_LOCAL_STAGING=='true') {
return STAGING_WHITELABEL;
}

View file

@ -11,6 +11,17 @@ interface Props {
export default function TransactionDataDisplay(props: Props) {
const { transaction } = props;
console.log(transaction)
const grainDisplay = (gt: pond.GrainTransaction) => {
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE && gt.bushelsPerTonne > 1){
return "Weight: " + Math.round(gt.bushels / gt.bushelsPerTonne*100)/100 + " mT"
}
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON && gt.bushelsPerTonne > 1){
return "Weight: " + Math.round(gt.bushels / (gt.bushelsPerTonne*0.907)*100)/100 + " t"
}
return "Bushels: " + gt.bushels
}
const display = () => {
let transactionData = transaction.transaction.transaction;
@ -27,9 +38,7 @@ export default function TransactionDataDisplay(props: Props) {
</Typography>
<Typography>Variant: {gt.subtype}</Typography>
<Typography>
{getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && gt.bushelsPerTonne > 1
? "Weight: " + gt.bushels / gt.bushelsPerTonne + " mT"
: "Bushels: " + gt.bushels}
{grainDisplay(gt)}
</Typography>
<Typography>Message: {gt.message}</Typography>
</Box>

View file

@ -455,8 +455,11 @@ export default function UserSettings(props: Props) {
case 1:
grainUnit = pond.GrainUnit.GRAIN_UNIT_BUSHELS;
break;
case 2:
grainUnit = pond.GrainUnit.GRAIN_UNIT_WEIGHT;
case 3:
grainUnit = pond.GrainUnit.GRAIN_UNIT_TONNE;
break;
case 4:
grainUnit = pond.GrainUnit.GRAIN_UNIT_TON;
break;
default:
grainUnit = pond.GrainUnit.GRAIN_UNIT_UNKNOWN;
@ -540,7 +543,8 @@ export default function UserSettings(props: Props) {
variant="outlined"
InputLabelProps={{ shrink: true }}>
<MenuItem value={pond.GrainUnit.GRAIN_UNIT_BUSHELS}>Bushels (bu)</MenuItem>
<MenuItem value={pond.GrainUnit.GRAIN_UNIT_WEIGHT}>Tonnes (mT)</MenuItem>
<MenuItem value={pond.GrainUnit.GRAIN_UNIT_TONNE}>Tonnes (mT)</MenuItem>
<MenuItem value={pond.GrainUnit.GRAIN_UNIT_TON}>US Tons (t)</MenuItem>
</TextField>
</Grid2>
)}

View file

@ -55,13 +55,33 @@ export function setDistanceUnit(unit: pond.DistanceUnit) {
}
export function getGrainUnit(): pond.GrainUnit {
return localStorage.getItem("grainUnit") === "mT"
? pond.GrainUnit.GRAIN_UNIT_WEIGHT
: pond.GrainUnit.GRAIN_UNIT_BUSHELS;
switch(localStorage.getItem("grainUnit")){
case "mT":
return pond.GrainUnit.GRAIN_UNIT_TONNE
case "t":
return pond.GrainUnit.GRAIN_UNIT_TON
default:
return pond.GrainUnit.GRAIN_UNIT_BUSHELS
}
// return localStorage.getItem("grainUnit") === "mT"
// ? pond.GrainUnit.GRAIN_UNIT_WEIGHT
// : pond.GrainUnit.GRAIN_UNIT_BUSHELS;
}
export function setGrainUnit(unit: pond.GrainUnit) {
localStorage.setItem("grainUnit", unit === pond.GrainUnit.GRAIN_UNIT_WEIGHT ? "mT" : "bu");
switch(unit){
case pond.GrainUnit.GRAIN_UNIT_WEIGHT:
case pond.GrainUnit.GRAIN_UNIT_TONNE:
localStorage.setItem("grainUnit", "mT")
break;
case pond.GrainUnit.GRAIN_UNIT_TON:
localStorage.setItem("grainUnit", "t")
break;
default:
localStorage.setItem("grainUnit", "bu")
}
// localStorage.setItem("grainUnit", unit === pond.GrainUnit.GRAIN_UNIT_WEIGHT ? "mT" : "bu");
}
export const distanceConversion = (val: number) => {