diff --git a/nginx.conf b/nginx.conf index b390bff..1537b32 100644 --- a/nginx.conf +++ b/nginx.conf @@ -71,6 +71,12 @@ http { 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 location = /index.html { return 301 /indexV2.html; diff --git a/package-lock.json b/package-lock.json index 3a682de..21837fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10911,7 +10911,7 @@ }, "node_modules/protobuf-ts": { "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": { "protobufjs": "^6.8.8" } diff --git a/public/service-worker.js b/public/service-worker.js new file mode 100644 index 0000000..6b6a19c --- /dev/null +++ b/public/service-worker.js @@ -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)); +}); diff --git a/src/app/App.tsx b/src/app/App.tsx index 16bb781..2c6b94b 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -50,7 +50,8 @@ function App() { const skipCallbacks = [ "/johndeere", - "/cnhi" + "/cnhi", + "/libracart" ] return ( diff --git a/src/app/UserWrapper.tsx b/src/app/UserWrapper.tsx index 722705e..cb4cdf8 100644 --- a/src/app/UserWrapper.tsx +++ b/src/app/UserWrapper.tsx @@ -82,7 +82,20 @@ export default function UserWrapper(props: Props) { firmware: new Map() }) }).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(() => { setLoading(false) diff --git a/src/assets/marketplaceImages/LibraCartGrey.png b/src/assets/marketplaceImages/LibraCartGrey.png new file mode 100644 index 0000000..331ac29 Binary files /dev/null and b/src/assets/marketplaceImages/LibraCartGrey.png differ diff --git a/src/bin/BinActions.tsx b/src/bin/BinActions.tsx index 5f4d93a..a425ea6 100644 --- a/src/bin/BinActions.tsx +++ b/src/bin/BinActions.tsx @@ -24,7 +24,7 @@ import RemoveSelfFromObject from "user/RemoveSelfFromObject"; import ShareObject from "user/ShareObject"; import { isOffline } from "utils/environment"; import ObjectTeams from "teams/ObjectTeams"; -// import BinDuplication from "./BinDuplication"; +import BinDuplication from "./BinDuplication"; import BinsIcon from "products/Bindapt/BinsIcon"; import HelpIcon from "@mui/icons-material/Help"; import BinSensors from "./BinSensors"; @@ -269,14 +269,14 @@ export default function BinActions(props: Props) { closeDialogCallback={() => setOpenState({ ...openState, users: false })} refreshCallback={refreshCallback} /> - {/* { setOpenState({ ...openState, duplication: false }); }} bin={bin} refreshCallback={refreshCallback} - /> */} + /> - {user.hasFeature("admin") && ( + {user.hasFeature("installer") && ( )} - {/* - - */} { interface Props { interactions: Interaction[]; interactionDevices: Map; - mode: pond.BinMode.BIN_MODE_DRYING | pond.BinMode.BIN_MODE_HYDRATING; + mode: pond.BinMode; plenums?: Plenum[]; ambients?: Ambient[]; heaters?: Controller[]; @@ -94,10 +94,21 @@ export default function BinConditioningCard(props: Props) { 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 ( - {mode === pond.BinMode.BIN_MODE_DRYING ? "Drying" : "Hydrating"} Conditions + {modeDisplay()} Conditions {conditionsDisplay()} diff --git a/src/bin/BinConditioningInteraction.tsx b/src/bin/BinConditioningInteraction.tsx index 7f5ec13..7fe8bea 100644 --- a/src/bin/BinConditioningInteraction.tsx +++ b/src/bin/BinConditioningInteraction.tsx @@ -47,8 +47,6 @@ const useStyles = makeStyles((theme: Theme) => { sliderThumb: { height: 15, width: 15, - marginTop: -6, - marginLeft: -7.5, backgroundColor: "yellow" }, sliderTrack: { @@ -60,10 +58,10 @@ const useStyles = makeStyles((theme: Theme) => { backgroundColor: "white" }, sliderValLabel: { - left: "calc(-50%)", - top: 22, + left: -20, + top: 40, + background: "transparent", "& *": { - background: "transparent", color: "#fff" } }, diff --git a/src/bin/BinDuplication.tsx b/src/bin/BinDuplication.tsx new file mode 100644 index 0000000..113830a --- /dev/null +++ b/src/bin/BinDuplication.tsx @@ -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 ( + + Create Duplicate Bin + + + This will create a new bin with the same settings as {bin.name()}. + + setBinDupName(e.target.value)} + /> + + + + + + + ); +} diff --git a/src/bin/BinSVGV2.tsx b/src/bin/BinSVGV2.tsx index 2c7a888..f3c3454 100644 --- a/src/bin/BinSVGV2.tsx +++ b/src/bin/BinSVGV2.tsx @@ -476,11 +476,11 @@ export default function BinSVGV2(props: Props) { return cableGrainPath; }; - const nodeClass = (nodeTemp: number) => { - if (hottestNodeTemp && hottestNodeTemp.toFixed(2) === nodeTemp.toFixed(2)) { + const nodeClass = (nodeTemp: number, underTop: boolean) => { + if (hottestNodeTemp && hottestNodeTemp.toFixed(2) === nodeTemp.toFixed(2) && underTop) { 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.cableNode; @@ -507,12 +507,12 @@ export default function BinSVGV2(props: Props) { //if the cables have the same number of nodes if (b.temperatures.length === a.temperatures.length) { //sort by temp only last and any type that has humidity first - if ( - a.settings.subtype === quack.GrainCableSubtype.GRAIN_CABLE_SUBTYPE_OPI_TEMP && - b.settings.subtype !== quack.GrainCableSubtype.GRAIN_CABLE_SUBTYPE_OPI_TEMP - ) { + if ((avg(a.humidities) === 0) && (avg(b.humidities) !== 0)) { return 1; - } else { + } else if ((avg(b.humidities) === 0) && (avg(a.humidities) !== 0)) { + return -1; + } + else { return a.key() > b.key() ? 1 : -1; } } @@ -759,7 +759,7 @@ export default function BinSVGV2(props: Props) { return ( {e.nodeNumber === topNode && !e.excluded && topNodeHat(cablePos, e.y)} - + ) } diff --git a/src/bin/BinSettings.tsx b/src/bin/BinSettings.tsx index 5558230..c65bd31 100644 --- a/src/bin/BinSettings.tsx +++ b/src/bin/BinSettings.tsx @@ -58,7 +58,7 @@ import { Bin } from "models"; import moment from "moment"; import { GetBinShapeDescribers } from "pbHelpers/Bin"; 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 { useHistory } from "react-router"; import { getDistanceUnit, or, getTemperatureUnit, getGrainUnit } from "utils"; @@ -176,7 +176,7 @@ export default function BinSettings(props: Props) { const grainOptions = GrainOptions(); const grainUseOptions = GetGrainUseOptions(); const [inputCapacity, setInputCapacity] = useState(""); - const [{ as }] = useGlobalState(); + const [{ user, as }] = useGlobalState(); const [grainDiff, setGrainDiff] = useState(0); const [isCustomInventory, setIsCustomInventory] = useState(false); const [grainUpdate, setGrainUpdate] = useState(false); @@ -197,10 +197,13 @@ export default function BinSettings(props: Props) { const [inventoryControl, setInventoryControl] = useState( pond.BinInventoryControl.BIN_INVENTORY_CONTROL_UNKNOWN ); - const [storageType, setStorageType] = useState( pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN ); + //libracart stuff + const libracartAPI = useLibraCartProxyAPI() + const [lcDestination, setlcDestination] = useState } + {inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART && + + { + let newForm = form; + newForm.libracartDestinationKey = option?.value; + setForm(newForm); + setlcDestination(option ? option : null); + }} + disabled={!canEdit} + options={lcDestinationOptions} + /> + + The linked bins inventory will be adjusted When the Libra Cart Destination data is synced every 6 hours + + + } {empty ? ( diff --git a/src/bin/BinStorageConditions.tsx b/src/bin/BinStorageConditions.tsx index 0f4daac..fa0e957 100644 --- a/src/bin/BinStorageConditions.tsx +++ b/src/bin/BinStorageConditions.tsx @@ -21,7 +21,7 @@ import { Bin, Component } from "models"; import { GrainCable } from "models/GrainCable"; import { UnitMeasurement } from "models/UnitMeasurement"; 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 React, { useEffect, useState } from "react"; import { avg, getTemperatureUnit } from "utils"; @@ -200,33 +200,32 @@ export default function BinStorageConditions(props: Props) { // } }); - let nodeEMCs: number[] = []; - - filteredNodes.moistures.forEach((emc, i) => { + if(cable.subType() !== quack.GrainCableSubtype.GRAIN_CABLE_SUBTYPE_OPI_TEMP){ + let nodeEMCs: number[] = []; + filteredNodes.moistures.forEach((emc, i) => { if (bin.settings.inventory?.targetMoisture) { - // if (i < cable.topNode) { - nodeEMCs.push(emc); + nodeEMCs.push(emc); emcCounts.total++; if ( emc > bin.settings.inventory.targetMoisture + - bin.settings.inventory.moistureTargetDeviation + bin.settings.inventory.moistureTargetDeviation ) { emcCounts.above++; } else if ( emc < bin.settings.inventory.targetMoisture - - bin.settings.inventory.moistureTargetDeviation + bin.settings.inventory.moistureTargetDeviation ) { emcCounts.below++; } else { emcCounts.onTarget++; } - // } - } - }); + } + }); + cableEMCAvgs.push(avg(nodeEMCs)); + } cableTempAvgs.push(avg(nodeTemps)); - cableEMCAvgs.push(avg(nodeEMCs)); } }); if (cableTempAvgs.length > 0) { diff --git a/src/bin/BinVisualizerV2.tsx b/src/bin/BinVisualizerV2.tsx index 0db4690..8cd41e0 100644 --- a/src/bin/BinVisualizerV2.tsx +++ b/src/bin/BinVisualizerV2.tsx @@ -250,6 +250,7 @@ export default function BinVisualizer(props: Props) { const [cfmHighOpen, setCFMHighOpen] = useState(false); const [{ user }] = useGlobalState(); const [newPreset, setNewPreset] = useState(pond.BinMode.BIN_MODE_NONE); + const [newBinMode, setNewBinMode] = useState(pond.BinMode.BIN_MODE_NONE); const [selectedCable, setSelectedCable] = useState(); const [openNodeDialog, setOpenNodeDialog] = useState(false); const [cableDevice, setCableDevice] = useState(); @@ -345,6 +346,7 @@ export default function BinVisualizer(props: Props) { setGrainOption(ToGrainOption(bin.settings.inventory.grainType)); setBushPerTonne(bin.settings.inventory.bushelsPerTonne.toFixed(2)); setGrainSubtype(bin.subtype()); + setNewBinMode(bin.settings.mode); } if (bin.settings) { 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 let filteredNodes = cable.filteredNodes(true) temps.push(...filteredNodes.temps) - humids.push(...filteredNodes.humids) - emcs.push(...filteredNodes.moistures) + //only push to the humidity values if the cable reads humidities + 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 humClone = cloneDeep(cable.humidities).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 //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 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 = { - tempC: temps[lowTempIndex], - humidity: humids[lowTempIndex], - emc: emcs[lowTempIndex], + tempC: filteredNodes.temps[lowTempIndex], + humidity: filteredNodes.humids[lowTempIndex], + emc: filteredNodes.moistures[lowTempIndex], tempCTrend: cableTrendData.coldNodeTrend?.tempTrend ?? 0, humidityTrend: cableTrendData.coldNodeTrend?.humidityTrend ?? 0, emcTrend: cableTrendData.coldNodeTrend?.emcTrend ?? 0 @@ -424,13 +430,13 @@ export default function BinVisualizer(props: Props) { } //do the same for the high node - if (!highNodeConditions || cable.maxTemp() > highNodeConditions.tempC) { + if (!highNodeConditions || Math.max(...filteredNodes.temps) > highNodeConditions.tempC) { 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 = { - tempC: temps[highTempIndex], - humidity: humids[highTempIndex], - emc: emcs[highTempIndex], + tempC: filteredNodes.temps[highTempIndex], + humidity: filteredNodes.humids[highTempIndex], + emc: filteredNodes.moistures[highTempIndex], tempCTrend: cableTrendData.hotNodeTrend?.tempTrend ?? 0, humidityTrend: cableTrendData.hotNodeTrend?.humidityTrend ?? 0, emcTrend: cableTrendData.hotNodeTrend?.emcTrend ?? 0 @@ -1967,6 +1973,7 @@ export default function BinVisualizer(props: Props) { const setModeStorage = () => { setNewPreset(pond.BinMode.BIN_MODE_STORAGE); + setNewBinMode(pond.BinMode.BIN_MODE_STORAGE); }; const setModeCooldown = () => { @@ -1974,6 +1981,7 @@ export default function BinVisualizer(props: Props) { return; } setNewPreset(pond.BinMode.BIN_MODE_COOLDOWN); + setNewBinMode(pond.BinMode.BIN_MODE_COOLDOWN); }; const setModeDrying = () => { @@ -1982,13 +1990,16 @@ export default function BinVisualizer(props: Props) { bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture ) { setNewPreset(pond.BinMode.BIN_MODE_HYDRATING); + setNewBinMode(pond.BinMode.BIN_MODE_HYDRATING); } else { setNewPreset(pond.BinMode.BIN_MODE_DRYING); + setNewBinMode(pond.BinMode.BIN_MODE_DRYING); } }; const closeMoistureDialog = () => { setNewPreset(0); + setNewBinMode(bin.settings.mode); setShowInputMoisture(false); }; @@ -2049,6 +2060,7 @@ export default function BinVisualizer(props: Props) { + + + + ); + }; + + return ( + + + + + + + + + + + + + + + + + + + + + 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. + + + + {newOrgDialog()} + + ); +} diff --git a/src/models/Bin.ts b/src/models/Bin.ts index c66d8f9..b84af8c 100644 --- a/src/models/Bin.ts +++ b/src/models/Bin.ts @@ -96,7 +96,7 @@ export class Bin { } 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 { @@ -126,8 +126,10 @@ export class Bin { } public bushels(): number { + let control = this.settings.inventory?.inventoryControl; 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 } return bushels @@ -137,7 +139,8 @@ export class Bin { let fill = 0; if (this.settings.inventory && this.settings.specs) { 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 } fill = Math.round( diff --git a/src/models/Component.ts b/src/models/Component.ts index 9b96599..473d85d 100644 --- a/src/models/Component.ts +++ b/src/models/Component.ts @@ -114,7 +114,7 @@ export class Component { return 0; } - public addressDescription = (deviceProduct?: pond.DeviceProduct) => { + public addressDescription(deviceProduct?: pond.DeviceProduct): string { if ( this.settings.addressType === quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY || (this.settings.addressType >= quack.AddressType.ADDRESS_TYPE_PIN_OFFSET1 && diff --git a/src/models/GrainCable.ts b/src/models/GrainCable.ts index 52d6b6b..70dc257 100644 --- a/src/models/GrainCable.ts +++ b/src/models/GrainCable.ts @@ -273,7 +273,7 @@ export class GrainCable { let filtered: number[] = [] values.forEach((val, index) => { //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) } }) diff --git a/src/navigation/Router.tsx b/src/navigation/Router.tsx index f6badb1..afe12c5 100644 --- a/src/navigation/Router.tsx +++ b/src/navigation/Router.tsx @@ -38,7 +38,7 @@ const Transactions = lazy(() => import("pages/Transactions")); const BinCableEstimator = lazy(() => import("pages/BinCableEstimator")); const APIDocs = lazy(() => import("pages/APIDocs")); 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 Firmware = lazy(() => import("pages/Firmware")); const Nfc = lazy(() => import("pages/Nfc")); @@ -46,6 +46,7 @@ const Contracts = lazy(() => import("pages/Contracts")); const Contract = lazy(() => import("pages/Contract")); const JohnDeere = lazy(() => import("pages/JohnDeere")); const CNHi = lazy(() => import("pages/CNHi")); +const LibraCart = lazy(() => import("pages/LibraCart")); export const appendToUrl = (appendage: number | string) => { const basePath = location.pathname.replace(/\/$/, ""); @@ -340,6 +341,9 @@ export default function Router() { {user.hasFeature("cnhi") && } /> } + {user.hasFeature("libra-cart") && + } /> + } {/* Map routes */} } /> } /> diff --git a/src/navigation/SideNavigator.tsx b/src/navigation/SideNavigator.tsx index f71ca9c..f3e95a4 100644 --- a/src/navigation/SideNavigator.tsx +++ b/src/navigation/SideNavigator.tsx @@ -48,6 +48,7 @@ import DataDuckIcon from "products/Bindapt/DataDuckIcon"; import ContractsIcon from "products/CommonIcons/contractIcon"; import JohnDeereIcon from "products/CommonIcons/johnDeereIcon"; import CNHiIcon from "products/CommonIcons/cnhiIcon"; +import LibraCartIcon from "products/CommonIcons/libracartIcon"; const drawerWidth = 230; @@ -69,19 +70,26 @@ const useStyles = makeStyles((theme: Theme) => ({ }) }, sideMenuOnClosed: { - transition: theme.transitions.create(["width", "z-index", "opacity"], { + transition: theme.transitions.create(["width"], { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.leavingScreen }), - overflowX: "hidden", - width: theme.spacing(7), - zIndex: theme.zIndex.drawer, - opacity: 0, + // overflowX: "hidden", + width: theme.spacing(0), + // zIndex: theme.zIndex.drawer, + // opacity: 0, [theme.breakpoints.up("md")]: { - width: theme.spacing(9), + width: theme.spacing(9.25), opacity: 1 } }, + sideMenuOnClosedMobile: { + transition: theme.transitions.create(["width"], { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen + }), + width: theme.spacing(0), + }, list: { paddingTop: 0 }, @@ -436,7 +444,7 @@ export default function SideNavigator(props: Props) { } - {(isAg || isStreamline) && + {(isAg || isStreamline || user.hasFeature("admin")) && } + } + {user.hasFeature("libra-cart") && + + goTo("/libracart")} + classes={getClasses("/libracart")} + > + + + + {open && } + + } ) @@ -489,7 +511,7 @@ export default function SideNavigator(props: Props) { classes={{ paper: classNames( classes.sideMenu, - open ? classes.sideMenuOpened : classes.sideMenuOnClosed + open ? classes.sideMenuOpened : (isMobile ? classes.sideMenuOnClosedMobile : classes.sideMenuOnClosed) ) }} anchor="left" diff --git a/src/pages/Bin.tsx b/src/pages/Bin.tsx index fcc1117..082a2c9 100644 --- a/src/pages/Bin.tsx +++ b/src/pages/Bin.tsx @@ -810,7 +810,8 @@ export default function Bin(props: Props) { {(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) && ( - {/* - { - setCardValDisplay("low"); - }}> - Low - - { - setCardValDisplay("average"); - }}> - Average - - { - setCardValDisplay("high"); - }}> - High - - */} @@ -1270,41 +1239,6 @@ export default function Bins(props: Props) { } ]} /> - {/* - { - setBinView("grid"); - sessionStorage.setItem("binsView", "grid"); - }}> - - */} - {/* hidden at dustins request so that grid view and list are the only two */} - {/* { - setBinView("scroll"); - sessionStorage.setItem("binsView", "scroll"); - }}> - - */} - {/* { - setBinView("list"); - sessionStorage.setItem("binsView", "list"); - }}> - - - */} { diff --git a/src/pages/CNHi.tsx b/src/pages/CNHi.tsx index 24be24c..260d614 100644 --- a/src/pages/CNHi.tsx +++ b/src/pages/CNHi.tsx @@ -20,7 +20,7 @@ import PageContainer from "./PageContainer"; export default function CNHi() { const [currentOrg, setCurrentOrg] = useState(""); const cnhiAPI = useCNHiProxyAPI(); - const [organizations, setOrganizations] = useState>(new Map()); + const [organizations, setOrganizations] = useState>(new Map()); const [fieldAccordion, setFieldAccordion] = useState(false); const [dataOptions, setDataOptions] = useState([]); const [{ as }] = useGlobalState(); diff --git a/src/pages/Devices.tsx b/src/pages/Devices.tsx index 7721ec1..673f097 100644 --- a/src/pages/Devices.tsx +++ b/src/pages/Devices.tsx @@ -472,7 +472,7 @@ export default function Devices() { ] if (hasPlenums) { columns.push({ - title: "Plenum", + title: "Temp/Humidity", // sortKey: "hi", // disableSort: true, render: (device: Device) => { diff --git a/src/pages/LibraCart.tsx b/src/pages/LibraCart.tsx new file mode 100644 index 0000000..19c4dc8 --- /dev/null +++ b/src/pages/LibraCart.tsx @@ -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>(new Map()); + const [dataOptions, setDataOptions] = useState([]); + const [{ as }] = useGlobalState(); + + //load organizations for the user + useEffect(() => { + setCurrentOrg(""); + libracartAPI + .listAccounts(0, 0, as) + .then(resp => { + let tempOrgs: Map = 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 ( + + + + { + //setFields(!fields); + updateOrgData(checked, pond.DataOption.DATA_OPTION_DESTINATIONS); + }} + /> + } + /> + + + Import your destinations for the option to link it to a bin + + + + ); + }; + return ( + + + + + + + + + + + {fieldOptions()} + + ); +} diff --git a/src/pages/Users.tsx b/src/pages/Users.tsx index 296c60a..c7e155c 100644 --- a/src/pages/Users.tsx +++ b/src/pages/Users.tsx @@ -128,7 +128,8 @@ export default function Users() { "developer", "marketplace", "installer", - "cnhi" + "cnhi", + "libra-cart" ].sort(); const [rows, setRows] = useState([]) diff --git a/src/pbHelpers/Power.tsx b/src/pbHelpers/Power.tsx index 67bb45b..b98a842 100644 --- a/src/pbHelpers/Power.tsx +++ b/src/pbHelpers/Power.tsx @@ -71,16 +71,19 @@ export function describePower(power?: pond.DevicePower | null): PowerDescriber { } } else { let thresholds = notChargingThresholds; - let suffix = "not charging"; - if (power.inputVoltage >= 4) { - thresholds = chargingThresholds; - if (power.chargePercent === 100) { - suffix = "charged"; - } else { - suffix = "charging"; - } - } - result.description = power.chargePercent.toString() + "%, " + suffix; + // 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 + // let suffix = "not charging"; + // if (power.inputVoltage >= 4) { + // thresholds = chargingThresholds; + // if (power.chargePercent === 100) { + // suffix = "charged"; + // } else { + // suffix = "charging"; + // } + // } + // 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++) { if (power.chargePercent >= thresholds[i].threshold) { result.icon = thresholds[i].icon; diff --git a/src/products/Bindapt/BindaptAvailability.ts b/src/products/Bindapt/BindaptAvailability.ts index be18f38..50575d8 100644 --- a/src/products/Bindapt/BindaptAvailability.ts +++ b/src/products/Bindapt/BindaptAvailability.ts @@ -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_LIDAR, [0x62]], [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]], diff --git a/src/products/CommonIcons/libracartIcon.tsx b/src/products/CommonIcons/libracartIcon.tsx new file mode 100644 index 0000000..c71c55a --- /dev/null +++ b/src/products/CommonIcons/libracartIcon.tsx @@ -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 ; +} \ No newline at end of file diff --git a/src/providers/index.ts b/src/providers/index.ts index 9689df0..cede030 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -45,7 +45,8 @@ export { useFileControllerAPI, useContractAPI, //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"; // export { SecurityContext, useSecurity } from "./security"; export { SnackbarContext, useSnackbar } from "./Snackbar"; diff --git a/src/providers/pond/devicePresetAPI.tsx b/src/providers/pond/devicePresetAPI.tsx new file mode 100644 index 0000000..e24d97c --- /dev/null +++ b/src/providers/pond/devicePresetAPI.tsx @@ -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>; + //list + listDevicePresets: ( + limit: number, + offset: number, + order?: "asc" | "desc", + orderBy?: string, + search?: string, + keys?: string[], + types?: string[] + ) => Promise>; + //update + updateDevicePreset: ( + key: string, + name: string, + settings: pond.DevicePresetSettings + ) => Promise>; + + //remove + removeDevicePreset: (key: string) => Promise>; + + //get + getDevicePreset: (key: string) => Promise>; +} + +export const DevicePresetAPIcontext = createContext( + {} as IDevicePresetInterface +); + +interface Props {} + +export default function DevicePresetProvider(props: PropsWithChildren) { + 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( + pondURL("/devicePreset?name=" + name + "&as=" + as + key + type), + settings + ); + } + return post( + 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( + 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( + pondURL("/devicePreset/" + key + "?as=" + as + "&name=" + name), + settings + ); + } + return put( + pondURL("/devicePreset/" + key + "?name=" + name), + settings + ); + }; + + //remove + const removeDevicePreset = (key: string) => { + if (as) { + return del(pondURL("/devicePreset/" + key + "?as=" + as)); + } + return del(pondURL("/devicePreset/" + key + "?as=" + as)); + }; + + //get + const getDevicePreset = (key: string) => { + if (as) { + return del(pondURL("/devicePreset/" + key + "?as=" + as)); + } + return del(pondURL("/devicePreset/" + key + "?as=" + as)); + }; + return ( + + {children} + + ); +} + +export const useDevicePresetAPI = () => useContext(DevicePresetAPIcontext); diff --git a/src/providers/pond/libracartProxyAPI.tsx b/src/providers/pond/libracartProxyAPI.tsx new file mode 100644 index 0000000..b56f65b --- /dev/null +++ b/src/providers/pond/libracartProxyAPI.tsx @@ -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>; + //list organizations + listAccounts: ( + limit: number, + offset: number, + as?: string, + keys?: string[], + types?: string[] + ) => Promise>; + updateAccount: ( + key: string, + options: pond.DataOption[], + as?: string + ) => Promise>; + listDestinations: ( + limit: number, + offset: number, + libracartKey?: string, + as?: string + ) => Promise>; + syncData: () => Promise +} + +export const LibraCartProxyAPIContext = createContext( + {} as ILibraCartProxyAPIContext +); + +interface Props {} + +export default function LibraCartProvider(props: PropsWithChildren) { + 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( + 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( + 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( + pondURL( + "/libracartAccounts/" + key + "?options=" + options.toString() + (as ? "&as=" + as : "") + ) + ); + }; + + const listDestinations = (limit: number, offset: number, libracartKey?: string, as?: string) => { + return get( + pondURL( + "/libracartDestinations?limit" + limit + "&offset=" + offset + (libracartKey ? "&libracartKey=" + libracartKey : "") + (as ? "&as=" + as : "") + ) + ) + } + + const syncData = () => { + return get(pondURL("/libracartImport" + (as ? "?as=" + as : ""))) + } + + return ( + + {children} + + ); +} + +export const useLibraCartProxyAPI = () => useContext(LibraCartProxyAPIContext); diff --git a/src/providers/pond/pond.tsx b/src/providers/pond/pond.tsx index 8e879f9..0c22353 100644 --- a/src/providers/pond/pond.tsx +++ b/src/providers/pond/pond.tsx @@ -35,7 +35,9 @@ import ContractProvider, { useContractAPI } from "./contractAPI"; import UsageProvider, { useUsageAPI } from "./usageAPI"; import KeyManagerProvider, { useKeyManagerAPI } from "./keyManagerAPI"; import JohnDeereProvider, { useJohnDeereProxyAPI } from "./johnDeereProxyAPI"; +import LibraCartProvider, { useLibraCartProxyAPI } from "./libracartProxyAPI"; import CNHiProvider, { useCNHiProxyAPI } from "./cnhiProxyAPI"; +import DevicePresetProvider, { useDevicePresetAPI } from "./devicePresetAPI"; // import NoteProvider from "providers/noteAPI"; export const pondURL = (partial: string, demo: boolean = false): string => { @@ -86,11 +88,15 @@ export default function PondProvider(props: PropsWithChildren) { - - - {children} - - + + + + + {children} + + + + @@ -163,5 +169,7 @@ export { useUsageAPI, useKeyManagerAPI, useJohnDeereProxyAPI, - useCNHiProxyAPI + useCNHiProxyAPI, + useLibraCartProxyAPI, + useDevicePresetAPI }; diff --git a/src/user/UserSettings.tsx b/src/user/UserSettings.tsx index ec86924..17575e5 100644 --- a/src/user/UserSettings.tsx +++ b/src/user/UserSettings.tsx @@ -10,7 +10,7 @@ import { Textsms as TextIcon, Contrast, } 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 UserAvatar from "./UserAvatar"; import { useGlobalState, useSnackbar, useUserAPI } from "providers"; @@ -31,6 +31,7 @@ import UnitsIcon from "@mui/icons-material/Category"; import { setThemeType } from "theme"; import { IsAdaptiveAgriculture } from "services/whiteLabel"; import { setDistanceUnit, setGrainUnit, setPressureUnit, setTemperatureUnit } from "utils"; +import FieldsIcon from "products/AgIcons/FieldsIcon"; const useStyles = makeStyles((theme: Theme) => { return ({ @@ -82,7 +83,9 @@ export default function UserSettings(props: Props) { const [avatarExpanded, setAvatarExpanded] = useState(false); const [notificationsExpanded, setNotificationsExpanded] = useState(false); const [unitsExpanded, setUnitsExpanded] = useState(false); + const [mapsExpanded, setMapsExpanded] = useState(false); const [avatarUrl, setAvatarUrl] = useState(""); + const [sliderVal, setSliderVal] = useState(globalState.user.settings.mapZoom); // const [themeValue, setThemeValue] = useState<"light" | "dark" | "system">(themeMode.mode) useEffect(() => { @@ -151,6 +154,30 @@ export default function UserSettings(props: Props) { setUser(updatedUser); }; + const changeDefaultZoom = (value: number) => { + setSliderVal(value); + let updatedUser = User.clone(user); + updatedUser.settings.mapZoom = value; + setUser(updatedUser) + }; + + const mapSettings = () => { + return ( + + Map Zoom Level + { + changeDefaultZoom(val as number); + }} + /> + + ); + }; + const generalSettings = () => { const { name, email, phoneNumber, timezone } = user.settings; @@ -631,15 +658,15 @@ export default function UserSettings(props: Props) { - {/* + - setMapsExpanded(!mapsExpanded)}> + setMapsExpanded(!mapsExpanded)}> {mapsExpanded ? : } - + @@ -648,7 +675,7 @@ export default function UserSettings(props: Props) { - */} + ); }; diff --git a/src/userFeatures/UserFeatures.tsx b/src/userFeatures/UserFeatures.tsx index 9f9a2f4..d22b4e8 100644 --- a/src/userFeatures/UserFeatures.tsx +++ b/src/userFeatures/UserFeatures.tsx @@ -5,6 +5,7 @@ import { useSnackbar, useUserAPI } from "hooks"; import React, { useEffect, useState } from "react"; import JohnDeereIcon from "products/CommonIcons/johnDeereIcon"; import CNHiIcon from "products/CommonIcons/cnhiIcon"; +import LibraCartIcon from "products/CommonIcons/libracartIcon"; //import AgLogo from "assets/whitelabels/AdaptiveAgriculture/AGLogoSquare.png"; import { IsAdaptiveAgriculture @@ -60,7 +61,15 @@ const agFeatureList: ProductDetails[] = [ "Integrate with the Case New Holland Industrial Center to bring your data into our platform" // bulletPoints: ["bullet one", "bullet two"], // questions: [], - } + }, + // { + // featureName: "libra-cart", + // featureLogo: , + // featureCost: 0, + // cardImage: FeatureImageTest, + // featureTitle: "Libra Cart", + // longDescription: "Integrate with Libra Cart to bring your data into our platform" + // } ]; // const constructionFeatureList: ProductDetails[] = [] @@ -80,7 +89,7 @@ export default function Marketplace() { useEffect(() => { let list: ProductDetails[] = []; - if (IsAdaptiveAgriculture()) { + if (IsAdaptiveAgriculture() || user.hasFeature("admin")) { list = list.concat(agFeatureList); } // if(IsAdCon()){