Merge branch 'staging_environment' into i2c_detect

This commit is contained in:
csawatzky 2025-10-06 16:15:24 -06:00
commit ceaff4da9e
49 changed files with 1470 additions and 412 deletions

View file

@ -71,6 +71,12 @@ http {
add_header Cache-Control "no-store, no-cache, must-revalidate"; add_header Cache-Control "no-store, no-cache, must-revalidate";
} }
# Force re-cache of old service-worker
location = /service-worker.js {
root /usr/share/nginx/html; # adjust if your build folder is elsewhere
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
# Redirect old /index.html requests to new file # Redirect old /index.html requests to new file
location = /index.html { location = /index.html {
return 301 /indexV2.html; return 301 /indexV2.html;

2
package-lock.json generated
View file

@ -10911,7 +10911,7 @@
}, },
"node_modules/protobuf-ts": { "node_modules/protobuf-ts": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#953a6c9d04b26b3fdc78a71016aff37a62d9e22a", "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#0d0084ce207218ea5c8c94addfbf009814c35a1f",
"dependencies": { "dependencies": {
"protobufjs": "^6.8.8" "protobufjs": "^6.8.8"
} }

24
public/service-worker.js Normal file
View file

@ -0,0 +1,24 @@
// public/service-worker.js
self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', async () => {
// Unregister old CRA SW
const regs = await self.registration.scope ? [self.registration] : await self.clients.getRegistrations();
for (const reg of regs) {
if (reg && reg.scriptURL.endsWith('service-worker.js')) {
await reg.unregister();
}
}
// Clear all caches
if ('caches' in self) {
const keys = await caches.keys();
for (const key of keys) {
await caches.delete(key);
}
}
// Reload all clients
const clients = await self.clients.matchAll({ type: 'window' });
clients.forEach(client => client.navigate(client.url));
});

View file

@ -50,7 +50,8 @@ function App() {
const skipCallbacks = [ const skipCallbacks = [
"/johndeere", "/johndeere",
"/cnhi" "/cnhi",
"/libracart"
] ]
return ( return (

View file

@ -82,7 +82,20 @@ export default function UserWrapper(props: Props) {
firmware: new Map() firmware: new Map()
}) })
}).catch(() => { }).catch(() => {
setGlobal(globalDefault) userAPI.getUser(user_id).then(user => {
setGlobal({
user: user ? user : User.create(),
team: globalDefault.team,
as: "",
showErrors: false,
userTeamPermissions: [],
backgroundTasksComplete: false,
firmware: new Map()
})
}).catch(() => {
setGlobal(globalDefault)
})
}) })
.finally(() => { .finally(() => {
setLoading(false) setLoading(false)

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View file

@ -24,7 +24,7 @@ import RemoveSelfFromObject from "user/RemoveSelfFromObject";
import ShareObject from "user/ShareObject"; import ShareObject from "user/ShareObject";
import { isOffline } from "utils/environment"; import { isOffline } from "utils/environment";
import ObjectTeams from "teams/ObjectTeams"; import ObjectTeams from "teams/ObjectTeams";
// import BinDuplication from "./BinDuplication"; import BinDuplication from "./BinDuplication";
import BinsIcon from "products/Bindapt/BinsIcon"; import BinsIcon from "products/Bindapt/BinsIcon";
import HelpIcon from "@mui/icons-material/Help"; import HelpIcon from "@mui/icons-material/Help";
import BinSensors from "./BinSensors"; import BinSensors from "./BinSensors";
@ -269,14 +269,14 @@ export default function BinActions(props: Props) {
closeDialogCallback={() => setOpenState({ ...openState, users: false })} closeDialogCallback={() => setOpenState({ ...openState, users: false })}
refreshCallback={refreshCallback} refreshCallback={refreshCallback}
/> />
{/* <BinDuplication <BinDuplication
open={openState.duplication} open={openState.duplication}
closeDialog={() => { closeDialog={() => {
setOpenState({ ...openState, duplication: false }); setOpenState({ ...openState, duplication: false });
}} }}
bin={bin} bin={bin}
refreshCallback={refreshCallback} refreshCallback={refreshCallback}
/> */} />
<RemoveSelfFromObject <RemoveSelfFromObject
scope={binScope(key)} scope={binScope(key)}
label={label} label={label}

View file

@ -103,8 +103,10 @@ export default function BinCard(props: Props) {
let filteredNodes = c.filteredNodes(true) let filteredNodes = c.filteredNodes(true)
allTemps.push(...filteredNodes.temps); allTemps.push(...filteredNodes.temps);
allHums.push(...filteredNodes.humids); if(avg(c.humidities) !== 0){ //if the average of the humidities is 0 that means that all the readings are 0 and it is a temp only cable
allMoistures.push(...filteredNodes.moistures); allHums.push(...filteredNodes.humids);
allMoistures.push(...filteredNodes.moistures);
}
//set the hottest and coldest nodes //set the hottest and coldest nodes
if (coldestNode === undefined || Math.min(...filteredNodes.temps) < coldestNode.temp) { if (coldestNode === undefined || Math.min(...filteredNodes.temps) < coldestNode.temp) {
@ -490,7 +492,7 @@ export default function BinCard(props: Props) {
return ( return (
<Card style={{ cursor: "pointer" }}> <Card style={{ cursor: "pointer" }}>
{user.hasFeature("admin") && ( {user.hasFeature("installer") && (
<IconButton <IconButton
style={{ style={{
height: 35, height: 35,
@ -506,9 +508,6 @@ export default function BinCard(props: Props) {
+ +
</IconButton> </IconButton>
)} )}
{/* <Box position="absolute" top={4} right={4}>
<BinModeDot mode={bin.settings.mode} />
</Box> */}
<Box padding={1}> <Box padding={1}>
<Typography <Typography
align="left" align="left"

View file

@ -33,7 +33,7 @@ const useStyles = makeStyles((theme: Theme) => {
interface Props { interface Props {
interactions: Interaction[]; interactions: Interaction[];
interactionDevices: Map<string, number>; interactionDevices: Map<string, number>;
mode: pond.BinMode.BIN_MODE_DRYING | pond.BinMode.BIN_MODE_HYDRATING; mode: pond.BinMode;
plenums?: Plenum[]; plenums?: Plenum[];
ambients?: Ambient[]; ambients?: Ambient[];
heaters?: Controller[]; heaters?: Controller[];
@ -94,10 +94,21 @@ export default function BinConditioningCard(props: Props) {
return conditioningInteractions; return conditioningInteractions;
}; };
const modeDisplay = () => {
switch(mode){
case pond.BinMode.BIN_MODE_DRYING:
return "Drying"
case pond.BinMode.BIN_MODE_HYDRATING:
return "Hydrating"
case pond.BinMode.BIN_MODE_COOLDOWN:
return "Cooldown"
}
}
return ( return (
<Card raised className={classes.card}> <Card raised className={classes.card}>
<Typography style={{ fontWeight: 650 }}> <Typography style={{ fontWeight: 650 }}>
{mode === pond.BinMode.BIN_MODE_DRYING ? "Drying" : "Hydrating"} Conditions {modeDisplay()} Conditions
</Typography> </Typography>
{conditionsDisplay()} {conditionsDisplay()}
</Card> </Card>

View file

@ -47,8 +47,6 @@ const useStyles = makeStyles((theme: Theme) => {
sliderThumb: { sliderThumb: {
height: 15, height: 15,
width: 15, width: 15,
marginTop: -6,
marginLeft: -7.5,
backgroundColor: "yellow" backgroundColor: "yellow"
}, },
sliderTrack: { sliderTrack: {
@ -60,10 +58,10 @@ const useStyles = makeStyles((theme: Theme) => {
backgroundColor: "white" backgroundColor: "white"
}, },
sliderValLabel: { sliderValLabel: {
left: "calc(-50%)", left: -20,
top: 22, top: 40,
background: "transparent",
"& *": { "& *": {
background: "transparent",
color: "#fff" color: "#fff"
} }
}, },

View file

@ -0,0 +1,72 @@
import {
Button,
DialogActions,
DialogContent,
DialogTitle,
TextField,
Typography
} from "@mui/material";
import ResponsiveDialog from "common/ResponsiveDialog";
import { Bin } from "models";
import { useBinAPI, useSnackbar } from "providers";
import { useEffect, useState } from "react";
interface Props {
open: boolean;
bin: Bin;
closeDialog: () => void;
refreshCallback: () => void;
}
export default function BinDuplication(props: Props) {
const { open, bin, closeDialog, refreshCallback } = props;
const [binDupName, setBinDupName] = useState("(copy of) " + bin.name());
const binAPI = useBinAPI();
const { openSnack } = useSnackbar();
useEffect(() => {
setBinDupName("(copy of) " + bin.name());
}, [bin]);
const duplicate = () => {
let settings = bin.settings;
settings.name = binDupName;
binAPI
.addBin(settings)
.then(resp => {
openSnack("Successfully duplicated bin");
})
.catch(err => {
openSnack("Failed to duplicate bin");
});
refreshCallback();
closeDialog();
};
return (
<ResponsiveDialog open={open} onClose={closeDialog}>
<DialogTitle>Create Duplicate Bin</DialogTitle>
<DialogContent>
<Typography>
This will create a new bin with the same settings as {bin.name()}.
</Typography>
<TextField
id={"duplicateName"}
fullWidth
margin="normal"
label="Name of Copy"
value={binDupName}
onChange={e => setBinDupName(e.target.value)}
/>
</DialogContent>
<DialogActions>
<Button onClick={closeDialog} style={{ color: "red" }}>
Cancel
</Button>
<Button onClick={duplicate} color="primary">
Confirm
</Button>
</DialogActions>
</ResponsiveDialog>
);
}

View file

@ -476,11 +476,11 @@ export default function BinSVGV2(props: Props) {
return cableGrainPath; return cableGrainPath;
}; };
const nodeClass = (nodeTemp: number) => { const nodeClass = (nodeTemp: number, underTop: boolean) => {
if (hottestNodeTemp && hottestNodeTemp.toFixed(2) === nodeTemp.toFixed(2)) { if (hottestNodeTemp && hottestNodeTemp.toFixed(2) === nodeTemp.toFixed(2) && underTop) {
return classes.hotNode; return classes.hotNode;
} }
if (coldestNodeTemp && coldestNodeTemp.toFixed(2) === nodeTemp.toFixed(2)) { if (coldestNodeTemp && coldestNodeTemp.toFixed(2) === nodeTemp.toFixed(2) && underTop) {
return classes.coldNode; return classes.coldNode;
} }
return classes.cableNode; return classes.cableNode;
@ -507,12 +507,12 @@ export default function BinSVGV2(props: Props) {
//if the cables have the same number of nodes //if the cables have the same number of nodes
if (b.temperatures.length === a.temperatures.length) { if (b.temperatures.length === a.temperatures.length) {
//sort by temp only last and any type that has humidity first //sort by temp only last and any type that has humidity first
if ( if ((avg(a.humidities) === 0) && (avg(b.humidities) !== 0)) {
a.settings.subtype === quack.GrainCableSubtype.GRAIN_CABLE_SUBTYPE_OPI_TEMP &&
b.settings.subtype !== quack.GrainCableSubtype.GRAIN_CABLE_SUBTYPE_OPI_TEMP
) {
return 1; return 1;
} else { } else if ((avg(b.humidities) === 0) && (avg(a.humidities) !== 0)) {
return -1;
}
else {
return a.key() > b.key() ? 1 : -1; return a.key() > b.key() ? 1 : -1;
} }
} }
@ -759,7 +759,7 @@ export default function BinSVGV2(props: Props) {
return ( return (
<g key={e.key}> <g key={e.key}>
{e.nodeNumber === topNode && !e.excluded && topNodeHat(cablePos, e.y)} {e.nodeNumber === topNode && !e.excluded && topNodeHat(cablePos, e.y)}
<circle cx={cablePos} cy={e.y} className={e.excluded ? classes.excludedNode : nodeClass(e.nodeTemp)} r="16"></circle> <circle cx={cablePos} cy={e.y} className={e.excluded ? classes.excludedNode : nodeClass(e.nodeTemp, e.nodeNumber <= topNode)} r="16"></circle>
</g> </g>
) )
} }

View file

@ -58,7 +58,7 @@ import { Bin } from "models";
import moment from "moment"; import moment from "moment";
import { GetBinShapeDescribers } from "pbHelpers/Bin"; import { GetBinShapeDescribers } from "pbHelpers/Bin";
import { pond } from "protobuf-ts/pond"; import { pond } from "protobuf-ts/pond";
import { useBinAPI, useBinYardAPI, useGlobalState } from "providers"; import { useBinAPI, useBinYardAPI, useGlobalState, useLibraCartProxyAPI } from "providers";
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
// import { useHistory } from "react-router"; // import { useHistory } from "react-router";
import { getDistanceUnit, or, getTemperatureUnit, getGrainUnit } from "utils"; import { getDistanceUnit, or, getTemperatureUnit, getGrainUnit } from "utils";
@ -176,7 +176,7 @@ export default function BinSettings(props: Props) {
const grainOptions = GrainOptions(); const grainOptions = GrainOptions();
const grainUseOptions = GetGrainUseOptions(); const grainUseOptions = GetGrainUseOptions();
const [inputCapacity, setInputCapacity] = useState<string>(""); const [inputCapacity, setInputCapacity] = useState<string>("");
const [{ as }] = useGlobalState(); const [{ user, as }] = useGlobalState();
const [grainDiff, setGrainDiff] = useState(0); const [grainDiff, setGrainDiff] = useState(0);
const [isCustomInventory, setIsCustomInventory] = useState<boolean>(false); const [isCustomInventory, setIsCustomInventory] = useState<boolean>(false);
const [grainUpdate, setGrainUpdate] = useState(false); const [grainUpdate, setGrainUpdate] = useState(false);
@ -197,10 +197,13 @@ export default function BinSettings(props: Props) {
const [inventoryControl, setInventoryControl] = useState<pond.BinInventoryControl>( const [inventoryControl, setInventoryControl] = useState<pond.BinInventoryControl>(
pond.BinInventoryControl.BIN_INVENTORY_CONTROL_UNKNOWN pond.BinInventoryControl.BIN_INVENTORY_CONTROL_UNKNOWN
); );
const [storageType, setStorageType] = useState<pond.BinStorage>( const [storageType, setStorageType] = useState<pond.BinStorage>(
pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN
); );
//libracart stuff
const libracartAPI = useLibraCartProxyAPI()
const [lcDestination, setlcDestination] = useState<Option | null>()
const [lcDestinationOptions, setlcDestinationOptions] = useState<Option[]>([])
useEffect(() => { useEffect(() => {
if (open) { if (open) {
@ -365,9 +368,33 @@ export default function BinSettings(props: Props) {
} }
}, [binYardAPI, form.yardKey, userID, as, props.binYards]); }, [binYardAPI, form.yardKey, userID, as, props.binYards]);
const loadLibraCartDestinations = useCallback(()=>{
if(inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART){
//load the destinations for the user/team they are viewing as
let options: Option[] = []
libracartAPI.listDestinations(0,0,undefined, as)
.then(resp=>{
//set the options for the search select
resp.data.destinations.forEach(d => {
let newOp: Option = {
label: d.name,
value: d.id
}
options.push(newOp)
//set the current option based on the key in the form
if (d.id === form.libracartDestinationKey){
setlcDestination(newOp)
}
})
setlcDestinationOptions(options)
}).catch(()=>{})
}
},[libracartAPI, inventoryControl, as, form.libracartDestinationKey])
useEffect(() => { useEffect(() => {
loadBinYards(); loadBinYards();
}, [loadBinYards]); loadLibraCartDestinations();
}, [loadBinYards, loadLibraCartDestinations]);
useEffect(() => { useEffect(() => {
if (bin?.settings.specs?.bushelCapacity) { if (bin?.settings.specs?.bushelCapacity) {
@ -458,6 +485,7 @@ export default function BinSettings(props: Props) {
form.inventory.inventoryControl = inventoryControl form.inventory.inventoryControl = inventoryControl
form.inventory.autoThreshold = autoFillThreshold form.inventory.autoThreshold = autoFillThreshold
} }
console.log(form)
binAPI binAPI
.updateBin(bin.key(), form, as) .updateBin(bin.key(), form, as)
.then(response => { .then(response => {
@ -713,6 +741,8 @@ export default function BinSettings(props: Props) {
return "Auto (cable)" return "Auto (cable)"
case pond.BinInventoryControl.BIN_INVENTORY_CONTROL_HYBRID_LIDAR: case pond.BinInventoryControl.BIN_INVENTORY_CONTROL_HYBRID_LIDAR:
return "Hybrid (lidar)" return "Hybrid (lidar)"
case pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART:
return "Auto (Libra Cart)"
default: default:
return "Manual" return "Manual"
} }
@ -860,7 +890,7 @@ export default function BinSettings(props: Props) {
<FormControlLabel <FormControlLabel
value={pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC} value={pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC}
control={<Radio />} control={<Radio />}
label={"Auto"} label={"Auto (Cable)"}
/> />
<FormControlLabel <FormControlLabel
value={pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR} value={pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR}
@ -872,6 +902,13 @@ export default function BinSettings(props: Props) {
control={<Radio />} control={<Radio />}
label={"Hybrid (Lidar)"} label={"Hybrid (Lidar)"}
/> />
{user.hasFeature("libra-cart") &&
<FormControlLabel
value={pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART}
control={<Radio />}
label={"Auto (Libra Cart)"}
/>
}
</RadioGroup> </RadioGroup>
</AccordionDetails> </AccordionDetails>
{inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC && {inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC &&
@ -912,6 +949,25 @@ export default function BinSettings(props: Props) {
/> />
</Box> </Box>
} }
{inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART &&
<Box width="100%" padding={2}>
<SearchSelect
label="Libra Cart Destination"
selected={lcDestination}
changeSelection={option => {
let newForm = form;
newForm.libracartDestinationKey = option?.value;
setForm(newForm);
setlcDestination(option ? option : null);
}}
disabled={!canEdit}
options={lcDestinationOptions}
/>
<Typography variant="caption">
The linked bins inventory will be adjusted When the Libra Cart Destination data is synced every 6 hours
</Typography>
</Box>
}
</Accordion> </Accordion>
{empty ? ( {empty ? (
<Box marginTop={3} display="flex" flexDirection="column" alignItems="center"> <Box marginTop={3} display="flex" flexDirection="column" alignItems="center">

View file

@ -21,7 +21,7 @@ import { Bin, Component } from "models";
import { GrainCable } from "models/GrainCable"; import { GrainCable } from "models/GrainCable";
import { UnitMeasurement } from "models/UnitMeasurement"; import { UnitMeasurement } from "models/UnitMeasurement";
import Co2Icon from "products/CommonIcons/co2Icon"; import Co2Icon from "products/CommonIcons/co2Icon";
import { pond } from "protobuf-ts/pond"; import { pond, quack } from "protobuf-ts/pond";
import { useBinAPI, useGlobalState, useSnackbar } from "providers"; import { useBinAPI, useGlobalState, useSnackbar } from "providers";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { avg, getTemperatureUnit } from "utils"; import { avg, getTemperatureUnit } from "utils";
@ -200,33 +200,32 @@ export default function BinStorageConditions(props: Props) {
// } // }
}); });
let nodeEMCs: number[] = []; if(cable.subType() !== quack.GrainCableSubtype.GRAIN_CABLE_SUBTYPE_OPI_TEMP){
let nodeEMCs: number[] = [];
filteredNodes.moistures.forEach((emc, i) => { filteredNodes.moistures.forEach((emc, i) => {
if (bin.settings.inventory?.targetMoisture) { if (bin.settings.inventory?.targetMoisture) {
// if (i < cable.topNode) { nodeEMCs.push(emc);
nodeEMCs.push(emc);
emcCounts.total++; emcCounts.total++;
if ( if (
emc > emc >
bin.settings.inventory.targetMoisture + bin.settings.inventory.targetMoisture +
bin.settings.inventory.moistureTargetDeviation bin.settings.inventory.moistureTargetDeviation
) { ) {
emcCounts.above++; emcCounts.above++;
} else if ( } else if (
emc < emc <
bin.settings.inventory.targetMoisture - bin.settings.inventory.targetMoisture -
bin.settings.inventory.moistureTargetDeviation bin.settings.inventory.moistureTargetDeviation
) { ) {
emcCounts.below++; emcCounts.below++;
} else { } else {
emcCounts.onTarget++; emcCounts.onTarget++;
} }
// } }
} });
}); cableEMCAvgs.push(avg(nodeEMCs));
}
cableTempAvgs.push(avg(nodeTemps)); cableTempAvgs.push(avg(nodeTemps));
cableEMCAvgs.push(avg(nodeEMCs));
} }
}); });
if (cableTempAvgs.length > 0) { if (cableTempAvgs.length > 0) {

View file

@ -250,6 +250,7 @@ export default function BinVisualizer(props: Props) {
const [cfmHighOpen, setCFMHighOpen] = useState(false); const [cfmHighOpen, setCFMHighOpen] = useState(false);
const [{ user }] = useGlobalState(); const [{ user }] = useGlobalState();
const [newPreset, setNewPreset] = useState<pond.BinMode>(pond.BinMode.BIN_MODE_NONE); const [newPreset, setNewPreset] = useState<pond.BinMode>(pond.BinMode.BIN_MODE_NONE);
const [newBinMode, setNewBinMode] = useState<pond.BinMode>(pond.BinMode.BIN_MODE_NONE);
const [selectedCable, setSelectedCable] = useState<GrainCable>(); const [selectedCable, setSelectedCable] = useState<GrainCable>();
const [openNodeDialog, setOpenNodeDialog] = useState(false); const [openNodeDialog, setOpenNodeDialog] = useState(false);
const [cableDevice, setCableDevice] = useState<Device | undefined>(); const [cableDevice, setCableDevice] = useState<Device | undefined>();
@ -345,6 +346,7 @@ export default function BinVisualizer(props: Props) {
setGrainOption(ToGrainOption(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()); setGrainSubtype(bin.subtype());
setNewBinMode(bin.settings.mode);
} }
if (bin.settings) { if (bin.settings) {
let t = bin.settings.outdoorTemp; let t = bin.settings.outdoorTemp;
@ -384,8 +386,11 @@ export default function BinVisualizer(props: Props) {
//also reverse it so that the node 1 (end of the cable) is in the first position //also reverse it so that the node 1 (end of the cable) is in the first position
let filteredNodes = cable.filteredNodes(true) let filteredNodes = cable.filteredNodes(true)
temps.push(...filteredNodes.temps) temps.push(...filteredNodes.temps)
humids.push(...filteredNodes.humids) //only push to the humidity values if the cable reads humidities
emcs.push(...filteredNodes.moistures) if(cable.subType() !== quack.GrainCableSubtype.GRAIN_CABLE_SUBTYPE_OPI_TEMP){
humids.push(...filteredNodes.humids)
emcs.push(...filteredNodes.moistures)
}
// let tempClone = cloneDeep(cable.temperatures).reverse(); // let tempClone = cloneDeep(cable.temperatures).reverse();
// let humClone = cloneDeep(cable.humidities).reverse(); // let humClone = cloneDeep(cable.humidities).reverse();
// let emcClone = cloneDeep(cable.grainMoistures).reverse(); // let emcClone = cloneDeep(cable.grainMoistures).reverse();
@ -409,14 +414,15 @@ export default function BinVisualizer(props: Props) {
//if the lowest temp for this cable is less than the temp in the low node conditions or the low node conditions are not set //if the lowest temp for this cable is less than the temp in the low node conditions or the low node conditions are not set
//use this cables lowest node and trend data in the condition //use this cables lowest node and trend data in the condition
if (!lowNodeConditions || Math.min(...temps) < lowNodeConditions.tempC) { if (!lowNodeConditions || Math.min(...filteredNodes.temps) < lowNodeConditions.tempC) {
//determine which node is the coldest so that the data displayed is for the same node //determine which node is the coldest so that the data displayed is for the same node
let lowTempIndex = let lowTempIndex =
temps.indexOf(Math.min(...temps)) === -1 ? 0 : temps.indexOf(Math.min(...temps)); filteredNodes.temps.indexOf(Math.min(...filteredNodes.temps)) === -1 ? 0 : filteredNodes.temps.indexOf(Math.min(...filteredNodes.temps));
console.log(lowTempIndex)
lowNodeConditions = { lowNodeConditions = {
tempC: temps[lowTempIndex], tempC: filteredNodes.temps[lowTempIndex],
humidity: humids[lowTempIndex], humidity: filteredNodes.humids[lowTempIndex],
emc: emcs[lowTempIndex], emc: filteredNodes.moistures[lowTempIndex],
tempCTrend: cableTrendData.coldNodeTrend?.tempTrend ?? 0, tempCTrend: cableTrendData.coldNodeTrend?.tempTrend ?? 0,
humidityTrend: cableTrendData.coldNodeTrend?.humidityTrend ?? 0, humidityTrend: cableTrendData.coldNodeTrend?.humidityTrend ?? 0,
emcTrend: cableTrendData.coldNodeTrend?.emcTrend ?? 0 emcTrend: cableTrendData.coldNodeTrend?.emcTrend ?? 0
@ -424,13 +430,13 @@ export default function BinVisualizer(props: Props) {
} }
//do the same for the high node //do the same for the high node
if (!highNodeConditions || cable.maxTemp() > highNodeConditions.tempC) { if (!highNodeConditions || Math.max(...filteredNodes.temps) > highNodeConditions.tempC) {
let highTempIndex = let highTempIndex =
temps.indexOf(Math.max(...temps)) === -1 ? 0 : temps.indexOf(Math.max(...temps)); filteredNodes.temps.indexOf(Math.max(...filteredNodes.temps)) === -1 ? 0 : filteredNodes.temps.indexOf(Math.max(...filteredNodes.temps));
highNodeConditions = { highNodeConditions = {
tempC: temps[highTempIndex], tempC: filteredNodes.temps[highTempIndex],
humidity: humids[highTempIndex], humidity: filteredNodes.humids[highTempIndex],
emc: emcs[highTempIndex], emc: filteredNodes.moistures[highTempIndex],
tempCTrend: cableTrendData.hotNodeTrend?.tempTrend ?? 0, tempCTrend: cableTrendData.hotNodeTrend?.tempTrend ?? 0,
humidityTrend: cableTrendData.hotNodeTrend?.humidityTrend ?? 0, humidityTrend: cableTrendData.hotNodeTrend?.humidityTrend ?? 0,
emcTrend: cableTrendData.hotNodeTrend?.emcTrend ?? 0 emcTrend: cableTrendData.hotNodeTrend?.emcTrend ?? 0
@ -1967,6 +1973,7 @@ export default function BinVisualizer(props: Props) {
const setModeStorage = () => { const setModeStorage = () => {
setNewPreset(pond.BinMode.BIN_MODE_STORAGE); setNewPreset(pond.BinMode.BIN_MODE_STORAGE);
setNewBinMode(pond.BinMode.BIN_MODE_STORAGE);
}; };
const setModeCooldown = () => { const setModeCooldown = () => {
@ -1974,6 +1981,7 @@ export default function BinVisualizer(props: Props) {
return; return;
} }
setNewPreset(pond.BinMode.BIN_MODE_COOLDOWN); setNewPreset(pond.BinMode.BIN_MODE_COOLDOWN);
setNewBinMode(pond.BinMode.BIN_MODE_COOLDOWN);
}; };
const setModeDrying = () => { const setModeDrying = () => {
@ -1982,13 +1990,16 @@ export default function BinVisualizer(props: Props) {
bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture
) { ) {
setNewPreset(pond.BinMode.BIN_MODE_HYDRATING); setNewPreset(pond.BinMode.BIN_MODE_HYDRATING);
setNewBinMode(pond.BinMode.BIN_MODE_HYDRATING);
} else { } else {
setNewPreset(pond.BinMode.BIN_MODE_DRYING); setNewPreset(pond.BinMode.BIN_MODE_DRYING);
setNewBinMode(pond.BinMode.BIN_MODE_DRYING);
} }
}; };
const closeMoistureDialog = () => { const closeMoistureDialog = () => {
setNewPreset(0); setNewPreset(0);
setNewBinMode(bin.settings.mode);
setShowInputMoisture(false); setShowInputMoisture(false);
}; };
@ -2049,6 +2060,7 @@ export default function BinVisualizer(props: Props) {
<Button <Button
onClick={() => { onClick={() => {
setNewPreset(0); setNewPreset(0);
setNewBinMode(bin.settings.mode);
setShowInputMoisture(false); setShowInputMoisture(false);
}} }}
color="primary"> color="primary">
@ -2090,7 +2102,13 @@ export default function BinVisualizer(props: Props) {
} }
if (b.settings.outdoorHumidity !== undefined) if (b.settings.outdoorHumidity !== undefined)
b.settings.outdoorHumidity = Number(outdoorHumidityInput); b.settings.outdoorHumidity = Number(outdoorHumidityInput);
b.settings.mode = newPreset;
if (b.settings.mode != newBinMode){
if(b.settings.inventory){
b.settings.inventory.initialTimestamp = moment().format();
}
b.settings.mode = newBinMode;
}
binAPI.updateBin(bin.key(), b.settings, as).then(resp => { binAPI.updateBin(bin.key(), b.settings, as).then(resp => {
refresh(); refresh();

View file

@ -30,7 +30,6 @@ const useStyles = makeStyles((_theme) => {
position: "relative", position: "relative",
minHeight: "233px", minHeight: "233px",
height: "auto !important", height: "auto !important",
width: "184px",
padding: 2 padding: 2
}, },
hidden: { hidden: {
@ -113,7 +112,7 @@ export default function BinsList(props: Props) {
return ( return (
<Grid container direction="row"> <Grid container direction="row">
{bins.map((b, i) => {bins.map((b, i) =>
isMobile ? ( (
<Grid <Grid
size={{ size={{
xs: 6, xs: 6,
@ -133,21 +132,6 @@ export default function BinsList(props: Props) {
valDisplay={valDisplay} valDisplay={valDisplay}
/> />
</Grid> </Grid>
) : (
<Box
key={i}
style={{ width: "184px" }}
className={classes.gridListTile}
onClick={() => {
!duplicate && goToBin(i)
}}>
<BinCardV2
bin={b}
duplicateBin={duplicateBin}
dupHovered={setDuplicate}
valDisplay={valDisplay}
/>
</Box>
) )
)} )}
</Grid> </Grid>

View file

@ -184,7 +184,7 @@ export default function BinComponentGraph(props: Props) {
<UnitMeasurementSummary <UnitMeasurementSummary
component={component} component={component}
reading={UnitMeasurement.convertLastMeasurement( reading={UnitMeasurement.convertLastMeasurement(
component.status.lastGoodMeasurement.map(m => UnitMeasurement.create(m, user)) lastMeasurement.map(m => UnitMeasurement.create(m, user))
)} )}
/> />
} }

View file

@ -0,0 +1,67 @@
import { useTheme } from "@mui/material";
import MaterialChartTooltip from "charts/MaterialChartTooltip";
import moment from "moment";
import { Area, AreaChart, ResponsiveContainer, Tooltip, TooltipProps, XAxis, YAxis } from "recharts";
export interface LevelAreaData {
timestamp: number;
value: number;
}
interface Props {
data: LevelAreaData[]
fill: string
customHeight?: string | number
}
export default function BinLevelAreaGraph(props: Props) {
const { data, customHeight, fill } = props
const theme = useTheme();
const now = moment();
return (
<ResponsiveContainer width={"100%"} height={customHeight ?? 350}>
<AreaChart data={data}>
<XAxis
allowDataOverflow
dataKey="timestamp"
domain={["dataMin", "dataMax"]}
name="Time"
tickFormatter={timestamp => {
let t = moment(timestamp);
return now.isSame(t, "day") ? t.format("LT") : t.format("MMM DD");
}}
scale="time"
type="number"
tick={{ fill: theme.palette.text.primary }}
stroke={theme.palette.divider}
interval="preserveStartEnd"
/>
<YAxis />
<Area
dataKey={"value"}
fill={fill}
stroke={fill}
strokeWidth={3}
type="monotone"
/>
<Tooltip
animationEasing="ease-out"
cursor={{ fill: theme.palette.text.primary, opacity: "0.15" }}
labelFormatter={timestamp => moment(timestamp).format("lll")}
content={(props: TooltipProps<any, any>) => {
return (
<MaterialChartTooltip
{...props}
valueFormatter={value => {
return value + " Bushels";
}}
/>
);
}}
/>
</AreaChart>
</ResponsiveContainer>
)
}

View file

@ -1,21 +1,18 @@
import { import {
Box,
Card, Card,
CircularProgress, CircularProgress,
FormControlLabel,
Grid,
Switch,
Typography Typography
} from "@mui/material"; } from "@mui/material";
import { blue, grey, red, yellow, orange } from "@mui/material/colors"; import { blue, grey, red, yellow, orange } from "@mui/material/colors";
import BarGraph, { BarData, RefArea } from "charts/BarGraph"; import BarGraph, { BarData } from "charts/BarGraph";
import { Bin } from "models"; import { Bin } from "models";
import moment, { Moment } from "moment"; import moment, { Moment } from "moment";
import { pond, quack } from "protobuf-ts/pond"; import { pond } from "protobuf-ts/pond";
import { useBinAPI, useGlobalState } from "providers"; import { useBinAPI, useGlobalState } from "providers";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { Legend } from "recharts"; import { Legend } from "recharts";
import { getGrainUnit } from "utils"; import { getGrainUnit } from "utils";
import BinLevelAreaGraph from "./BinLevelAreaGraph";
interface Props { interface Props {
binLoading: boolean; binLoading: boolean;
@ -27,12 +24,16 @@ interface Props {
customHeight?: number | string; customHeight?: number | string;
} }
interface InventoryAt {
timestamp: number;
value: number;
}
export default function BinLevelOverTime(props: Props) { export default function BinLevelOverTime(props: Props) {
const { bin, fertilizerBin, colour, startDate, endDate, customHeight } = props; const { bin, fertilizerBin, colour, startDate, endDate, customHeight } = props;
const binAPI = useBinAPI(); const binAPI = useBinAPI();
const [{as}] = useGlobalState(); const [{as}] = useGlobalState();
const [data, setData] = useState<BarData[]>([]); const [inventoryData, setInventoryData] = useState<InventoryAt[]>([]);
const [modeAreas, setModeAreas] = useState<RefArea[]>([]);
const [dataLoading, setDataLoading] = useState(false); const [dataLoading, setDataLoading] = useState(false);
const [capacity, setCapacity] = useState<number | undefined>(); const [capacity, setCapacity] = useState<number | undefined>();
@ -76,33 +77,15 @@ export default function BinLevelOverTime(props: Props) {
.listHistoryBetween(bin.key(), 500, startDate.toISOString(), endDate.toISOString()) .listHistoryBetween(bin.key(), 500, startDate.toISOString(), endDate.toISOString())
.then(resp => { .then(resp => {
let data: BarData[] = []; let data: BarData[] = [];
let modeAreas: RefArea[] = [];
let lastBushels = -1; let lastBushels = -1;
let currentMode: pond.BinMode | undefined = undefined
//values for ref areas let modeData: BarData[] = []
let x1 = 0;
let x2 = 0;
let y1 = -50;
let y2 = -200;
let currentMode: pond.BinMode;
resp.data.history.forEach(hist => { resp.data.history.forEach(hist => {
//build the data for the inventory bar graph
if (hist.settings?.inventory) { if (hist.settings?.inventory) {
let settings = pond.BinSettings.fromObject(hist.settings);
let bushels = hist.settings.inventory.grainBushels ?? 0; let bushels = hist.settings.inventory.grainBushels ?? 0;
if (bushels !== lastBushels || currentMode !== settings.mode) { if (bushels !== lastBushels) {
x1 = moment(hist.timestamp).valueOf(); let newData: InventoryAt = {
x2 = moment(hist.timestamp).valueOf();
modeAreas.push({
x1: x1,
x2: x2,
y1: cap ? cap * -0.1 : y1,
y2: cap ? cap * -0.2 : y2,
fill: getFill(settings.mode)
});
currentMode = settings.mode;
let newData: BarData = {
timestamp: moment(hist.timestamp).valueOf(), timestamp: moment(hist.timestamp).valueOf(),
value: fertilizerBin value: fertilizerBin
? Math.round(bushels * 35.239) ? Math.round(bushels * 35.239)
@ -114,10 +97,21 @@ export default function BinLevelOverTime(props: Props) {
lastBushels = bushels; lastBushels = bushels;
} }
} }
//build the data for the mode change bar graph
let histBin = Bin.create()
histBin.settings = hist.settings ?? pond.BinSettings.create()
if(hist.settings && currentMode !== hist.settings.mode){
currentMode = hist.settings.mode
modeData.push({
timestamp: moment(hist.timestamp).valueOf(),
value: 1,
fill: getFill(currentMode)
})
}
}); });
let currentTime = moment().valueOf();
if (data.length === 0) { if (data.length === 0) {
let bushels = bin.settings.inventory?.grainBushels ?? 0; let bushels = bin.bushels();
let currentTime = moment().valueOf();
data.push({ data.push({
value: fertilizerBin value: fertilizerBin
? Math.round(bushels * 35.239) ? Math.round(bushels * 35.239)
@ -126,17 +120,16 @@ export default function BinLevelOverTime(props: Props) {
: bushels, : bushels,
timestamp: currentTime timestamp: currentTime
}); });
modeAreas.push({
x1: currentTime,
x2: currentTime,
y1: cap ? cap * -0.1 : y1,
y2: cap ? cap * -0.2 : y2,
fill: getFill(bin.settings.mode)
});
} }
setData(data); if(modeData.length === 0){
setModeAreas(modeAreas); modeData.push({
timestamp: currentTime,
value: 1,
fill: getFill(bin.settings.mode)
})
}
setInventoryData(data);
setModeData(modeData)
}) })
.finally(() => { .finally(() => {
setDataLoading(false); setDataLoading(false);
@ -180,7 +173,7 @@ export default function BinLevelOverTime(props: Props) {
}); });
}); });
if (autoBarData.length === 0) { if (autoBarData.length === 0) {
let bushels = bin.settings.inventory?.grainBushels ?? 0; let bushels = bin.bushels();
let currentTime = moment().valueOf(); let currentTime = moment().valueOf();
autoBarData.push({ autoBarData.push({
value: fertilizerBin value: fertilizerBin
@ -191,7 +184,7 @@ export default function BinLevelOverTime(props: Props) {
timestamp: currentTime timestamp: currentTime
}); });
} }
setData(autoBarData); setInventoryData(autoBarData);
}) })
.catch(err => {}) .catch(err => {})
.finally(() => { .finally(() => {
@ -220,6 +213,13 @@ export default function BinLevelOverTime(props: Props) {
}) })
} }
}) })
if(modeData.length === 0){
modeData.push({
timestamp: moment().valueOf(),
value: 1,
fill: getFill(bin.settings.mode)
})
}
setModeData(modeData) setModeData(modeData)
}) })
.catch(err => { .catch(err => {
@ -238,7 +238,8 @@ export default function BinLevelOverTime(props: Props) {
let control = bin.inventoryControl() let control = bin.inventoryControl()
//for automatic lidar and cables get the data from measurements as an object measurement //for automatic lidar and cables get the data from measurements as an object measurement
if(control === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR || if(control === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR ||
control === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC control === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC ||
control === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART
){ ){
//load the measurement data for the bar graph //load the measurement data for the bar graph
loadMeasurementData() loadMeasurementData()
@ -270,23 +271,30 @@ export default function BinLevelOverTime(props: Props) {
}; };
const inventoryChart = () => { const inventoryChart = () => {
return ( if(inventoryData.length > 10){
<BarGraph return (
<BinLevelAreaGraph
data={inventoryData}
fill={colour}
/>
)
}else{
return (
<BarGraph
customHeight={customHeight} customHeight={customHeight}
data={data} data={inventoryData}
refAreas={modeAreas}
barColour={colour} barColour={colour}
yMax={capacity} yMax={capacity}
yMin={0} yMin={0}
graphLegend={modeAreas.length > 0 ? legend() : undefined}
labelColour="white" labelColour="white"
labels labels
useGradient useGradient
/> />
); );
}
}; };
const autoBinModeChart = () => { const binModeChart = () => {
return ( return (
<BarGraph <BarGraph
data={modeData} data={modeData}
@ -305,9 +313,7 @@ export default function BinLevelOverTime(props: Props) {
Grain Levels Over Time Grain Levels Over Time
</Typography> </Typography>
{dataLoading ? <CircularProgress /> : inventoryChart()} {dataLoading ? <CircularProgress /> : inventoryChart()}
{/* when using auto will need to make a new chart just for the bin modes since the auto inventory comes from another place */} {binModeChart()}
{(bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR ||
bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC) && autoBinModeChart()}
</Card> </Card>
); );
} }

File diff suppressed because one or more lines are too long

View file

@ -59,7 +59,7 @@ const useStyles = makeStyles((theme: Theme) => {
}, },
stickyHeader: { stickyHeader: {
position: "sticky", position: "sticky",
// backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(40, 40, 40)", backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(40, 40, 40)",
top: 0, top: 0,
zIndex: 300 //giving a really high z-index to make sure nothing is in front of it zIndex: 300 //giving a really high z-index to make sure nothing is in front of it
} }

View file

@ -28,6 +28,13 @@ export default function PeriodSelect(props: Props) {
} }
}, [unit, prevUnit, onChange, value]); }, [unit, prevUnit, onChange, value]);
useEffect(()=>{
let initialUnit: TimeUnit = bestUnit(initialMs);
let initialValue = milliToX(initialMs, initialUnit).toString();
setValue(initialValue)
setUnit(initialUnit)
},[initialMs])
const changeValue = (event: any) => { const changeValue = (event: any) => {
let value = event.target.value; let value = event.target.value;
setValue(value); setValue(value);

View file

@ -699,6 +699,86 @@ export default function ComponentForm(props: Props) {
); );
}; };
const componentMode = () => {
const { component, coefficient, offset } = form;
return (
<React.Fragment>
<Grid size={{ xs: 4, sm: 3 }}>
<Tooltip
title="Switch to be able to customize the mode of the component"
placement="bottom">
<FormControlLabel
control={
<Switch
checked={component.settings.calibrate}
onChange={toggleCalibrate}
name="mode"
aria-label="mode"
color="secondary"
/>
}
label="Mode"
labelPlacement="top"
className={classes.switchControl}
disabled={!canEdit}
/>
</Tooltip>
</Grid>
<Grid size={{ xs: 10, sm: 4 }}>
{compMode.mode.entryA && (
<TextField
id={compMode.mode.labelA}
label={compMode.mode.labelA}
disabled={!component.settings.calibrate}
error={!isCoefficientValid()}
value={coefficient}
onChange={changeCoefficient}
margin="normal"
fullWidth
select={compMode.mode.entryA === "select"}
type={compMode.mode.entryA}
variant="outlined">
{compMode.mode.entryA === "select" &&
compMode.mode.dictionaryA.map((elem: any) => {
if (!elem.restricted || (elem.restricted && user.hasAdmin())) {
return (
<MenuItem key={elem.key} value={elem.value}>
{elem.key}
</MenuItem>
);
}
return undefined; //TODO-CS: Time permitting re-think the structure of this function so that I don't have to return undefined
})}
</TextField>
)}
</Grid>
<Grid size={{ xs: 10, sm: 5 }}>
{compMode.mode.entryB && (
<TextField
id={compMode.mode.labelB}
label={compMode.mode.labelB}
disabled={!component.settings.calibrate}
error={!isOffsetValid(compMode.mode.minB, compMode.mode.maxB)}
value={offset}
onChange={changeOffset}
onFocus={selectText}
margin="normal"
fullWidth
type={compMode.mode.entryB}
variant="outlined">
{compMode.mode.entryB === "select" &&
compMode.mode.dictionaryB.map((elem: any) => (
<MenuItem key={elem.key} value={elem.value}>
{elem.key}
</MenuItem>
))}
</TextField>
)}
</Grid>
</React.Fragment>
)
}
const describeSource = (measurementType: quack.MeasurementType): MeasurementDescriber => { const describeSource = (measurementType: quack.MeasurementType): MeasurementDescriber => {
const { component } = form; const { component } = form;
return describeMeasurement(measurementType, component.type(), component.subType()); return describeMeasurement(measurementType, component.type(), component.subType());
@ -1046,87 +1126,17 @@ export default function ComponentForm(props: Props) {
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
} }
{!compMode ? ( {device.featureSupported("individualCalibrations") ? (
device.featureSupported("individualCalibrations") ? (
individualCalibration()
) : (
blanketCalibration()
)
) : (
<React.Fragment> <React.Fragment>
<Grid size={{ xs: 4, sm: 3 }}> {compMode && componentMode()}
<Tooltip {individualCalibration()}
title="Switch to be able to customize the mode of the component"
placement="bottom">
<FormControlLabel
control={
<Switch
checked={component.settings.calibrate}
onChange={toggleCalibrate}
name="mode"
aria-label="mode"
color="secondary"
/>
}
label="Mode"
labelPlacement="top"
className={classes.switchControl}
disabled={!canEdit}
/>
</Tooltip>
</Grid>
<Grid size={{ xs: 10, sm: 4 }}>
{compMode.mode.entryA && (
<TextField
id={compMode.mode.labelA}
label={compMode.mode.labelA}
disabled={!component.settings.calibrate}
error={!isCoefficientValid()}
value={coefficient}
onChange={changeCoefficient}
margin="normal"
fullWidth
select={compMode.mode.entryA === "select"}
type={compMode.mode.entryA}
variant="outlined">
{compMode.mode.entryA === "select" &&
compMode.mode.dictionaryA.map((elem: any) => {
if (!elem.restricted || (elem.restricted && user.hasAdmin())) {
return (
<MenuItem key={elem.key} value={elem.value}>
{elem.key}
</MenuItem>
);
}
return undefined; //TODO-CS: Time permitting re-think the structure of this function so that I don't have to return undefined
})}
</TextField>
)}
</Grid>
<Grid size={{ xs: 10, sm: 5 }}>
{compMode.mode.entryB && (
<TextField
id={compMode.mode.labelB}
label={compMode.mode.labelB}
disabled={!component.settings.calibrate}
error={!isOffsetValid(compMode.mode.minB, compMode.mode.maxB)}
value={offset}
onChange={changeOffset}
onFocus={selectText}
margin="normal"
fullWidth
type={compMode.mode.entryB}
variant="outlined">
{compMode.mode.entryB === "select" &&
compMode.mode.dictionaryB.map((elem: any) => (
<MenuItem key={elem.key} value={elem.value}>
{elem.key}
</MenuItem>
))}
</TextField>
)}
</Grid>
</React.Fragment> </React.Fragment>
) : (
!compMode ? (
blanketCalibration()
):(
componentMode()
)
)} )}
<TextField <TextField
id="dataAveraging" id="dataAveraging"

View file

@ -38,6 +38,45 @@
] ]
} }
}, },
{
"type": 12,
"subtype": 1,
"mode": {
"labelA": "Power Mode",
"entryA": "select",
"dictionaryA": [
{
"key": "High Power",
"value": 1,
"restricted": false
},
{
"key": "Low Power",
"value": 2,
"restricted": false
},
{
"key": "Calibration",
"value": 3,
"restricted": true
}
],
"labelB": "Number of Averages",
"entryB": "number",
"dictionaryB": [
{
"key": "min",
"value": 1,
"restricted": false
},
{
"key": "max",
"value": 8,
"restricted": false
}
]
}
},
{ {
"type": 28, "type": 28,
"subtype": 0, "subtype": 0,

View file

@ -6,7 +6,7 @@ import {
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
Grid, Grid2 as Grid,
InputAdornment, InputAdornment,
MenuItem, MenuItem,
Slider, Slider,
@ -19,7 +19,7 @@ import { DevicePreset } from "models/DevicePreset";
import { ObjectTypeString } from "objects/ObjectDescriber"; import { ObjectTypeString } from "objects/ObjectDescriber";
import { pond } from "protobuf-ts/pond"; import { pond } from "protobuf-ts/pond";
import { useSnackbar } from "providers"; import { useSnackbar } from "providers";
// import { useDevicePresetAPI } from "providers/pond/devicePresetAPI"; import { useDevicePresetAPI } from "providers/pond/devicePresetAPI";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { getTemperatureUnit } from "utils"; import { getTemperatureUnit } from "utils";
@ -43,7 +43,7 @@ export default function DevicePresetCard(props: Props) {
removeCallback, removeCallback,
updateCallback updateCallback
} = props; } = props;
// const devicePresetAPI = useDevicePresetAPI(); const devicePresetAPI = useDevicePresetAPI();
const [presetName, setPresetName] = useState(""); const [presetName, setPresetName] = useState("");
const [presetType, setPresetType] = useState<pond.PresetType>(1); const [presetType, setPresetType] = useState<pond.PresetType>(1);
const [controllerType, setControllerType] = useState<pond.ControllerType>(1); const [controllerType, setControllerType] = useState<pond.ControllerType>(1);
@ -81,22 +81,22 @@ export default function DevicePresetCard(props: Props) {
p.settings.temperature = tempC; p.settings.temperature = tempC;
p.settings.humidity = hum; p.settings.humidity = hum;
// devicePresetAPI devicePresetAPI
// .addDevicePreset(p.name, p.settings, objectKey, typestring) .addDevicePreset(p.name, p.settings, objectKey, typestring)
// .then(resp => { .then(resp => {
// //get the temp key from the preset //get the temp key from the preset
// let temp = p.key; let temp = p.key;
// //update the key in the preset with the key that came back from the add //update the key in the preset with the key that came back from the add
// p.key = resp.data.key; p.key = resp.data.key;
// //use the callback function passing in the preset and the temp key //use the callback function passing in the preset and the temp key
// if (addCallback) { if (addCallback) {
// addCallback(p, temp); addCallback(p, temp);
// } }
// openSnack("New preset saved"); openSnack("New preset saved");
// }) })
// .catch(err => { .catch(err => {
// openSnack("Failed to save new preset"); openSnack("Failed to save new preset");
// }); });
}; };
const updatePreset = () => { const updatePreset = () => {
@ -108,32 +108,32 @@ export default function DevicePresetCard(props: Props) {
newPreset.settings.temperature = tempC; newPreset.settings.temperature = tempC;
newPreset.settings.humidity = hum; newPreset.settings.humidity = hum;
// devicePresetAPI devicePresetAPI
// .updateDevicePreset(newPreset.key, newPreset.name, newPreset.settings) .updateDevicePreset(newPreset.key, newPreset.name, newPreset.settings)
// .then(resp => { .then(resp => {
// if (updateCallback) { if (updateCallback) {
// updateCallback(oldPreset, newPreset); updateCallback(oldPreset, newPreset);
// } }
// openSnack("Preset Updated"); openSnack("Preset Updated");
// }) })
// .catch(err => { .catch(err => {
// openSnack("There was a problem updating the preset"); openSnack("There was a problem updating the preset");
// }); });
}; };
const removePreset = () => { const removePreset = () => {
// devicePresetAPI devicePresetAPI
// .removeDevicePreset(preset.key) .removeDevicePreset(preset.key)
// .then(resp => { .then(resp => {
// openSnack("Preset has been deleted"); openSnack("Preset has been deleted");
// setOpenDeleteConfirmation(false); setOpenDeleteConfirmation(false);
// if (removeCallback) { if (removeCallback) {
// removeCallback(preset); removeCallback(preset);
// } }
// }) })
// .catch(err => { .catch(err => {
// openSnack("There was a problem deleting this preset"); openSnack("There was a problem deleting this preset");
// }); });
}; };
const deleteConfirmation = () => { const deleteConfirmation = () => {
@ -192,7 +192,7 @@ export default function DevicePresetCard(props: Props) {
<Card raised style={{ padding: 10, width: "100%" }}> <Card raised style={{ padding: 10, width: "100%" }}>
{deleteConfirmation()} {deleteConfirmation()}
<Grid container direction="row" alignContent="center" alignItems="center"> <Grid container direction="row" alignContent="center" alignItems="center">
<Grid item xs={12} sm={6}> <Grid size={{xs:12, sm:6}}>
<TextField <TextField
variant="outlined" variant="outlined"
margin="normal" margin="normal"
@ -203,7 +203,7 @@ export default function DevicePresetCard(props: Props) {
}} }}
/> />
</Grid> </Grid>
<Grid item xs={12} sm={6}> <Grid size={{xs:12, sm:6}}>
{presetType !== pond.PresetType.PRESET_TYPE_UNKNOWN && {presetType !== pond.PresetType.PRESET_TYPE_UNKNOWN &&
controllerType !== pond.ControllerType.CONTROLLER_TYPE_UNKNOWN && ( controllerType !== pond.ControllerType.CONTROLLER_TYPE_UNKNOWN && (
<Box paddingLeft={2}> <Box paddingLeft={2}>
@ -216,7 +216,7 @@ export default function DevicePresetCard(props: Props) {
</Box> </Box>
)} )}
</Grid> </Grid>
<Grid item xs={6}> <Grid size={6}>
{/* type */} {/* type */}
<TextField <TextField
style={{ width: "100%", marginRight: 5 }} style={{ width: "100%", marginRight: 5 }}
@ -250,7 +250,7 @@ export default function DevicePresetCard(props: Props) {
</MenuItem> </MenuItem>
</TextField> </TextField>
</Grid> </Grid>
<Grid item xs={6}> <Grid size={6}>
{/* controller type */} {/* controller type */}
<TextField <TextField
style={{ width: "100%", marginLeft: 5 }} style={{ width: "100%", marginLeft: 5 }}
@ -281,7 +281,7 @@ export default function DevicePresetCard(props: Props) {
</Grid> </Grid>
{/* temp slider */} {/* temp slider */}
{isMobile ? ( {isMobile ? (
<Grid item xs={12}> <Grid size={12}>
<Box <Box
display="flex" display="flex"
alignContent="center" alignContent="center"
@ -338,10 +338,10 @@ export default function DevicePresetCard(props: Props) {
</Grid> </Grid>
) : ( ) : (
<React.Fragment> <React.Fragment>
<Grid item xs={2}> <Grid size={2}>
<Typography>Temperature</Typography> <Typography>Temperature</Typography>
</Grid> </Grid>
<Grid item xs={7}> <Grid size={7}>
<Box paddingRight={2}> <Box paddingRight={2}>
<Slider <Slider
//orientation="vertical" //orientation="vertical"
@ -362,7 +362,7 @@ export default function DevicePresetCard(props: Props) {
/> />
</Box> </Box>
</Grid> </Grid>
<Grid item xs={3}> <Grid size={3}>
<TextField <TextField
style={{ width: "100%" }} style={{ width: "100%" }}
margin="dense" margin="dense"
@ -395,7 +395,7 @@ export default function DevicePresetCard(props: Props) {
)} )}
{/* humidity slider */} {/* humidity slider */}
{isMobile ? ( {isMobile ? (
<Grid item xs={12}> <Grid size={12}>
<Box <Box
display="flex" display="flex"
alignContent="center" alignContent="center"
@ -437,10 +437,10 @@ export default function DevicePresetCard(props: Props) {
</Grid> </Grid>
) : ( ) : (
<React.Fragment> <React.Fragment>
<Grid item xs={2}> <Grid size={2}>
<Typography>Humidity</Typography> <Typography>Humidity</Typography>
</Grid> </Grid>
<Grid item xs={7}> <Grid size={7}>
<Box paddingRight={2}> <Box paddingRight={2}>
<Slider <Slider
//orientation="vertical" //orientation="vertical"
@ -456,7 +456,7 @@ export default function DevicePresetCard(props: Props) {
/> />
</Box> </Box>
</Grid> </Grid>
<Grid item xs={3}> <Grid size={3}>
<TextField <TextField
style={{ width: "100%" }} style={{ width: "100%" }}
margin="dense" margin="dense"

View file

@ -338,14 +338,12 @@ export default function GateDevice(props: Props) {
return ( return (
<Grid <Grid
container container
direction="row" direction={(isMobile) ? "column" : "row"}
alignContent="center"
width="100%" width="100%"
//alignItems="center" alignItems="center"
> >
<Grid size={{ sm: 12, lg: isMobile || drawerView ? 12 : 4}} style={{ padding: 10 }}> <Grid size={{ sm: 12, lg: isMobile || drawerView ? 12 : 4}} style={{ padding: 10 }}>
<Box <Box
paddingLeft={1}
display="flex" display="flex"
justifyContent="space-between" justifyContent="space-between"
alignItems="center" alignItems="center"
@ -364,20 +362,6 @@ export default function GateDevice(props: Props) {
} }
]} ]}
/> />
{/* <ToggleButtonGroup value={detail} exclusive size="small" aria-label="detail">
<ToggleButton
onClick={() => setDetail("analytics")}
value={"analytics"}
aria-label="Analysis Graphs">
Analysis
</ToggleButton>
<ToggleButton
onClick={() => setDetail("sensors")}
value={"sensors"}
aria-label="Sensor Graphs">
Sensors
</ToggleButton>
</ToggleButtonGroup> */}
</Box> </Box>
<Card raised> <Card raised>
<GateSVG <GateSVG

View file

@ -258,9 +258,9 @@ export default function GateFlowGraph(props: Props) {
<React.Fragment> <React.Fragment>
{flowEvents.length > 0 ? ( {flowEvents.length > 0 ? (
<Grid container direction="row" spacing={2} className={classes.eventGrid}> <Grid container direction="row" spacing={2} className={classes.eventGrid}>
<Grid size={3}> <Grid size={{xs: 6, md: 3}}>
<Card raised className={classes.eventCard}> <Card raised className={classes.eventCard}>
<Grid container alignItems="center"> <Grid container width="100%" direction="column" alignItems="center">
<Grid size={9}> <Grid size={9}>
<Box display="flex" justifyContent="space-between"> <Box display="flex" justifyContent="space-between">
<Typography>Total Events:</Typography> <Typography>Total Events:</Typography>
@ -271,7 +271,7 @@ export default function GateFlowGraph(props: Props) {
</Grid> </Grid>
<Grid size={9}> <Grid size={9}>
<Box display="flex" justifyContent="space-between"> <Box display="flex" justifyContent="space-between">
<Typography>Total Time:</Typography> <Typography>Time:</Typography>
<Typography style={{ fontWeight: 650, fontSize: 15 }}> <Typography style={{ fontWeight: 650, fontSize: 15 }}>
{moment.duration(totalTimeS, "s").humanize()} {moment.duration(totalTimeS, "s").humanize()}
</Typography> </Typography>
@ -280,9 +280,9 @@ export default function GateFlowGraph(props: Props) {
</Grid> </Grid>
</Card> </Card>
</Grid> </Grid>
<Grid size={3}> <Grid size={{xs: 6, md: 3}}>
<Card raised className={classes.eventCard}> <Card raised className={classes.eventCard}>
<Grid container alignItems="center"> <Grid container width="100%" direction="column" alignItems="center">
<Grid size={9}> <Grid size={9}>
<Box display="flex" justifyContent="space-between"> <Box display="flex" justifyContent="space-between">
<Typography>Events Inside:</Typography> <Typography>Events Inside:</Typography>
@ -293,7 +293,7 @@ export default function GateFlowGraph(props: Props) {
</Grid> </Grid>
<Grid size={9}> <Grid size={9}>
<Box display="flex" justifyContent="space-between"> <Box display="flex" justifyContent="space-between">
<Typography>Event Time:</Typography> <Typography>Time:</Typography>
<Typography style={{ fontWeight: 650, fontSize: 15 }}> <Typography style={{ fontWeight: 650, fontSize: 15 }}>
{moment.duration(timeSInside, "s").humanize()} {moment.duration(timeSInside, "s").humanize()}
</Typography> </Typography>
@ -302,9 +302,9 @@ export default function GateFlowGraph(props: Props) {
</Grid> </Grid>
</Card> </Card>
</Grid> </Grid>
<Grid size={3}> <Grid size={{xs: 6, md: 3}}>
<Card raised className={classes.eventCard}> <Card raised className={classes.eventCard}>
<Grid container alignItems="center"> <Grid container width="100%" direction="column" alignItems="center">
<Grid size={9}> <Grid size={9}>
<Box display="flex" justifyContent="space-between"> <Box display="flex" justifyContent="space-between">
<Typography>Events Outside:</Typography> <Typography>Events Outside:</Typography>
@ -315,7 +315,7 @@ export default function GateFlowGraph(props: Props) {
</Grid> </Grid>
<Grid size={9}> <Grid size={9}>
<Box display="flex" justifyContent="space-between"> <Box display="flex" justifyContent="space-between">
<Typography>Event Time:</Typography> <Typography>Time:</Typography>
<Typography style={{ fontWeight: 650, fontSize: 15 }}> <Typography style={{ fontWeight: 650, fontSize: 15 }}>
{moment.duration(timeSOutside, "s").humanize()} {moment.duration(timeSOutside, "s").humanize()}
</Typography> </Typography>
@ -324,15 +324,15 @@ export default function GateFlowGraph(props: Props) {
</Grid> </Grid>
</Card> </Card>
</Grid> </Grid>
<Grid size={3}> <Grid size={{xs: 6, md: 3}}>
<Card raised className={classes.eventCard}> <Card raised className={classes.eventCard}>
<Grid container direction="column" alignItems="center"> <Grid container width="100%" direction="column" alignItems="center">
<Grid> <Grid>
<Typography>Performance:</Typography> <Typography>Performance:</Typography>
</Grid> </Grid>
<Grid> <Grid>
<Typography style={{ fontWeight: 650, fontSize: 15 }}> <Typography style={{ fontWeight: 650, fontSize: 15 }}>
{(eventsInside / totalEvents) * 100}% {((eventsInside / totalEvents) * 100).toFixed(2)}%
</Typography> </Typography>
</Grid> </Grid>
</Grid> </Grid>
@ -367,7 +367,7 @@ export default function GateFlowGraph(props: Props) {
<React.Fragment> <React.Fragment>
{runtime && ( {runtime && (
<Grid container direction="row" spacing={2} className={classes.runtimeGrid}> <Grid container direction="row" spacing={2} className={classes.runtimeGrid}>
<Grid size={3}> <Grid size={{xs: 6, md: 3}}>
<Card raised className={classes.calcCard}> <Card raised className={classes.calcCard}>
<Grid container width="100%" direction="column" alignItems="center"> <Grid container width="100%" direction="column" alignItems="center">
<Grid>Approximate Runtime:</Grid> <Grid>Approximate Runtime:</Grid>
@ -379,7 +379,7 @@ export default function GateFlowGraph(props: Props) {
</Grid> </Grid>
</Card> </Card>
</Grid> </Grid>
<Grid size={3}> <Grid size={{xs: 6, md: 3}}>
<Card raised className={classes.calcCard}> <Card raised className={classes.calcCard}>
<Grid container width="100%" direction="column" alignItems="center"> <Grid container width="100%" direction="column" alignItems="center">
<Grid>Cost to run PCA:</Grid> <Grid>Cost to run PCA:</Grid>
@ -394,7 +394,7 @@ export default function GateFlowGraph(props: Props) {
</Grid> </Grid>
</Card> </Card>
</Grid> </Grid>
<Grid size={3}> <Grid size={{xs: 6, md: 3}}>
<Card raised className={classes.calcCard}> <Card raised className={classes.calcCard}>
<Grid container width="100%" direction="column" alignItems="center"> <Grid container width="100%" direction="column" alignItems="center">
<Grid>Cost to run APU:</Grid> <Grid>Cost to run APU:</Grid>
@ -409,7 +409,7 @@ export default function GateFlowGraph(props: Props) {
</Grid> </Grid>
</Card> </Card>
</Grid> </Grid>
<Grid size={3}> <Grid size={{xs: 6, md: 3}}>
<Card raised className={classes.calcCard}> <Card raised className={classes.calcCard}>
<Grid container width="100%" direction="column" alignItems="center"> <Grid container width="100%" direction="column" alignItems="center">
<Grid>Total Cost:</Grid> <Grid>Total Cost:</Grid>

View file

@ -190,7 +190,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
[ [
pond.Grain.GRAIN_OATS, pond.Grain.GRAIN_OATS,
{ {
name: "Oats", name: "Oats a",
group: "Oats", group: "Oats",
equation: Equation.henderson, equation: Equation.henderson,
a: 0.000085511, a: 0.000085511,
@ -203,6 +203,40 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
weightConversionKg: 15.4222988297197, weightConversionKg: 15.4222988297197,
bushelsPerTonne: 64.842 bushelsPerTonne: 64.842
} }
],
[
pond.Grain.GRAIN_OATS_B,
{
name: "Oats b",
group: "Oats",
equation: Equation.chungPfost,
a: 442.85,
b: 0.21228,
c: 35.803,
setTempC: 32.0,
targetMC: 13.6,
img: OatImg,
colour: "#79955a",
weightConversionKg: 15.4222988297197,
bushelsPerTonne: 64.842
}
],
[
pond.Grain.GRAIN_OATS_C,
{
name: "Oats c",
group: "Oats",
equation: Equation.oswin,
a: 12.412,
b: -0.060707,
c: 2.9397,
setTempC: 32.0,
targetMC: 13.6,
img: OatImg,
colour: "#79955a",
weightConversionKg: 15.4222988297197,
bushelsPerTonne: 64.842
}
], ],
[ [
pond.Grain.GRAIN_PEANUTS, pond.Grain.GRAIN_PEANUTS,
@ -506,6 +540,66 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
weightConversionKg: 27.2155, weightConversionKg: 27.2155,
bushelsPerTonne: 36.744 bushelsPerTonne: 36.744
} }
],[
pond.Grain.GRAIN_RYE_A,
{
name: "Rye a",
group: "Rye",
equation: Equation.henderson,
a: 0.00006343,
b: 2.2060,
c: 13.1810,
setTempC: defaultSetTemp,
targetMC: 15.0,
colour: "grey",
weightConversionKg: 25.4,
bushelsPerTonne: 39.368
}
],[
pond.Grain.GRAIN_RYE_B,
{
name: "Rye b",
group: "Rye",
equation: Equation.chungPfost,
a: 461.0230,
b: 0.1840,
c: 36.7410,
setTempC: defaultSetTemp,
targetMC: 15.0,
colour: "grey",
weightConversionKg: 25.4,
bushelsPerTonne: 39.368
},
],[
pond.Grain.GRAIN_RYE_C,
{
name: "Rye c",
group: "Rye",
equation: Equation.halsey,
a: 4.2970,
b: 0.380,
c: 2.2710,
setTempC: defaultSetTemp,
targetMC: 15.0,
colour: "grey",
weightConversionKg: 25.4,
bushelsPerTonne: 39.368
},
],[
pond.Grain.GRAIN_RYE_D,
{
name: "Rye d",
group: "Rye",
equation: Equation.oswin,
a: 11.8870,
b: 0.0210,
c: 3.2620,
setTempC: defaultSetTemp,
targetMC: 15.0,
colour: "grey",
weightConversionKg: 25.4,
bushelsPerTonne: 39.368
}
] ]
]); ]);

View file

@ -0,0 +1,220 @@
import { Help } from "@mui/icons-material";
import {
Avatar,
Box,
Button,
DialogActions,
DialogContent,
DialogTitle,
Grid2 as Grid,
MenuItem,
TextField,
Tooltip,
Typography
} from "@mui/material";
import ResponsiveDialog from "common/ResponsiveDialog";
import { teamScope, User } from "models";
import { pond } from "protobuf-ts/pond";
import { useGlobalState, useSnackbar, useUserAPI } from "providers";
import { useLibraCartProxyAPI } from "providers/pond/libracartProxyAPI";
import React, { useEffect, useState } from "react";
import TeamSearch from "teams/TeamSearch";
export default function LibraCartAccess() {
const [openDialog, setOpenDialog] = useState(false);
const [teamKey, setTeamKey] = useState("");
const [teamUsers, setTeamUsers] = useState<User[]>([]);
const [primaryUser, setPrimaryUser] = useState("");
const [libracartUsername, setLibraCartUserName] = useState("");
const [libracartCode, setLibraCartCode] = useState<string | null>("");
const [libracartAuth, setLibraCartAuth] = useState<string | null>("");
//const [activeStep, setActiveStep] = useState<number>(0);
const userAPI = useUserAPI();
const libracartAPI = useLibraCartProxyAPI();
//const [dataOps, setDataOps] = useState<pond.DataOption[]>([]);
const { openSnack } = useSnackbar();
const [{ as }] = useGlobalState();
const submitNewOrganization = () => {
if (libracartCode && libracartAuth) {
libracartAPI
.addAccount(teamKey, primaryUser, libracartCode, libracartAuth, libracartUsername)
.then(resp => {
openSnack("Added New Libra Cart Account Link");
})
.catch(err => {
openSnack("Failed to Add New Libra Cart Account Link");
});
setOpenDialog(false);
}
};
useEffect(() => {
if (teamKey !== "") {
userAPI.listObjectUsers(teamScope(teamKey)).then(resp => {
setTeamUsers(resp.data.users.map((u: pond.User) => User.any(u)));
});
}
}, [teamKey, userAPI]);
// useEffect(() => {
// let code = localStorage.getItem("state");
// if (code) {
// setLibraCartCode(code);
// setOpenDialog(true);
// }
// }, [searchParams, libracartCode]);
useEffect(() => {
let code = new URLSearchParams(window.location.search).get("state"); // this is the account_code
let auth = new URLSearchParams(window.location.search).get("authorization_token"); // this is the authorization used to verify where it came from
if (code && auth) {
setLibraCartCode(code);
setLibraCartAuth(auth);
setOpenDialog(true);
}
}, [window.location]);
const validate = () => {
let invalid = false;
if (libracartUsername === "" || teamKey === "" || primaryUser === "" || libracartCode === "") {
invalid = true;
}
return invalid;
};
const general = () => {
return (
<Box>
<TextField
margin="dense"
label="Libra Cart Email"
fullWidth
variant="outlined"
value={libracartUsername}
onChange={e => setLibraCartUserName(e.target.value)}
/>
<TeamSearch label="Team" setTeamCallback={setTeamKey} />
<TextField
disabled={teamKey === ""}
margin="dense"
label="Primary User"
select
fullWidth
variant="outlined"
value={primaryUser}
onChange={e => setPrimaryUser(e.target.value)}>
{teamUsers.map(u => (
<MenuItem key={u.id()} value={u.id()}>
<Grid container direction="row" alignItems="center">
<Grid>
<Avatar
alt={u.name()}
src={
u.settings.avatar && u.settings.avatar !== "" ? u.settings.avatar : undefined
}
/>
</Grid>
<Grid>
<Box marginLeft={2}>{u.name()}</Box>
</Grid>
</Grid>
</MenuItem>
))}
</TextField>
<TextField
margin="dense"
label="code"
disabled
fullWidth
variant="outlined"
value={libracartCode}
/>
<TextField
margin="dense"
label="authorization token"
disabled
fullWidth
variant="outlined"
value={libracartAuth}
/>
</Box>
);
};
const newOrgDialog = () => {
return (
<ResponsiveDialog
open={openDialog}
onClose={() => {
setOpenDialog(false);
}}>
<DialogTitle>Enter New Integration Link</DialogTitle>
<DialogContent>{general()}</DialogContent>
<DialogActions>
<Button
onClick={() => {
setOpenDialog(false);
}}>
Close
</Button>
<Button
disabled={validate()}
onClick={submitNewOrganization}
variant="contained"
color="primary">
Submit
</Button>
</DialogActions>
</ResponsiveDialog>
);
};
return (
<Box style={{ padding: 10 }}>
<Grid container direction="row" alignContent="center" alignItems="center" wrap="nowrap" width="100%" justifyContent="space-between">
<Grid container direction="row" alignContent="center" alignItems="center" wrap="nowrap">
<Grid>
<Button
variant="contained"
color="primary"
onClick={e => {
e.preventDefault();
window.open(`https://cloud.agrimatics.com/grain/integrations`, "_blank");
}}>
Link Libra Cart Account
</Button>
</Grid>
<Grid>
<Tooltip
title='The integration can be found by selecting my account in the corner,
selecting the "Manage Account" option and then selecting integrations in the left side menu'>
<Help />
</Tooltip>
</Grid>
</Grid>
<Grid>
<Tooltip title={"This will Sync the data for all Libra Cart accounts linked to " + (as ? "this team" : "your account")}>
<Button
variant="contained"
color="primary"
onClick={e => {
libracartAPI.syncData().then(resp => {}).catch(err => {})
}}>
Sync
</Button>
</Tooltip>
</Grid>
</Grid>
<Box paddingTop={2}>
<Typography>
Libra Cart data will sync every 6 hours, if the data is needed immediately you can select the Sync button. It may take up to 15 minutes for the data to appear in our platform.
</Typography>
</Box>
{newOrgDialog()}
</Box>
);
}

View file

@ -96,7 +96,7 @@ export class Bin {
} }
public empty(): boolean { public empty(): boolean {
return this.settings.inventory?.empty || (this.settings.inventory !== undefined && this.settings.inventory !== null && this.settings.inventory.grainBushels < 5); return this.settings.inventory?.empty || (this.settings.inventory !== undefined && this.settings.inventory !== null && this.bushels() < 5);
} }
public objectType(): pond.ObjectType { public objectType(): pond.ObjectType {
@ -126,8 +126,10 @@ export class Bin {
} }
public bushels(): number { public bushels(): number {
let control = this.settings.inventory?.inventoryControl;
let bushels = this.settings.inventory?.grainBushels || 0 let bushels = this.settings.inventory?.grainBushels || 0
if (this.settings.inventory?.inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR){ if (control === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR ||
control === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART){
bushels = this.status.grainBushels bushels = this.status.grainBushels
} }
return bushels return bushels
@ -137,7 +139,8 @@ export class Bin {
let fill = 0; let fill = 0;
if (this.settings.inventory && this.settings.specs) { if (this.settings.inventory && this.settings.specs) {
let bushels = this.settings.inventory.grainBushels let bushels = this.settings.inventory.grainBushels
if (this.settings.inventory.inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR){ if (this.settings.inventory.inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR ||
this.settings.inventory.inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART){
bushels = this.status.grainBushels bushels = this.status.grainBushels
} }
fill = Math.round( fill = Math.round(

View file

@ -114,7 +114,7 @@ export class Component {
return 0; return 0;
} }
public addressDescription = (deviceProduct?: pond.DeviceProduct) => { public addressDescription(deviceProduct?: pond.DeviceProduct): string {
if ( if (
this.settings.addressType === quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY || this.settings.addressType === quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY ||
(this.settings.addressType >= quack.AddressType.ADDRESS_TYPE_PIN_OFFSET1 && (this.settings.addressType >= quack.AddressType.ADDRESS_TYPE_PIN_OFFSET1 &&

View file

@ -273,7 +273,7 @@ export class GrainCable {
let filtered: number[] = [] let filtered: number[] = []
values.forEach((val, index) => { values.forEach((val, index) => {
//if the node is not excluded AND is either under the top node OR top node is not set //if the node is not excluded AND is either under the top node OR top node is not set
if(!this.excludedNodes.includes(index) && (index <= this.topNode || this.topNode === 0)){ if(!this.excludedNodes.includes(index) && (index < this.topNode || this.topNode === 0)){
filtered.push(val) filtered.push(val)
} }
}) })

View file

@ -38,7 +38,7 @@ const Transactions = lazy(() => import("pages/Transactions"));
const BinCableEstimator = lazy(() => import("pages/BinCableEstimator")); const BinCableEstimator = lazy(() => import("pages/BinCableEstimator"));
const APIDocs = lazy(() => import("pages/APIDocs")); const APIDocs = lazy(() => import("pages/APIDocs"));
const Fields = lazy(()=> import("pages/Fields")); const Fields = lazy(()=> import("pages/Fields"));
const Marketplace = lazy(() => import("userFeatures/UserFeatures")) const Marketplace = lazy(() => import("pages/Marketplace"))
const Logs = lazy(() => import("pages/Logs")) const Logs = lazy(() => import("pages/Logs"))
const Firmware = lazy(() => import("pages/Firmware")); const Firmware = lazy(() => import("pages/Firmware"));
const Nfc = lazy(() => import("pages/Nfc")); const Nfc = lazy(() => import("pages/Nfc"));
@ -46,6 +46,7 @@ const Contracts = lazy(() => import("pages/Contracts"));
const Contract = lazy(() => import("pages/Contract")); const Contract = lazy(() => import("pages/Contract"));
const JohnDeere = lazy(() => import("pages/JohnDeere")); const JohnDeere = lazy(() => import("pages/JohnDeere"));
const CNHi = lazy(() => import("pages/CNHi")); const CNHi = lazy(() => import("pages/CNHi"));
const LibraCart = lazy(() => import("pages/LibraCart"));
export const appendToUrl = (appendage: number | string) => { export const appendToUrl = (appendage: number | string) => {
const basePath = location.pathname.replace(/\/$/, ""); const basePath = location.pathname.replace(/\/$/, "");
@ -340,6 +341,9 @@ export default function Router() {
{user.hasFeature("cnhi") && {user.hasFeature("cnhi") &&
<Route path="cnhi" element={<CNHi />} /> <Route path="cnhi" element={<CNHi />} />
} }
{user.hasFeature("libra-cart") &&
<Route path="libracart" element={<LibraCart />} />
}
{/* Map routes */} {/* Map routes */}
<Route path="visualFarm" element={<FieldMap />} /> <Route path="visualFarm" element={<FieldMap />} />
<Route path="aviationMap" element={<AviationMap />} /> <Route path="aviationMap" element={<AviationMap />} />

View file

@ -48,6 +48,7 @@ import DataDuckIcon from "products/Bindapt/DataDuckIcon";
import ContractsIcon from "products/CommonIcons/contractIcon"; import ContractsIcon from "products/CommonIcons/contractIcon";
import JohnDeereIcon from "products/CommonIcons/johnDeereIcon"; import JohnDeereIcon from "products/CommonIcons/johnDeereIcon";
import CNHiIcon from "products/CommonIcons/cnhiIcon"; import CNHiIcon from "products/CommonIcons/cnhiIcon";
import LibraCartIcon from "products/CommonIcons/libracartIcon";
const drawerWidth = 230; const drawerWidth = 230;
@ -69,19 +70,26 @@ const useStyles = makeStyles((theme: Theme) => ({
}) })
}, },
sideMenuOnClosed: { sideMenuOnClosed: {
transition: theme.transitions.create(["width", "z-index", "opacity"], { transition: theme.transitions.create(["width"], {
easing: theme.transitions.easing.sharp, easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen duration: theme.transitions.duration.leavingScreen
}), }),
overflowX: "hidden", // overflowX: "hidden",
width: theme.spacing(7), width: theme.spacing(0),
zIndex: theme.zIndex.drawer, // zIndex: theme.zIndex.drawer,
opacity: 0, // opacity: 0,
[theme.breakpoints.up("md")]: { [theme.breakpoints.up("md")]: {
width: theme.spacing(9), width: theme.spacing(9.25),
opacity: 1 opacity: 1
} }
}, },
sideMenuOnClosedMobile: {
transition: theme.transitions.create(["width"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
}),
width: theme.spacing(0),
},
list: { list: {
paddingTop: 0 paddingTop: 0
}, },
@ -436,7 +444,7 @@ export default function SideNavigator(props: Props) {
</ListItemButton> </ListItemButton>
</Tooltip> </Tooltip>
} }
{(isAg || isStreamline) && {(isAg || isStreamline || user.hasFeature("admin")) &&
<Tooltip title="Marketplace" placement="right"> <Tooltip title="Marketplace" placement="right">
<ListItemButton <ListItemButton
id="tour-marketplace" id="tour-marketplace"
@ -477,6 +485,20 @@ export default function SideNavigator(props: Props) {
{open && <ListItemText primary="Case New Holland" />} {open && <ListItemText primary="Case New Holland" />}
</ListItemButton> </ListItemButton>
</Tooltip> </Tooltip>
}
{user.hasFeature("libra-cart") &&
<Tooltip title="Libra Cart" placement="right">
<ListItemButton
id="tour-libraCart"
onClick={() => goTo("/libracart")}
classes={getClasses("/libracart")}
>
<ListItemIcon>
<LibraCartIcon />
</ListItemIcon>
{open && <ListItemText primary="Libra Cart" />}
</ListItemButton>
</Tooltip>
} }
</List> </List>
) )
@ -489,7 +511,7 @@ export default function SideNavigator(props: Props) {
classes={{ classes={{
paper: classNames( paper: classNames(
classes.sideMenu, classes.sideMenu,
open ? classes.sideMenuOpened : classes.sideMenuOnClosed open ? classes.sideMenuOpened : (isMobile ? classes.sideMenuOnClosedMobile : classes.sideMenuOnClosed)
) )
}} }}
anchor="left" anchor="left"

View file

@ -810,7 +810,8 @@ export default function Bin(props: Props) {
</Grid> </Grid>
<Grid size={{ xs: 12, sm: 12, md: 12, lg: 2.6 }}> <Grid size={{ xs: 12, sm: 12, md: 12, lg: 2.6 }}>
{(bin.settings.mode === pond.BinMode.BIN_MODE_DRYING || {(bin.settings.mode === pond.BinMode.BIN_MODE_DRYING ||
bin.settings.mode === pond.BinMode.BIN_MODE_HYDRATING) && ( bin.settings.mode === pond.BinMode.BIN_MODE_HYDRATING ||
bin.settings.mode === pond.BinMode.BIN_MODE_COOLDOWN) && (
<BinConditioningCard <BinConditioningCard
mode={bin.settings.mode} mode={bin.settings.mode}
interactions={interactions} interactions={interactions}

View file

@ -1214,37 +1214,6 @@ export default function Bins(props: Props) {
} }
]} ]}
/> />
{/* <StyledToggleButtonGroup
id="cardValueDisplay"
value={cardValDisplay}
exclusive
size="small"
aria-label="card detail">
<StyledToggle
value={"low"}
aria-label="low"
onClick={() => {
setCardValDisplay("low");
}}>
Low
</StyledToggle>
<StyledToggle
value={"average"}
aria-label="average"
onClick={() => {
setCardValDisplay("average");
}}>
Average
</StyledToggle>
<StyledToggle
value={"high"}
aria-label="high"
onClick={() => {
setCardValDisplay("high");
}}>
High
</StyledToggle>
</StyledToggleButtonGroup> */}
</Box> </Box>
</Grid> </Grid>
<Grid > <Grid >
@ -1270,41 +1239,6 @@ export default function Bins(props: Props) {
} }
]} ]}
/> />
{/* <StyledToggleButtonGroup
id="tour-graph-tabs"
value={binView}
exclusive
size="small"
aria-label="detail">
<StyledToggle
value={"grid"}
aria-label="grid view"
onClick={() => {
setBinView("grid");
sessionStorage.setItem("binsView", "grid");
}}>
<ViewComfy />
</StyledToggle> */}
{/* hidden at dustins request so that grid view and list are the only two */}
{/* <StyledToggle
value={"scroll"}
aria-label="scroll view"
onClick={() => {
setBinView("scroll");
sessionStorage.setItem("binsView", "scroll");
}}>
<ViewColumn />
</StyledToggle> */}
{/* <StyledToggle
value={"list"}
aria-label="list view"
onClick={() => {
setBinView("list");
sessionStorage.setItem("binsView", "list");
}}>
<ViewList />
</StyledToggle>
</StyledToggleButtonGroup> */}
<MoreVert <MoreVert
className={classes.icon} className={classes.icon}
onClick={event => { onClick={event => {

View file

@ -20,7 +20,7 @@ import PageContainer from "./PageContainer";
export default function CNHi() { export default function CNHi() {
const [currentOrg, setCurrentOrg] = useState(""); const [currentOrg, setCurrentOrg] = useState("");
const cnhiAPI = useCNHiProxyAPI(); const cnhiAPI = useCNHiProxyAPI();
const [organizations, setOrganizations] = useState<Map<string, pond.JDAccount>>(new Map()); const [organizations, setOrganizations] = useState<Map<string, pond.CNHiAccount>>(new Map());
const [fieldAccordion, setFieldAccordion] = useState(false); const [fieldAccordion, setFieldAccordion] = useState(false);
const [dataOptions, setDataOptions] = useState<pond.DataOption[]>([]); const [dataOptions, setDataOptions] = useState<pond.DataOption[]>([]);
const [{ as }] = useGlobalState(); const [{ as }] = useGlobalState();

View file

@ -472,7 +472,7 @@ export default function Devices() {
] ]
if (hasPlenums) { if (hasPlenums) {
columns.push({ columns.push({
title: "Plenum", title: "Temp/Humidity",
// sortKey: "hi", // sortKey: "hi",
// disableSort: true, // disableSort: true,
render: (device: Device) => { render: (device: Device) => {

134
src/pages/LibraCart.tsx Normal file
View file

@ -0,0 +1,134 @@
import {
Box,
Button,
Checkbox,
FormControlLabel,
Grid2 as Grid,
MenuItem,
Select
} from "@mui/material";
import LibraCartAccess from "integrations/LibraCart/LibraCartAccess";
import { pond } from "protobuf-ts/pond";
import { useGlobalState } from "providers";
import { useLibraCartProxyAPI } from "providers/pond/libracartProxyAPI";
import React, { useEffect, useState } from "react";
import PageContainer from "./PageContainer";
export default function LibraCart() {
const [currentOrg, setCurrentOrg] = useState("");
const libracartAPI = useLibraCartProxyAPI();
const [organizations, setOrganizations] = useState<Map<string, pond.LibraCartAccount>>(new Map());
const [dataOptions, setDataOptions] = useState<pond.DataOption[]>([]);
const [{ as }] = useGlobalState();
//load organizations for the user
useEffect(() => {
setCurrentOrg("");
libracartAPI
.listAccounts(0, 0, as)
.then(resp => {
let tempOrgs: Map<string, pond.LibraCartAccount> = new Map();
resp.data.accounts.forEach(org => {
let organization = pond.LibraCartAccount.fromObject(org);
tempOrgs.set(organization.key, organization);
});
setOrganizations(tempOrgs);
})
.catch(err => {});
}, [libracartAPI, as]);
useEffect(() => {
let organization = organizations.get(currentOrg);
if (organization) {
let currentOptions: pond.DataOption[] = organization.options ?? [];
setDataOptions(currentOptions);
}
}, [currentOrg, organizations]);
const updateOrgData = (checked: boolean, option: pond.DataOption) => {
let currentOps: pond.DataOption[] = dataOptions;
if (checked && !currentOps.includes(option)) {
currentOps.push(option);
} else if (!checked && currentOps.includes(option)) {
currentOps.splice(currentOps.indexOf(option), 1);
}
setDataOptions([...currentOps]);
};
const submit = () => {
libracartAPI.updateAccount(currentOrg, dataOptions, as ?? undefined).then(resp => {
//update the organization in the map to have the correct dataOptions
let org = organizations.get(currentOrg);
if (org) {
org.options = dataOptions;
}
});
};
const fieldOptions = () => {
return (
<React.Fragment>
<Grid container direction="row" spacing={2} alignItems="center" alignContent="center">
<Grid size={6}>
<FormControlLabel
label="Destinations"
control={
<Checkbox
checked={dataOptions.includes(pond.DataOption.DATA_OPTION_DESTINATIONS)}
onChange={(_, checked) => {
//setFields(!fields);
updateOrgData(checked, pond.DataOption.DATA_OPTION_DESTINATIONS);
}}
/>
}
/>
</Grid>
<Grid size={6}>
Import your destinations for the option to link it to a bin
</Grid>
</Grid>
</React.Fragment>
);
};
return (
<PageContainer>
<LibraCartAccess />
<Grid
container
direction="row"
justifyContent="space-between"
alignContent="center"
alignItems="center"
style={{ padding: 10 }}>
<Grid>
<Select
//style={{ maxWidth: 110 }}
title="Libra Cart Account"
displayEmpty
disableUnderline={true}
value={currentOrg}
onChange={(event: any) => {
setCurrentOrg(event.target.value);
}}>
<MenuItem key={""} value={""}>
Select account to adjust accessable data
</MenuItem>
{Array.from(organizations.values()).map(org => {
return (
<MenuItem key={org.key} value={org.key}>
{org.username}
</MenuItem>
);
})}
</Select>
</Grid>
<Grid>
<Button onClick={submit} variant="contained" color="primary">
Update
</Button>
</Grid>
</Grid>
<Box style={{ padding: 10 }}>{fieldOptions()}</Box>
</PageContainer>
);
}

View file

@ -128,7 +128,8 @@ export default function Users() {
"developer", "developer",
"marketplace", "marketplace",
"installer", "installer",
"cnhi" "cnhi",
"libra-cart"
].sort(); ].sort();
const [rows, setRows] = useState<User[]>([]) const [rows, setRows] = useState<User[]>([])

View file

@ -71,16 +71,19 @@ export function describePower(power?: pond.DevicePower | null): PowerDescriber {
} }
} else { } else {
let thresholds = notChargingThresholds; let thresholds = notChargingThresholds;
let suffix = "not charging"; // we would not actually know if it is charging here since it is just looking at the voltage now and not comparing to previous voltage
if (power.inputVoltage >= 4) { // let suffix = "not charging";
thresholds = chargingThresholds; // if (power.inputVoltage >= 4) {
if (power.chargePercent === 100) { // thresholds = chargingThresholds;
suffix = "charged"; // if (power.chargePercent === 100) {
} else { // suffix = "charged";
suffix = "charging"; // } else {
} // suffix = "charging";
} // }
result.description = power.chargePercent.toString() + "%, " + suffix; // }
// result.description = power.chargePercent.toString() + "%, " + suffix;
// TODO: there is a plan in place to add a boolean value to power in the backend to have it check and set the boolean, we can then use that to know if it is charging or not
result.description = power.chargePercent.toString() + "%"
for (let i = 0; i < thresholds.length; i++) { for (let i = 0; i < thresholds.length; i++) {
if (power.chargePercent >= thresholds[i].threshold) { if (power.chargePercent >= thresholds[i].threshold) {
result.icon = thresholds[i].icon; result.icon = thresholds[i].icon;

View file

@ -251,7 +251,8 @@ export const BindaptV2MonitorAvailability: DeviceAvailabilityMap = new Map<
// [quack.ComponentType.COMPONENT_TYPE_VAPOUR_PRESSURE_DEFICIT, [0x40]], //component type deprecated? // [quack.ComponentType.COMPONENT_TYPE_VAPOUR_PRESSURE_DEFICIT, [0x40]], //component type deprecated?
[quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]], [quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]],
[quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]], [quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]],
[quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]] [quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]],
[quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE, [0x71]]
]) ])
], ],
[quack.AddressType.ADDRESS_TYPE_POWER, [0]], [quack.AddressType.ADDRESS_TYPE_POWER, [0]],

View file

@ -0,0 +1,24 @@
// update logo images when they share official black and white
import LibraCartLogoWhite from "assets/marketplaceImages/LibraCartGrey.png";
import LibraCartLogoBlack from "assets/marketplaceImages/LibraCartGrey.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
interface Props {
type?: "light" | "dark";
}
export default function LibraCartIcon(props: Props) {
const themeType = useThemeType();
const { type } = props;
const src = () => {
if (type) {
return type === "light" ? LibraCartLogoWhite : LibraCartLogoBlack;
}
return themeType === "light" ? LibraCartLogoBlack : LibraCartLogoWhite;
};
return <ImgIcon alt="libra-cart" src={src()} />;
}

View file

@ -45,7 +45,8 @@ export {
useFileControllerAPI, useFileControllerAPI,
useContractAPI, //TODO: update api with resolve, reject useContractAPI, //TODO: update api with resolve, reject
useJohnDeereProxyAPI, //TODO: update api with resolve, reject useJohnDeereProxyAPI, //TODO: update api with resolve, reject
useCNHiProxyAPI //TODO: update api with resolve, reject useCNHiProxyAPI, //TODO: update api with resolve, reject
useLibraCartProxyAPI //TODO: update api with resolve, reject
} from "./pond/pond"; } from "./pond/pond";
// export { SecurityContext, useSecurity } from "./security"; // export { SecurityContext, useSecurity } from "./security";
export { SnackbarContext, useSnackbar } from "./Snackbar"; export { SnackbarContext, useSnackbar } from "./Snackbar";

View file

@ -0,0 +1,150 @@
import { useHTTP } from "hooks";
import { createContext, PropsWithChildren, useContext } from "react";
import { pond } from "protobuf-ts/pond";
import { pondURL } from "./pond";
import { AxiosResponse } from "axios";
import { useGlobalState } from "providers";
export interface IDevicePresetInterface {
//add
addDevicePreset: (
name: string,
settings: pond.DevicePresetSettings,
parentKey?: string,
parentTypes?: string
) => Promise<AxiosResponse<pond.AddDevicePresetResponse>>;
//list
listDevicePresets: (
limit: number,
offset: number,
order?: "asc" | "desc",
orderBy?: string,
search?: string,
keys?: string[],
types?: string[]
) => Promise<AxiosResponse<pond.ListDevicePresetResponse>>;
//update
updateDevicePreset: (
key: string,
name: string,
settings: pond.DevicePresetSettings
) => Promise<AxiosResponse<pond.UpdateDevicePresetResponse>>;
//remove
removeDevicePreset: (key: string) => Promise<AxiosResponse<pond.DeleteDevicePresetResponse>>;
//get
getDevicePreset: (key: string) => Promise<AxiosResponse<pond.GetDevicePresetResponse>>;
}
export const DevicePresetAPIcontext = createContext<IDevicePresetInterface>(
{} as IDevicePresetInterface
);
interface Props {}
export default function DevicePresetProvider(props: PropsWithChildren<Props>) {
const { children } = props;
const { get, del, post, put } = useHTTP();
const [{ as }] = useGlobalState();
//add
const addDevicePreset = (
name: string,
settings: pond.DevicePresetSettings,
parentKey?: string,
parentType?: string
) => {
let key = "";
let type = "";
if (parentKey && parentType) {
key = "&parentKey=" + parentKey;
type = "&parentType=" + parentType;
}
if (as) {
return post<pond.AddDevicePresetResponse>(
pondURL("/devicePreset?name=" + name + "&as=" + as + key + type),
settings
);
}
return post<pond.AddDevicePresetResponse>(
pondURL("/devicePreset?name=" + name + key + type),
settings
);
};
//list
const listDevicePresets = (
limit: number,
offset: number,
order?: "asc" | "desc",
orderBy?: string,
search?: string,
keys?: string[],
types?: string[]
) => {
let asText = "";
if (as) asText = "&as=" + as;
return get<pond.ListDevicePresetResponse>(
pondURL(
"/devicePresets?limit=" +
limit +
"&offset=" +
offset +
("&order=" + (order ? order : "asc")) +
("&by=" + (orderBy ? orderBy : "key")) +
(search ? "&search=" + search : "") +
(keys ? "&keys=" + keys.toString() : "") +
(types ? "&types=" + types.toString() : "") +
asText
)
);
};
//update
const updateDevicePreset = (key: string, name: string, settings: pond.DevicePresetSettings) => {
if (as) {
return put<pond.UpdateDevicePresetResponse>(
pondURL("/devicePreset/" + key + "?as=" + as + "&name=" + name),
settings
);
}
return put<pond.UpdateDevicePresetResponse>(
pondURL("/devicePreset/" + key + "?name=" + name),
settings
);
};
//remove
const removeDevicePreset = (key: string) => {
if (as) {
return del<pond.DeleteDevicePresetResponse>(pondURL("/devicePreset/" + key + "?as=" + as));
}
return del<pond.DeleteDevicePresetResponse>(pondURL("/devicePreset/" + key + "?as=" + as));
};
//get
const getDevicePreset = (key: string) => {
if (as) {
return del<pond.GetDevicePresetResponse>(pondURL("/devicePreset/" + key + "?as=" + as));
}
return del<pond.GetDevicePresetResponse>(pondURL("/devicePreset/" + key + "?as=" + as));
};
return (
<DevicePresetAPIcontext.Provider
value={{
//add
addDevicePreset,
//list
listDevicePresets,
//update
updateDevicePreset,
//remove
removeDevicePreset,
//get
getDevicePreset
}}>
{children}
</DevicePresetAPIcontext.Provider>
);
}
export const useDevicePresetAPI = () => useContext(DevicePresetAPIcontext);

View file

@ -0,0 +1,128 @@
import { AxiosResponse } from "axios";
import { useHTTP } from "hooks";
import { pond } from "protobuf-ts/pond";
import React, { createContext, PropsWithChildren, useContext } from "react";
//import { or } from "utils";
import { pondURL } from "./pond";
import { useGlobalState } from "providers/StateContainer";
export interface ILibraCartProxyAPIContext {
//add new organization
addAccount: (
teamKey: string,
userID: string,
libracartCode: string,
libracartAuth: string,
libracartUsername: string
) => Promise<AxiosResponse<pond.AddLibraCartAccountResponse>>;
//list organizations
listAccounts: (
limit: number,
offset: number,
as?: string,
keys?: string[],
types?: string[]
) => Promise<AxiosResponse<pond.ListLibraCartAccountsResponse>>;
updateAccount: (
key: string,
options: pond.DataOption[],
as?: string
) => Promise<AxiosResponse<pond.UpdateLibraCartAccountResponse>>;
listDestinations: (
limit: number,
offset: number,
libracartKey?: string,
as?: string
) => Promise<AxiosResponse<pond.ListLibraCartDestinationsResponse>>;
syncData: () => Promise<any>
}
export const LibraCartProxyAPIContext = createContext<ILibraCartProxyAPIContext>(
{} as ILibraCartProxyAPIContext
);
interface Props {}
export default function LibraCartProvider(props: PropsWithChildren<Props>) {
const { children } = props;
const [{as}] = useGlobalState();
const { post, get, put } = useHTTP();
const addAccount = (
teamKey: string,
userID: string,
libracartCode: string,
libracartAuth: string,
libracartUsername: string
) => {
return post<pond.AddLibraCartAccountResponse>(
pondURL(
"/libracartAccounts?team=" +
teamKey +
"&user=" +
userID +
"&code=" +
libracartCode +
"&auth=" +
libracartAuth +
"&libracartUsername=" +
libracartUsername
)
);
};
const listAccounts = (
limit: number,
offset: number,
as?: string,
keys?: string[],
types?: string[]
) => {
return get<pond.ListLibraCartAccountsResponse>(
pondURL(
"/libracartAccounts?limit=" +
limit +
"&offset=" +
offset +
(as ? "&as=" + as : "") +
(keys ? "&keys=" + keys.join(",") : "") +
(types ? "&types=" + types.join(",") : "")
)
);
};
const updateAccount = (key: string, options: pond.DataOption[], as?: string) => {
return put<pond.UpdateLibraCartAccountResponse>(
pondURL(
"/libracartAccounts/" + key + "?options=" + options.toString() + (as ? "&as=" + as : "")
)
);
};
const listDestinations = (limit: number, offset: number, libracartKey?: string, as?: string) => {
return get<pond.ListLibraCartDestinationsResponse>(
pondURL(
"/libracartDestinations?limit" + limit + "&offset=" + offset + (libracartKey ? "&libracartKey=" + libracartKey : "") + (as ? "&as=" + as : "")
)
)
}
const syncData = () => {
return get(pondURL("/libracartImport" + (as ? "?as=" + as : "")))
}
return (
<LibraCartProxyAPIContext.Provider
value={{
addAccount,
listAccounts,
updateAccount,
listDestinations,
syncData
}}>
{children}
</LibraCartProxyAPIContext.Provider>
);
}
export const useLibraCartProxyAPI = () => useContext(LibraCartProxyAPIContext);

View file

@ -35,7 +35,9 @@ import ContractProvider, { useContractAPI } from "./contractAPI";
import UsageProvider, { useUsageAPI } from "./usageAPI"; import UsageProvider, { useUsageAPI } from "./usageAPI";
import KeyManagerProvider, { useKeyManagerAPI } from "./keyManagerAPI"; import KeyManagerProvider, { useKeyManagerAPI } from "./keyManagerAPI";
import JohnDeereProvider, { useJohnDeereProxyAPI } from "./johnDeereProxyAPI"; import JohnDeereProvider, { useJohnDeereProxyAPI } from "./johnDeereProxyAPI";
import LibraCartProvider, { useLibraCartProxyAPI } from "./libracartProxyAPI";
import CNHiProvider, { useCNHiProxyAPI } from "./cnhiProxyAPI"; import CNHiProvider, { useCNHiProxyAPI } from "./cnhiProxyAPI";
import DevicePresetProvider, { useDevicePresetAPI } from "./devicePresetAPI";
// import NoteProvider from "providers/noteAPI"; // import NoteProvider from "providers/noteAPI";
export const pondURL = (partial: string, demo: boolean = false): string => { export const pondURL = (partial: string, demo: boolean = false): string => {
@ -86,11 +88,15 @@ export default function PondProvider(props: PropsWithChildren<any>) {
<ContractProvider> <ContractProvider>
<JohnDeereProvider> <JohnDeereProvider>
<CNHiProvider> <CNHiProvider>
<UsageProvider> <LibraCartProvider>
<KeyManagerProvider> <UsageProvider>
{children} <KeyManagerProvider>
</KeyManagerProvider> <DevicePresetProvider>
</UsageProvider> {children}
</DevicePresetProvider>
</KeyManagerProvider>
</UsageProvider>
</LibraCartProvider>
</CNHiProvider> </CNHiProvider>
</JohnDeereProvider> </JohnDeereProvider>
</ContractProvider> </ContractProvider>
@ -163,5 +169,7 @@ export {
useUsageAPI, useUsageAPI,
useKeyManagerAPI, useKeyManagerAPI,
useJohnDeereProxyAPI, useJohnDeereProxyAPI,
useCNHiProxyAPI useCNHiProxyAPI,
useLibraCartProxyAPI,
useDevicePresetAPI
}; };

View file

@ -10,7 +10,7 @@ import {
Textsms as TextIcon, Textsms as TextIcon,
Contrast, Contrast,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { AppBar, Box, Button, Checkbox, CircularProgress, Collapse, Divider, FormControl, FormControlLabel, FormGroup, FormHelperText, FormLabel, Grid2, IconButton, List, ListItem, ListItemButton, ListItemIcon, ListItemText, MenuItem, Radio, RadioGroup, TextField, Theme, ToggleButton, ToggleButtonGroup, Toolbar, Tooltip, Typography } from "@mui/material"; import { AppBar, Box, Button, Checkbox, CircularProgress, Collapse, Divider, FormControl, FormControlLabel, FormGroup, FormHelperText, FormLabel, Grid2, IconButton, List, ListItem, ListItemButton, ListItemIcon, ListItemText, MenuItem, Radio, RadioGroup, Slider, TextField, Theme, ToggleButton, ToggleButtonGroup, Toolbar, Tooltip, Typography } from "@mui/material";
import ResponsiveDialog from "common/ResponsiveDialog"; import ResponsiveDialog from "common/ResponsiveDialog";
import UserAvatar from "./UserAvatar"; import UserAvatar from "./UserAvatar";
import { useGlobalState, useSnackbar, useUserAPI } from "providers"; import { useGlobalState, useSnackbar, useUserAPI } from "providers";
@ -31,6 +31,7 @@ import UnitsIcon from "@mui/icons-material/Category";
import { setThemeType } from "theme"; import { setThemeType } from "theme";
import { IsAdaptiveAgriculture } from "services/whiteLabel"; import { IsAdaptiveAgriculture } from "services/whiteLabel";
import { setDistanceUnit, setGrainUnit, setPressureUnit, setTemperatureUnit } from "utils"; import { setDistanceUnit, setGrainUnit, setPressureUnit, setTemperatureUnit } from "utils";
import FieldsIcon from "products/AgIcons/FieldsIcon";
const useStyles = makeStyles((theme: Theme) => { const useStyles = makeStyles((theme: Theme) => {
return ({ return ({
@ -82,7 +83,9 @@ export default function UserSettings(props: Props) {
const [avatarExpanded, setAvatarExpanded] = useState<boolean>(false); const [avatarExpanded, setAvatarExpanded] = useState<boolean>(false);
const [notificationsExpanded, setNotificationsExpanded] = useState<boolean>(false); const [notificationsExpanded, setNotificationsExpanded] = useState<boolean>(false);
const [unitsExpanded, setUnitsExpanded] = useState<boolean>(false); const [unitsExpanded, setUnitsExpanded] = useState<boolean>(false);
const [mapsExpanded, setMapsExpanded] = useState<boolean>(false);
const [avatarUrl, setAvatarUrl] = useState<string>(""); const [avatarUrl, setAvatarUrl] = useState<string>("");
const [sliderVal, setSliderVal] = useState(globalState.user.settings.mapZoom);
// const [themeValue, setThemeValue] = useState<"light" | "dark" | "system">(themeMode.mode) // const [themeValue, setThemeValue] = useState<"light" | "dark" | "system">(themeMode.mode)
useEffect(() => { useEffect(() => {
@ -151,6 +154,30 @@ export default function UserSettings(props: Props) {
setUser(updatedUser); setUser(updatedUser);
}; };
const changeDefaultZoom = (value: number) => {
setSliderVal(value);
let updatedUser = User.clone(user);
updatedUser.settings.mapZoom = value;
setUser(updatedUser)
};
const mapSettings = () => {
return (
<Box display="flex">
<Typography>Map Zoom Level</Typography>
<Slider
valueLabelDisplay="auto"
value={sliderVal}
min={1}
max={16}
onChange={(_, val) => {
changeDefaultZoom(val as number);
}}
/>
</Box>
);
};
const generalSettings = () => { const generalSettings = () => {
const { name, email, phoneNumber, timezone } = user.settings; const { name, email, phoneNumber, timezone } = user.settings;
@ -631,15 +658,15 @@ export default function UserSettings(props: Props) {
</List> </List>
</Collapse> </Collapse>
{/* <Divider /> <Divider />
<ListItem button onClick={() => setMapsExpanded(!mapsExpanded)}> <ListItemButton onClick={() => setMapsExpanded(!mapsExpanded)}>
<ListItemIcon> <ListItemIcon>
<FieldsIcon /> <FieldsIcon />
</ListItemIcon> </ListItemIcon>
<ListItemText primary={"Map Settings"} /> <ListItemText primary={"Map Settings"} />
{mapsExpanded ? <ExpandLess /> : <ExpandMore />} {mapsExpanded ? <ExpandLess /> : <ExpandMore />}
</ListItem> </ListItemButton>
<Collapse in={mapsExpanded} timeout="auto" unmountOnExit> <Collapse in={mapsExpanded} timeout="auto" unmountOnExit>
<List component="div"> <List component="div">
<ListItem> <ListItem>
@ -648,7 +675,7 @@ export default function UserSettings(props: Props) {
</ListItemText> </ListItemText>
</ListItem> </ListItem>
</List> </List>
</Collapse> */} </Collapse>
</List> </List>
); );
}; };

View file

@ -5,6 +5,7 @@ import { useSnackbar, useUserAPI } from "hooks";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import JohnDeereIcon from "products/CommonIcons/johnDeereIcon"; import JohnDeereIcon from "products/CommonIcons/johnDeereIcon";
import CNHiIcon from "products/CommonIcons/cnhiIcon"; import CNHiIcon from "products/CommonIcons/cnhiIcon";
import LibraCartIcon from "products/CommonIcons/libracartIcon";
//import AgLogo from "assets/whitelabels/AdaptiveAgriculture/AGLogoSquare.png"; //import AgLogo from "assets/whitelabels/AdaptiveAgriculture/AGLogoSquare.png";
import { import {
IsAdaptiveAgriculture IsAdaptiveAgriculture
@ -60,7 +61,15 @@ const agFeatureList: ProductDetails[] = [
"Integrate with the Case New Holland Industrial Center to bring your data into our platform" "Integrate with the Case New Holland Industrial Center to bring your data into our platform"
// bulletPoints: ["bullet one", "bullet two"], // bulletPoints: ["bullet one", "bullet two"],
// questions: [], // questions: [],
} },
// {
// featureName: "libra-cart",
// featureLogo: <LibraCartIcon />,
// featureCost: 0,
// cardImage: FeatureImageTest,
// featureTitle: "Libra Cart",
// longDescription: "Integrate with Libra Cart to bring your data into our platform"
// }
]; ];
// const constructionFeatureList: ProductDetails[] = [] // const constructionFeatureList: ProductDetails[] = []
@ -80,7 +89,7 @@ export default function Marketplace() {
useEffect(() => { useEffect(() => {
let list: ProductDetails[] = []; let list: ProductDetails[] = [];
if (IsAdaptiveAgriculture()) { if (IsAdaptiveAgriculture() || user.hasFeature("admin")) {
list = list.concat(agFeatureList); list = list.concat(agFeatureList);
} }
// if(IsAdCon()){ // if(IsAdCon()){