diff --git a/package-lock.json b/package-lock.json index 6786318..1d30334 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10864,7 +10864,7 @@ }, "node_modules/protobuf-ts": { "version": "1.0.0", - "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#e5ab6369bbfc866e7e8792f94a62f1ca8931e67e", + "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#8650e48a44dffc4510ab1fb1e869fd73e242db69", "dependencies": { "protobufjs": "^6.8.8" } diff --git a/package.json b/package.json index 1f588fb..8ffd6e1 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,9 @@ "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview", - "build:staging": "VITE_AUTH0_CLIENT_DOMAIN=adaptivestaging.us.auth0.com VITE_AUTH0_AUDIENCE=stagingapi.brandxtech.ca VITE_AUTH0_CLIENT_ID=3ib460VvLwdeyse5iUSQfxkVdQaUmphP VITE_APP_API_URL=https://stagingapi.brandxtech.ca/v1 VITE_APP_WS_URL=wss://stagingapi.brandxtech.ca/v1/live NODE_OPTIONS=--max_old_space_size=4096 VITE_APP_GOOGLE_API_KEY=${GOOGLE_API_KEY} VITE_APP_STRIPE_PUBLIC_KEY=${STRIPE_PUBLIC_KEY_STAGING} VITE_MAPBOX_ACCESS_TOKEN=${MAPBOX_ACCESS_TOKEN} VITE_CNHI_CLIENT_ID=${CNHI_CLIENT_ID} VITE_CNHI_AUTHORIZE_URL=${CNHI_AUTHORIZE_URL} VITE_CNHI_REDIRECT_URI=${CNHI_REDIRECT_URI} VITE_CNHI_SCOPES=${CNHI_SCOPES} VITE_CNHI_CONNECTION=${CNHI_CONNECTION} VITE_CNHI_AUDIENCE=${CNHI_AUDIENCE} VITE_APP_IMAGE4IO_USERNAME=${IMAGE4IO_USERNAME} VITE_APP_IMAGE4IO_PASSWORD=${IMAGE4IO_PASSWORD} VITE_JD_CLIENT_ID=${JD_CLIENT_ID} VITE_JD_AUTHORIZE_URL=${JD_AUTHORIZE_URL} VITE_JD_REDIRECT_URI=${JD_REDIRECT_URI} VITE_JD_SCOPES=${JD_SCOPES} VITE_JD_STATE=${JD_STATE} VITE_OPEN_WEATHERMAP={OPEN_WEATHERMAP} vite build", - "build:development": "VITE_AUTH0_CLIENT_ID=dzJTpqIeMA4Rwk4xujtwPbAO3TY32bM1 VITE_AUTH0_AUDIENCE=bxt-dev.api.adaptiveagriculture.ca VITE_APP_API_URL=https://bxt-dev.api.adaptiveagriculture.ca/v1 NODE_OPTIONS=--max_old_space_size=4096 VITE_APP_GOOGLE_API_KEY=${GOOGLE_API_KEY} VITE_APP_STRIPE_PUBLIC_KEY=${STRIPE_PUBLIC_KEY_DEVELOPMENT} VITE_MAPBOX_ACCESS_TOKEN=${MAPBOX_ACCESS_TOKEN} VITE_CNHI_CLIENT_ID=${CNHI_CLIENT_ID} VITE_CNHI_AUTHORIZE_URL=${CNHI_AUTHORIZE_URL} VITE_CNHI_REDIRECT_URI=${CNHI_REDIRECT_URI} VITE_CNHI_SCOPES=${CNHI_SCOPES} VITE_CNHI_CONNECTION=${CNHI_CONNECTION} VITE_CNHI_AUDIENCE=${CNHI_AUDIENCE} VITE_APP_IMAGE4IO_USERNAME=${IMAGE4IO_USERNAME} VITE_APP_IMAGE4IO_PASSWORD=${IMAGE4IO_PASSWORD} VITE_JD_CLIENT_ID=${JD_CLIENT_ID} VITE_JD_AUTHORIZE_URL=${JD_AUTHORIZE_URL} VITE_JD_REDIRECT_URI=${JD_REDIRECT_URI} VITE_JD_SCOPES=${JD_SCOPES} VITE_JD_STATE=${JD_STATE} VITE_OPEN_WEATHERMAP={OPEN_WEATHERMAP} vite build", - "build:production": "VITE_AUTH0_CLIENT_ID=5pUCkl2SfogkWmM244UDcLEUOp8EFdHd VITE_AUTH0_CLIENT_DOMAIN=brandxtech.auth0.com VITE_AUTH0_AUDIENCE=api.brandxtech.ca VITE_APP_API_URL=https://api.brandxtech.ca/v1 NODE_OPTIONS=--max_old_space_size=4096 VITE_APP_GOOGLE_API_KEY=${GOOGLE_API_KEY} VITE_APP_STRIPE_PUBLIC_KEY=${STRIPE_PUBLIC_KEY_PRODUCTION} VITE_MAPBOX_ACCESS_TOKEN=${MAPBOX_ACCESS_TOKEN} VITE_CNHI_CLIENT_ID=${CNHI_CLIENT_ID} VITE_CNHI_AUTHORIZE_URL=${CNHI_AUTHORIZE_URL} VITE_CNHI_REDIRECT_URI=${CNHI_REDIRECT_URI} VITE_CNHI_SCOPES=${CNHI_SCOPES} VITE_CNHI_CONNECTION=${CNHI_CONNECTION} VITE_CNHI_AUDIENCE=${CNHI_AUDIENCE} VITE_APP_IMAGE4IO_USERNAME=${IMAGE4IO_USERNAME} VITE_APP_IMAGE4IO_PASSWORD=${IMAGE4IO_PASSWORD} VITE_JD_CLIENT_ID=${JD_CLIENT_ID} VITE_JD_AUTHORIZE_URL=${JD_AUTHORIZE_URL} VITE_JD_REDIRECT_URI=${JD_REDIRECT_URI} VITE_JD_SCOPES=${JD_SCOPES} VITE_JD_STATE=${JD_STATE} VITE_OPEN_WEATHERMAP={OPEN_WEATHERMAP} vite build", + "build:staging": "VITE_AUTH0_CLIENT_DOMAIN=adaptivestaging.us.auth0.com VITE_AUTH0_AUDIENCE=stagingapi.brandxtech.ca VITE_AUTH0_CLIENT_ID=3ib460VvLwdeyse5iUSQfxkVdQaUmphP VITE_APP_API_URL=https://stagingapi.brandxtech.ca/v1 VITE_APP_WS_URL=wss://stagingapi.brandxtech.ca/v1/live NODE_OPTIONS=--max_old_space_size=4096 VITE_APP_GOOGLE_API_KEY=${GOOGLE_API_KEY} VITE_APP_STRIPE_PUBLIC_KEY=${STRIPE_PUBLIC_KEY_STAGING} VITE_MAPBOX_ACCESS_TOKEN=${MAPBOX_ACCESS_TOKEN} VITE_CNHI_CLIENT_ID=${CNHI_CLIENT_ID} VITE_CNHI_AUTHORIZE_URL=${CNHI_AUTHORIZE_URL} VITE_CNHI_REDIRECT_URI=${CNHI_REDIRECT_URI} VITE_CNHI_SCOPES=${CNHI_SCOPES} VITE_CNHI_CONNECTION=${CNHI_CONNECTION} VITE_CNHI_AUDIENCE=${CNHI_AUDIENCE} VITE_APP_IMAGE4IO_USERNAME=${IMAGE4IO_USERNAME} VITE_APP_IMAGE4IO_PASSWORD=${IMAGE4IO_PASSWORD} VITE_JD_CLIENT_ID=${JD_CLIENT_ID} VITE_JD_AUTHORIZE_URL=${JD_AUTHORIZE_URL} VITE_JD_REDIRECT_URI=${JD_REDIRECT_URI} VITE_JD_SCOPES=${JD_SCOPES} VITE_JD_STATE=${JD_STATE} VITE_OPEN_WEATHERMAP=${OPEN_WEATHERMAP} vite build", + "build:development": "VITE_AUTH0_CLIENT_ID=dzJTpqIeMA4Rwk4xujtwPbAO3TY32bM1 VITE_AUTH0_AUDIENCE=bxt-dev.api.adaptiveagriculture.ca VITE_APP_API_URL=https://bxt-dev.api.adaptiveagriculture.ca/v1 NODE_OPTIONS=--max_old_space_size=4096 VITE_APP_GOOGLE_API_KEY=${GOOGLE_API_KEY} VITE_APP_STRIPE_PUBLIC_KEY=${STRIPE_PUBLIC_KEY_DEVELOPMENT} VITE_MAPBOX_ACCESS_TOKEN=${MAPBOX_ACCESS_TOKEN} VITE_CNHI_CLIENT_ID=${CNHI_CLIENT_ID} VITE_CNHI_AUTHORIZE_URL=${CNHI_AUTHORIZE_URL} VITE_CNHI_REDIRECT_URI=${CNHI_REDIRECT_URI} VITE_CNHI_SCOPES=${CNHI_SCOPES} VITE_CNHI_CONNECTION=${CNHI_CONNECTION} VITE_CNHI_AUDIENCE=${CNHI_AUDIENCE} VITE_APP_IMAGE4IO_USERNAME=${IMAGE4IO_USERNAME} VITE_APP_IMAGE4IO_PASSWORD=${IMAGE4IO_PASSWORD} VITE_JD_CLIENT_ID=${JD_CLIENT_ID} VITE_JD_AUTHORIZE_URL=${JD_AUTHORIZE_URL} VITE_JD_REDIRECT_URI=${JD_REDIRECT_URI} VITE_JD_SCOPES=${JD_SCOPES} VITE_JD_STATE=${JD_STATE} VITE_OPEN_WEATHERMAP=${OPEN_WEATHERMAP} vite build", + "build:production": "VITE_AUTH0_CLIENT_ID=5pUCkl2SfogkWmM244UDcLEUOp8EFdHd VITE_AUTH0_CLIENT_DOMAIN=brandxtech.auth0.com VITE_AUTH0_AUDIENCE=api.brandxtech.ca VITE_APP_API_URL=https://api.brandxtech.ca/v1 NODE_OPTIONS=--max_old_space_size=4096 VITE_APP_GOOGLE_API_KEY=${GOOGLE_API_KEY} VITE_APP_STRIPE_PUBLIC_KEY=${STRIPE_PUBLIC_KEY_PRODUCTION} VITE_MAPBOX_ACCESS_TOKEN=${MAPBOX_ACCESS_TOKEN} VITE_CNHI_CLIENT_ID=${CNHI_CLIENT_ID} VITE_CNHI_AUTHORIZE_URL=${CNHI_AUTHORIZE_URL} VITE_CNHI_REDIRECT_URI=${CNHI_REDIRECT_URI} VITE_CNHI_SCOPES=${CNHI_SCOPES} VITE_CNHI_CONNECTION=${CNHI_CONNECTION} VITE_CNHI_AUDIENCE=${CNHI_AUDIENCE} VITE_APP_IMAGE4IO_USERNAME=${IMAGE4IO_USERNAME} VITE_APP_IMAGE4IO_PASSWORD=${IMAGE4IO_PASSWORD} VITE_JD_CLIENT_ID=${JD_CLIENT_ID} VITE_JD_AUTHORIZE_URL=${JD_AUTHORIZE_URL} VITE_JD_REDIRECT_URI=${JD_REDIRECT_URI} VITE_JD_SCOPES=${JD_SCOPES} VITE_JD_STATE=${JD_STATE} VITE_OPEN_WEATHERMAP=${OPEN_WEATHERMAP} vite build", "build:offline": "npx env-cmd offline,whitelabel npm run build", "test": "vitest" }, diff --git a/src/app/App.css b/src/app/App.css index 3b20b88..89db113 100644 --- a/src/app/App.css +++ b/src/app/App.css @@ -22,6 +22,7 @@ html, body, #root { height: 100%; margin: 0; padding: 0; + overflow-y: hidden; } /* styles.css or a CSS module */ diff --git a/src/bin/BinCardV2.tsx b/src/bin/BinCardV2.tsx index 58bff74..8f106ee 100644 --- a/src/bin/BinCardV2.tsx +++ b/src/bin/BinCardV2.tsx @@ -58,9 +58,9 @@ interface GrainDetails { export default function BinCard(props: Props) { const { bin, valDisplay } = props; const [cables, setCables] = useState(); - const [modeDetails, setModeDetails] = useState(""); + //const [modeDetails, setModeDetails] = useState(""); const [lidarPercentage, setLidarPercentage] = useState(); - const [lidarBushels, setLidarBushels] = useState(); + //const [lidarBushels, setLidarBushels] = useState(); const [{ user }] = useGlobalState(); const classes = useStyles(); const [average, setAverage] = useState(); @@ -84,6 +84,7 @@ export default function BinCard(props: Props) { let coldestNode: GrainDetails | undefined; let hottestNode: GrainDetails | undefined; let now = moment(); + //loop through the cables in the bin status to prep for display bin.status.grainCables.forEach(cable => { let c: GrainCable = new GrainCable(); //flip the cables so that for display node 1 is at the bottom @@ -137,8 +138,9 @@ export default function BinCard(props: Props) { }; } - let lastRead = moment(cable.lastRead); - let elapsedMS = now.diff(lastRead); + //check cables missed readings + let lastCableRead = moment(cable.lastRead); + let elapsedMS = now.diff(lastCableRead); if (elapsedMS > cable.measurementInterval) { let missedReadings = Math.floor(elapsedMS / cable.measurementInterval); @@ -147,6 +149,18 @@ export default function BinCard(props: Props) { } } }); + //loop through the CO2's to check for missed readings + bin.status.co2.forEach(co2 => { + let lastRead = moment(co2.lastRead); + let elapsedMS = now.diff(lastRead); + if (elapsedMS > co2.measurementInterval) { + let missedReadings = Math.floor(elapsedMS / co2.measurementInterval); + if (missedReadings > mostMissedReadings) { + mostMissedReadings = missedReadings; + } + } + }) + setCables(newGrainCables); setMostMissed(mostMissedReadings); setColdNode(coldestNode); @@ -165,28 +179,28 @@ export default function BinCard(props: Props) { let ratio = 1 - cm / height; let capacity = or(bin.settings.specs?.bushelCapacity, 0); let lidarEstimate = Math.round(capacity * ratio); - setLidarBushels(lidarEstimate); + //setLidarBushels(lidarEstimate); setLidarPercentage(Math.round((lidarEstimate / capacity) * 100)); } }, [bin]); - useEffect(() => { - let now = moment(); - let duration = moment.duration(moment(bin.status.lastModeChange).diff(now)); - let days = duration.asDays(); - days = Math.abs(days); - duration.subtract(moment.duration(days, "days")); - let hours = duration.hours(); - hours = Math.abs(hours); + // useEffect(() => { + // let now = moment(); + // let duration = moment.duration(moment(bin.status.lastModeChange).diff(now)); + // let days = duration.asDays(); + // days = Math.abs(days); + // duration.subtract(moment.duration(days, "days")); + // let hours = duration.hours(); + // hours = Math.abs(hours); - if (days > 50 && bin.settings.mode === pond.BinMode.BIN_MODE_DRYING) { - setModeDetails("Calculating..."); - } else if (bin.settings.mode === pond.BinMode.BIN_MODE_NONE) { - setModeDetails("No Mode"); - } else { - setModeDetails(Math.floor(days) + "d " + hours + "h"); - } - }, [bin]); + // if (days > 50 && bin.settings.mode === pond.BinMode.BIN_MODE_DRYING) { + // setModeDetails("Calculating..."); + // } else if (bin.settings.mode === pond.BinMode.BIN_MODE_NONE) { + // setModeDetails("No Mode"); + // } else { + // setModeDetails(Math.floor(days) + "d " + hours + "h"); + // } + // }, [bin]); const typeDisplay = () => { let grainType = bin.settings.inventory?.grainType ?? pond.Grain.GRAIN_NONE; @@ -440,9 +454,10 @@ export default function BinCard(props: Props) { const componentStateBanner = () => { const hasCables = bin.status.grainCables.length > 0; + const hasCO2 = bin.status.co2.length > 0; return ( - {hasCables ? ( + {hasCables || hasCO2 ? ( {mostMissed < 3 ? "Active" : mostMissed <= warningThreshold ? "Missing" : "Inactive"} diff --git a/src/bin/BinComponentTypes.tsx b/src/bin/BinComponentTypes.tsx index e5f64e3..01562e5 100644 --- a/src/bin/BinComponentTypes.tsx +++ b/src/bin/BinComponentTypes.tsx @@ -32,6 +32,7 @@ import { MoreVert } from "@mui/icons-material"; import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; import { Ambient } from "models/Ambient"; import { makeStyles } from "@mui/styles"; +import { CO2 } from "models/CO2"; const useStyles = makeStyles((theme: Theme) => { return ({ @@ -123,7 +124,7 @@ export default function BinComponentTypes(props: Props) { const [heaters, setHeaters] = useState([]); const [fans, setFans] = useState([]); const [lidars, setLidars] = useState([]); - const [headspaceCo2s, setHeadspaceCo2s] = useState([]); + const [headspaceCo2s, setHeadspaceCo2s] = useState([]); const [tempUnit, setTempUnit] = useState(); const [pressureUnit, setPressureUnit] = useState(); const [selectedComponentKey, setSelectedComponentKey] = useState(""); @@ -176,7 +177,7 @@ export default function BinComponentTypes(props: Props) { var lidar: Component[] = []; var unassigned: Component[] = []; var ambients: Ambient[] = []; - var headspaceCo2s: Component[] = []; + var headspaceCo2s: CO2[] = []; components.forEach(comp => { let pref = preferences.get(comp.key()); if (pref) { @@ -198,7 +199,8 @@ export default function BinComponentTypes(props: Props) { } else if (comp.type() === quack.ComponentType.COMPONENT_TYPE_LIDAR) { lidar.push(comp); } else if (comp.type() === quack.ComponentType.COMPONENT_TYPE_CO2) { - headspaceCo2s.push(comp); + let coComp = CO2.create(comp) + headspaceCo2s.push(coComp); } } } else { @@ -716,9 +718,8 @@ export default function BinComponentTypes(props: Props) { return distanceCM + "cm"; }; - const headspaceList = (sensors: Headspace[], lidars: Component[], headspaceCo2s: Component[]) => { + const headspaceList = (sensors: Headspace[], lidars: Component[], headspaceCo2s: CO2[]) => { if (sensors.length < 1 && lidars.length < 1 && headspaceCo2s.length < 1) return null; - let headspaceComponents = lidars.concat(headspaceCo2s); return ( @@ -778,7 +779,73 @@ export default function BinComponentTypes(props: Props) { ); })} - {headspaceComponents.map((component, index) => { + {headspaceCo2s.map((co2, index) => { + let cIcon = GetComponentIcon( + co2.settings.type, + co2.settings.subtype, + theme.palette.mode + ); + + let colour = "white"; //default colour if there are no measurements to get the type from + + let ppmString = "--"; + if ( + co2.status.measurement[0] && + co2.status.measurement[0].values[0] && + co2.status.measurement[0].values[0].values[0] + ) { + colour = describeMeasurement( + co2.status.measurement[0].type, + co2.type(), + co2.subType() + ).colour(); + ppmString = co2.status.measurement[0].values[0].values[0] + " ppm"; + } + return ( + + + + + {co2.name()} + + + + + +
+ + {ppmString} + +
+
+
+ + ) => { + setComponentUnnassigned(false); + setSelectedComponentType(co2.type()); + setSelectedComponentKey(co2.key()); + setAnchorEl(event.currentTarget); + }}> + + + +
+
+
+ ); + }) + } + {lidars.map((component, index) => { let cIcon = GetComponentIcon( component.settings.type, component.settings.subtype, diff --git a/src/bin/BinSettings.tsx b/src/bin/BinSettings.tsx index a793dd5..5558230 100644 --- a/src/bin/BinSettings.tsx +++ b/src/bin/BinSettings.tsx @@ -540,8 +540,7 @@ export default function BinSettings(props: Props) { let specs = form.specs; let specsValid = specs && Number(inputCapacity) > 0 && specs.diameterCm > 0 && specs.heightCm > 0; - let tempsValid = +formExtension.highTemp > +formExtension.lowTemp; - return specsValid && tempsValid; + return specsValid default: return true; } @@ -1415,20 +1414,6 @@ export default function BinSettings(props: Props) { let angle = RadiansToDegrees(Math.atan((coneCM ?? 0) / (dCm / 2))); //('arcTan(opposite / adjacent))' and convert to degrees angle = Math.round(angle * 100) / 100; // round to 2 digits - //update the form with the new specs - updateForm( - "specs", - pond.BinSpecs.create({ - ...form.specs, - modelId: 0, - advancedDimensions: pond.BinAdvancedDimensions.create({ - ...form.specs?.advancedDimensions, - topConeHeight: coneCM, - roofAngle: angle - }) - }) - ); - let ext = cloneDeep(formExtension); //calculate the total height from the parts let s = isNaN(parseFloat(sidewallHeight)) ? 0 : parseFloat(sidewallHeight); @@ -1448,6 +1433,29 @@ export default function BinSettings(props: Props) { //update the extensions with the new values setFormExtension({ ...ext }); + + //need to get the total height in cm + let totalHeightCM = s + t + h; //the total height in the users units + if(getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET){ + totalHeightCM = totalHeightCM * 30.48 //convert from feet to cm + }else{ + totalHeightCM = totalHeightCM * 100 //convert from m to cm + } + + //update the form with the new specs + updateForm( + "specs", + pond.BinSpecs.create({ + ...form.specs, + modelId: 0, + heightCm: Math.round(totalHeightCM), + advancedDimensions: pond.BinAdvancedDimensions.create({ + ...form.specs?.advancedDimensions, + topConeHeight: coneCM, + roofAngle: angle + }) + }) + ); }} InputProps={{ endAdornment: ( @@ -1497,9 +1505,17 @@ export default function BinSettings(props: Props) { setInputCapacity(bushelCap.toFixed(0)); ext.capacity = bushelCap.toFixed(0); ext.topConeHeight = newConeHeight; - ext.height = (s + t + h).toFixed(2); - + ext.height = (s + t + h).toFixed(2) + setFormExtension(ext); + + //need to get the total height in cm + let totalHeightCM = s + t + h; //the total height in the users units + if(getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET){ + totalHeightCM = totalHeightCM * 30.48 //convert from feet to cm + }else{ + totalHeightCM = totalHeightCM * 100 //convert from m to cm + } setModelID(0); //update the form with the new specs @@ -1508,6 +1524,7 @@ export default function BinSettings(props: Props) { pond.BinSpecs.create({ ...form.specs, modelId: 0, + heightCm: Math.round(totalHeightCM), advancedDimensions: pond.BinAdvancedDimensions.create({ ...form.specs?.advancedDimensions, roofAngle: angle ?? 0, @@ -1553,11 +1570,21 @@ export default function BinSettings(props: Props) { setFormExtension({ ...ext }); setModelID(0); + + //need to get the total height in cm + let totalHeightCM = s + t + h; //the total height in the users units + if(getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET){ + totalHeightCM = totalHeightCM * 30.48 //convert from feet to cm + }else{ + totalHeightCM = totalHeightCM * 100 //convert from m to cm + } + updateForm( "specs", pond.BinSpecs.create({ ...form.specs, modelId: 0, + heightCm: Math.round(totalHeightCM), advancedDimensions: pond.BinAdvancedDimensions.create({ ...form.specs?.advancedDimensions, sidewallHeight: diff --git a/src/bin/BinStorageConditions.tsx b/src/bin/BinStorageConditions.tsx index 3d857e3..5d5e682 100644 --- a/src/bin/BinStorageConditions.tsx +++ b/src/bin/BinStorageConditions.tsx @@ -27,6 +27,7 @@ import React, { useEffect, useState } from "react"; import { avg, getTemperatureUnit } from "utils"; import { makeStyles } from "@mui/styles"; import { Mark } from "@mui/material/Slider/useSlider.types"; +import { CO2 } from "models/CO2"; const useStyles = makeStyles((theme: Theme) => { return ({ @@ -117,7 +118,7 @@ const useStyles = makeStyles((theme: Theme) => { interface Props { bin: Bin; - headspaceCO2: Component[]; + headspaceCO2: CO2[]; cables: GrainCable[]; } diff --git a/src/bin/BinTransactions.tsx b/src/bin/BinTransactions.tsx index f8e7c90..2a8d6e2 100644 --- a/src/bin/BinTransactions.tsx +++ b/src/bin/BinTransactions.tsx @@ -162,8 +162,9 @@ export default function BinTransactions(props: Props){ } }).catch(err => { openSnack("There was a problem submitting transaction approval") + }).finally(() => { + refresh() }) - refresh() } } @@ -174,9 +175,10 @@ export default function BinTransactions(props: Props){ openSnack("Transaction rejected") }).catch(err => { openSnack("There was a problem rejecting the transaction") + }).finally(() => { + refresh() }) } - refresh() } const getOptions = (type: pond.ObjectType) => { diff --git a/src/common/DataImports/BinCables/bracketData.json b/src/common/DataImports/BinCables/bracketData.json index 8b34492..32deb3f 100644 --- a/src/common/DataImports/BinCables/bracketData.json +++ b/src/common/DataImports/BinCables/bracketData.json @@ -1,17 +1,17 @@ [ { "ID": 1, - "Name": "CABL-BRK1", - "DealerPrice": 210 + "Name": "4-Rib", + "DealerPrice": 165 }, { "ID": 2, - "Name": "CABL-BRK2", - "DealerPrice": 217.5 + "Name": "4-Rib", + "DealerPrice": 165 }, { "ID": 3, - "Name": "CABL-BRK3", - "DealerPrice": 87 + "Name": "2-Rib", + "DealerPrice": 90 } ] diff --git a/src/common/ResponsiveTable.tsx b/src/common/ResponsiveTable.tsx index f904e0a..1d722db 100644 --- a/src/common/ResponsiveTable.tsx +++ b/src/common/ResponsiveTable.tsx @@ -5,6 +5,7 @@ import { useEffect, useRef, useState } from "react"; import React from "react"; import { useMobile } from "hooks"; import classNames from "classnames"; +import { getThemeType } from "theme"; const useStyles = makeStyles((theme: Theme) => { //const isMobile = useMobile() @@ -56,6 +57,12 @@ const useStyles = makeStyles((theme: Theme) => { backgroundColor: "rgba(150, 150, 150, 0.15)", } }, + stickyHeader: { + position: "sticky", + backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(40, 40, 40)", + top: 0, + zIndex: 300 //giving a really high z-index to make sure nothing is in front of it + } })}, ); @@ -101,6 +108,7 @@ interface Props { endTitleElement?: string | JSX.Element | (() => string | JSX.Element); loadMore?: () => void; resizeable?: boolean; + stickyHeader?: boolean } export default function ResponsiveTable(props: Props) { @@ -136,6 +144,7 @@ export default function ResponsiveTable(props: Props) { setOrder, loadMore, // resizeable, + stickyHeader } = props const classes = useStyles(); const columns = typeof(props.columns) === "function" ? props.columns() : props.columns @@ -486,9 +495,9 @@ export default function ResponsiveTable(props: Props) { } return ( - + - + @@ -702,6 +711,6 @@ export default function ResponsiveTable(props: Props) { - + ) } \ No newline at end of file diff --git a/src/maps/mapObjectTools/agMapTools.tsx b/src/maps/mapObjectTools/agMapTools.tsx index eb2ca82..f51e109 100644 --- a/src/maps/mapObjectTools/agMapTools.tsx +++ b/src/maps/mapObjectTools/agMapTools.tsx @@ -27,6 +27,7 @@ import DeviceIcon from "products/Bindapt/BindaptIcon"; import { pond } from "protobuf-ts/pond"; //import { useHistory } from "react-router"; import { makeStyles } from "@mui/styles"; +import { useNavigate } from "react-router-dom"; const useStyles = makeStyles((theme: Theme) => ({ iconButtons: { @@ -99,11 +100,11 @@ export default function AgMapTools(props: Props) { const [binControlHovered, setBinControlHovered] = useState(false); const [scoutingControlHovered, setScoutingControlHovered] = useState(false); const [showFieldTitle, setShowFieldTitles] = useState(false); + const navigate = useNavigate(); //const history = useHistory(); const goToMyFields = () => { - console.log("navigate to my fields page") - //history.push("/myFields"); + navigate("/fields"); }; const toggleControls = (control: "field" | "bin" | "scout") => { diff --git a/src/models/CO2.ts b/src/models/CO2.ts new file mode 100644 index 0000000..62e5d73 --- /dev/null +++ b/src/models/CO2.ts @@ -0,0 +1,110 @@ +import { pond } from "protobuf-ts/pond"; +import { quack } from "protobuf-ts/pond"; +import { or } from "utils/types"; +import { clone, cloneDeep } from "lodash"; +import { Component } from "models"; +import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; +import { extractGrainCable } from "pbHelpers/ComponentTypes"; + +export class CO2 { + public settings: pond.ComponentSettings = pond.ComponentSettings.create(); + public status: pond.ComponentStatus = pond.ComponentStatus.create(); + public lastReading: string = ""; + public ppm: number = 0; + + public static create(comp: Component): CO2 { + let my = new CO2(); + my.settings = comp.settings; + my.status = comp.status; + + if (comp.status.lastGoodMeasurement.length > 0) { + comp.status.lastGoodMeasurement.forEach(um => { + if (um.timestamps[0]) { + my.lastReading = um.timestamps[0]; + } + if (um.values[0] && um.values[0].values.length > 0) { + if (um.type === quack.MeasurementType.MEASUREMENT_TYPE_PPM) { + my.ppm = um.values[0].values[0]; + } + } + }); + } + return my; + } + + public static createPond(comp: pond.Component): CO2 { + let my = new CO2(); + my.settings = comp.settings ? comp.settings : pond.ComponentSettings.create(); + my.status = comp.status ? comp.status : pond.ComponentStatus.create(); + + if (comp.status && comp.status.lastGoodMeasurement.length > 0) { + comp.status.lastGoodMeasurement.forEach(um => { + if (um.timestamps[0]) { + my.lastReading = um.timestamps[0]; + } + if (um.values[0] && um.values[0].values.length > 0) { + if (um.type === quack.MeasurementType.MEASUREMENT_TYPE_PPM) { + my.ppm = um.values[0].values[0]; + } + } + }); + } + return my; + } + + public static any(data: any): CO2 { + let comp = pond.Component.fromObject(cloneDeep(data)); + let my = CO2.createPond(comp); + if (data && data.status && data.status.lastMeasurement) { + my.status.lastMeasurement = data.status.lastMeasurement; + } + return my; + } + + public update(other: CO2) { + this.settings = other.settings; + this.status = other.status; + } + + public name(): string { + return this.settings.name !== "" ? this.settings.name : "Component " + this.key(); + } + + public key(): string { + return this.settings.key; + } + + public location(): quack.ComponentID { + return quack.ComponentID.fromObject({ + type: this.settings.type, + addressType: this.settings.addressType, + address: this.settings.address + }); + } + + public locationString(): string { + return ( + or(this.settings.type, 0).toString() + + "-" + + or(this.settings.addressType, 0).toString() + + "-" + + or(this.settings.address, 0).toString() + ); + } + + public type(): quack.ComponentType { + return this.settings.type; + } + + public subType(): number { + return this.settings.subtype; + } + + //return the CO2 as a component + public asComponent(): Component { + let component = Component.create(); + component.settings = this.settings; + component.status = this.status; + return component; + } +} diff --git a/src/pages/AviationMap.tsx b/src/pages/AviationMap.tsx index 450f453..d0a3140 100644 --- a/src/pages/AviationMap.tsx +++ b/src/pages/AviationMap.tsx @@ -6,7 +6,7 @@ export default function AviationMap() { const location = useLocation(); return ( - + {location.state !== undefined && location.state !== null && location.state.long !== undefined && diff --git a/src/pages/Bin.tsx b/src/pages/Bin.tsx index f8a339f..fc9463f 100644 --- a/src/pages/Bin.tsx +++ b/src/pages/Bin.tsx @@ -68,6 +68,7 @@ import { Controller } from "models/Controller"; import TaskViewer from "tasks/TaskViewer"; import ButtonGroup from "common/ButtonGroup"; import BinTransactions from "bin/BinTransactions"; +import { CO2 } from "models/CO2"; interface TabPanelProps { children?: React.ReactNode; @@ -192,7 +193,7 @@ export default function Bin(props: Props) { const [ambients, setAmbients] = useState([]); const [grainCables, setGrainCables] = useState([]); const [pressures, setPressures] = useState([]); - const [headspaceCO2, setHeadspaceCO2] = useState([]); + const [headspaceCO2, setHeadspaceCO2] = useState([]); const [heaters, setHeaters] = useState([]); const [fans, setFans] = useState([]); const [detail, setDetail] = useState< @@ -210,61 +211,12 @@ export default function Bin(props: Props) { new Map() ); const [binPresets, setBinPresets] = useState([]); - const [missedCableReadings, setMissedCableReadings] = useState(0); + const [missedReadings, setMissedReadings] = useState(0); const handleChange = (_event: React.ChangeEvent<{}>, newValue: number) => { setValue(newValue); }; - // const StyledToggle = styled(ToggleButton)(() => ({ - // root: { - // backgroundColor: "transparent", - // overflow: "visible", - // content: "content-box", - // "&$selected": { - // backgroundColor: "gold", - // color: "black", - // borderRadius: 24, - // fontWeight: "bold" - // }, - // "&$selected:hover": { - // backgroundColor: "rgb(255, 255, 0)", - // color: "black", - // borderRadius: 24 - // } - // }, - // selected: {} - // })) - - // const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }: { theme: Theme }) => ({ - // grouped: { - // margin: theme.spacing(-0.5), - // border: "none", - // padding: theme.spacing(1), - // "&:not(:first-child):not(:last-child)": { - // borderRadius: 24, - // marginRight: theme.spacing(0.5), - // marginLeft: theme.spacing(0.5) - // }, - // "&:first-child": { - // borderRadius: 24, - // marginLeft: theme.spacing(0.25) - // }, - // "&:last-child": { - // borderRadius: 24, - // marginRight: theme.spacing(0.25) - // } - // }, - // root: { - // backgroundColor: darken( - // theme.palette.background.paper, - // getThemeType() === "light" ? 0.05 : 0.25 - // ), - // borderRadius: 24, - // content: "border-box" - // } - // })) - const load = useCallback(() => { if (loadRef.current || user.id() === "") return; setBinLoading(true); @@ -364,9 +316,10 @@ export default function Bin(props: Props) { var fans: Controller[] = []; var heaters: Controller[] = []; var pressures: Pressure[] = []; - var headspaceCo2: Component[] = []; + var headspaceCo2: CO2[] = []; var mostMissed: number = 0; let now = moment(); + components.forEach(comp => { let pref = preferences.get(comp.key()); if (pref) { @@ -395,7 +348,17 @@ export default function Bin(props: Props) { pressures.push(Pressure.create(comp)); if (pref.type === pond.BinComponent.BIN_COMPONENT_HEADSPACE) { if (comp.type() === quack.ComponentType.COMPONENT_TYPE_CO2) { - headspaceCo2.push(comp); + //check if there are missed readings from the co2 and compare them to what we already had + let coComp = CO2.create(comp) + let lastRead = moment(coComp.lastReading); + let elapsedMS = now.diff(lastRead); + if (elapsedMS > coComp.settings.measurementPeriodMs) { + let missedReadings = Math.floor(elapsedMS / coComp.settings.measurementPeriodMs); + if (missedReadings > mostMissed) { + mostMissed = missedReadings; + } + } + headspaceCo2.push(coComp); } } } else { @@ -403,7 +366,7 @@ export default function Bin(props: Props) { } } }); - setMissedCableReadings(mostMissed); + setMissedReadings(mostMissed); setPlenums(plenums); setGrainCables(grainCables); setPressures(pressures); @@ -1097,9 +1060,37 @@ export default function Bin(props: Props) { }; const componentState = () => { + const hasCables = grainCables.length > 0 + const hasCO2 = headspaceCO2.length > 0 return ( - + + {missedReadings < 3 + ? "Active" + : missedReadings <= warningThreshold + ? "Missing" + : "Inactive"} + + {missedReadings < 3 ? ( + + ) : missedReadings <= warningThreshold ? ( + + ) : ( + + )} + + : + - {missedCableReadings < 3 - ? "Active" - : missedCableReadings <= warningThreshold - ? "Missing" - : "Inactive"} - - {missedCableReadings < 3 ? ( - - ) : missedCableReadings <= warningThreshold ? ( - - ) : ( - - )} + Unmonitored + + } ); }; return ( - + {showPlenumError()} {page()} : page(); + return !props.insert ? {page()} : page(); } diff --git a/src/pages/ConstructionSiteMap.tsx b/src/pages/ConstructionSiteMap.tsx index 8f1b690..27eaf46 100644 --- a/src/pages/ConstructionSiteMap.tsx +++ b/src/pages/ConstructionSiteMap.tsx @@ -6,7 +6,7 @@ export default function ConstructionSiteMap() { const location = useLocation(); return ( - + {location.state !== undefined && location.state !== null && location.state.long !== undefined && diff --git a/src/pages/Devices.tsx b/src/pages/Devices.tsx index 972f451..db23b6c 100644 --- a/src/pages/Devices.tsx +++ b/src/pages/Devices.tsx @@ -718,7 +718,7 @@ export default function Devices() { ) return( - + title={} subtitle={subtitle} + stickyHeader rows={devices} renderGutter={ isMobile ? gutter : undefined } columns={columns} diff --git a/src/pages/FieldMap.tsx b/src/pages/FieldMap.tsx index 9f8c112..2fc42f4 100644 --- a/src/pages/FieldMap.tsx +++ b/src/pages/FieldMap.tsx @@ -5,7 +5,7 @@ import { useLocation } from "react-router"; export default function FieldMap() { const location = useLocation(); return ( - + {location.state !== undefined && location.state !== null && location.state.long !== undefined && diff --git a/src/pages/PageContainer.tsx b/src/pages/PageContainer.tsx index a8822a7..0c78ed6 100644 --- a/src/pages/PageContainer.tsx +++ b/src/pages/PageContainer.tsx @@ -39,7 +39,7 @@ interface Props extends PropsWithChildren{ fullViewport?: boolean; isCenterCenter?: boolean; sx?: SxProps; - padding?: number; + spacing?: number; } export const PageContainer: React.FunctionComponent = (props: Props) => { @@ -48,7 +48,7 @@ export const PageContainer: React.FunctionComponent = (props: Props) => { const { children, fullViewport, isCenterCenter, sx } = props; // let fullViewport = false // let isCenterCenter = false - const padding = props.padding ? props.padding : isMobile ? 1 : 2; + const spacing = props.spacing; return ( = (props: Props) => { )} sx={{ ...sx, // Spread existing sx - ...(padding !== undefined && { padding }), // Conditionally add padding + paddingX: spacing, + marginTop: spacing + // ...(padding !== undefined && { padding }), // Conditionally add padding }} disableGutters maxWidth={false}