From e654aa002b6dac4cecaed011c7b1d479d394c8a6 Mon Sep 17 00:00:00 2001 From: Carter Date: Fri, 27 Jun 2025 11:33:43 -0600 Subject: [PATCH 01/22] changed default spacing value back to 0, fixed some pageContainers using padding instead of spacing --- src/pages/Bin.tsx | 2 +- src/pages/PageContainer.tsx | 2 +- src/pages/Team.tsx | 2 +- src/pages/Teams.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/Bin.tsx b/src/pages/Bin.tsx index fc9463f..fcc1117 100644 --- a/src/pages/Bin.tsx +++ b/src/pages/Bin.tsx @@ -1107,7 +1107,7 @@ export default function Bin(props: Props) { }; return ( - + {showPlenumError()} = (props: Props) => { const { children, fullViewport, isCenterCenter, sx } = props; // let fullViewport = false // let isCenterCenter = false - const spacing = props.spacing ? props.spacing : 2; + const spacing = props.spacing ?? 0; return ( + {title()} diff --git a/src/pages/Teams.tsx b/src/pages/Teams.tsx index 2e74628..90e3401 100644 --- a/src/pages/Teams.tsx +++ b/src/pages/Teams.tsx @@ -5,7 +5,7 @@ import TeamList from "teams/TeamList"; export default function Teams() { const isMobile = useMobile() return ( - + ); From 24d27121d169b155604b01e9707fb0ff096c2ede Mon Sep 17 00:00:00 2001 From: Carter Date: Thu, 3 Jul 2025 13:41:09 -0600 Subject: [PATCH 02/22] added signup callback page that runs an api call to finish signup --- src/app/main.tsx | 4 +- src/navigation/Router.tsx | 3 ++ src/pages/SignupCallback.tsx | 87 ++++++++++++++++++++++++++++++++++ src/providers/auth.tsx | 49 ++++++++++++------- src/providers/pond/userAPI.tsx | 20 +++++++- 5 files changed, 142 insertions(+), 21 deletions(-) create mode 100644 src/pages/SignupCallback.tsx diff --git a/src/app/main.tsx b/src/app/main.tsx index e2acdd9..2d8615c 100644 --- a/src/app/main.tsx +++ b/src/app/main.tsx @@ -19,7 +19,7 @@ if ('serviceWorker' in navigator) { } createRoot(document.getElementById('root')!).render( - + // - , + // , ) diff --git a/src/navigation/Router.tsx b/src/navigation/Router.tsx index 29362ad..f6badb1 100644 --- a/src/navigation/Router.tsx +++ b/src/navigation/Router.tsx @@ -9,6 +9,7 @@ import { getWhitelabel } from "services/whiteLabel"; import Ventilation from "pages/VentEditor"; import { useGlobalState } from "providers"; import DeviceSupport from "pages/DeviceSupport"; +import SignupCallback from "pages/SignupCallback"; const DeviceHistory = lazy(() => import("pages/DeviceHistory")); const DevicePage = lazy(() => import("pages/Device")); @@ -302,6 +303,8 @@ export default function Router() { {/* Redirects */} } /> + {/* } /> */} + } /> {/* } /> */} {/* } /> */} {/* } /> */} diff --git a/src/pages/SignupCallback.tsx b/src/pages/SignupCallback.tsx new file mode 100644 index 0000000..78215f4 --- /dev/null +++ b/src/pages/SignupCallback.tsx @@ -0,0 +1,87 @@ +import { useDeviceAPI, useUserAPI } from "hooks"; +import PageContainer from "./PageContainer"; +import { useEffect, useState } from "react"; +import { Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider } from "@mui/material"; +import { useBinAPI, useTeamAPI } from "providers"; +import { CheckCircleOutline } from "@mui/icons-material"; +import { green } from "@mui/material/colors"; + +interface Props { + +} + +export default function SignupCallback (props: Props) { + const userAPI = useUserAPI(); + const teamAPI = useTeamAPI() + const deviceAPI = useDeviceAPI(); + const binAPI = useBinAPI(); + const [loading, setLoading] = useState(false) + const [signupDialogOpen, setSignupDialogOpen] = useState(true) + const [hasTeams, setHasTeams] = useState(false) + const [hasDevices, setHasDevices] = useState(false) + const [hasBins, setHasBins] = useState(false) + + const [doneBins, setDoneBins] = useState(false) + const [doneTeams, setDoneTeams] = useState(false) + const [doneDevices, setDoneDevices] = useState(false) + + useEffect(() => { + console.log(hasTeams) + }, [hasTeams]) + + useEffect(() => { + setLoading(true) + userAPI.completeSignup().then(resp => { + console.log(resp) + teamAPI.listTeams(1, 0).then(resp => { + setHasTeams(resp.data.teams.length > 0) + }).finally(() => setDoneTeams(true)) + deviceAPI.list(1, 0).then(resp => { + setHasDevices(resp.data.devices.length > 0) + }).finally(() => setDoneDevices(true)) + binAPI.listBins(1, 0).then(resp => { + setHasBins(resp.data.bins.length > 0) + }).finally(() => setDoneBins(true)) + }).finally(() => setLoading(false)) + }, []) + + const loadingComplete = () => { + return loading === false && doneBins && doneTeams && doneDevices + } + + const content = () => { + return ( + + { loadingComplete() ? + + : + + } + + {hasTeams && "Teams found!"} + {hasDevices && "Devices found!"} + {hasBins && "Bins found!"} + + + ) + } + + return ( + + + + Finishing Signup + + + { content() } + + {loadingComplete() && + + } + + + + ) +} \ No newline at end of file diff --git a/src/providers/auth.tsx b/src/providers/auth.tsx index 22ac76d..f7ee585 100644 --- a/src/providers/auth.tsx +++ b/src/providers/auth.tsx @@ -9,28 +9,30 @@ interface Props extends PropsWithChildren{ export default function AuthWrapper(props: Props) { const { children, setToken } = props; - const { isLoading, loginWithRedirect, isAuthenticated, getAccessTokenSilently } = useAuth0(); + const { isLoading, loginWithRedirect, isAuthenticated, getAccessTokenSilently, getIdTokenClaims } = useAuth0(); useEffect(() => { const url = new URL(window.location.href); - const isManualLoginPage = url.pathname.includes("/login"); + const options: RedirectLoginOptions = { + authorizationParams: {} // Initialize here + }; - if (isManualLoginPage) { - const options: RedirectLoginOptions = { - authorizationParams: {} // Initialize here - }; - + if (isManualLoginPage && options.authorizationParams) { + let parsed = queryString.parse(url.search); if (parsed.signup === "true") { - options.authorizationParams!.screen_hint = "signup"; - options.authorizationParams!.prompt = "login"; + options.authorizationParams.screen_hint = "signUp"; + // options.authorizationParams.m + // options.authorizationParams.prompt = "login"; } if (parsed.email && parsed.email !== "") { - options.authorizationParams!.login_hint = parsed.email.toString(); // prefill email + options.authorizationParams.login_hint = parsed.email.toString(); // prefill email } + + options.authorizationParams.redirect_uri = "http://localhost:5173/signupCallback" loginWithRedirect(options) @@ -40,15 +42,28 @@ export default function AuthWrapper(props: Props) { }, [isAuthenticated, loginWithRedirect, isLoading]); useEffect(() => { - if (isAuthenticated) getAccessTokenSilently().then(t => { - if (t.length < 1) { + const url = new URL(window.location.href); + const isSignupCallback = url.pathname.includes("/signupCallback"); + if (isAuthenticated) { + getAccessTokenSilently().then(t => { + if (t.length < 1) { + loginWithRedirect() + } + setToken(t) + }).catch(() => { + // No token, go back to login loginWithRedirect() + }) + if (isSignupCallback) { + console.log("IS SIGNUP") + getIdTokenClaims().then(id_claims => { + console.log(id_claims) + console.log(id_claims?.nickname) + if (id_claims) console.log(Object.keys(id_claims)) + }) } - setToken(t) - }).catch(() => { - // No token, go back to login - loginWithRedirect() - }) + + } }, [setToken, isAuthenticated]) return children; diff --git a/src/providers/pond/userAPI.tsx b/src/providers/pond/userAPI.tsx index 247f77c..21befb2 100644 --- a/src/providers/pond/userAPI.tsx +++ b/src/providers/pond/userAPI.tsx @@ -28,6 +28,7 @@ export interface IUserAPIContext { prefixSearch?: string, ) => Promise; listProfiles: () => Promise; + completeSignup: () => Promise; listObjectUsers: (scope: Scope) => Promise; updateObjectUsers: (scope: Scope, users: pond.IUser[]) => Promise; getUser: (id: string, scope?: Scope) => Promise; @@ -55,8 +56,22 @@ export const UserAPIContext = createContext({} as IUserAPIConte export default function UserProvider(props: PropsWithChildren) { const { children } = props; - const { get, put } = useHTTP(); - const [{ as }] = useGlobalState(); + const { get, put, post } = useHTTP(); + const [{ as, user }] = useGlobalState(); + + const completeSignup = (): Promise => { + return new Promise((resolve, reject) => { + let request = pond.CompleteSignupUserRequest.create(); + request.avatar = user.settings.avatar + request.email = user.settings.email + request.id = user.id() + request.name = user.name() + post(pondURL("/signups"), request).then(res => { + console.log(res) + resolve(pond.CompleteSignupUserResponse.create()) + }) + }) + } const listUsers = ( limit: number, @@ -234,6 +249,7 @@ export default function UserProvider(props: PropsWithChildren) { return ( Date: Mon, 7 Jul 2025 11:57:53 -0600 Subject: [PATCH 03/22] added dark streamline logo, minor styling adjustments --- public/Streamline/stream-logo-dark.png | Bin 0 -> 26259 bytes src/device/SyncDevice.tsx | 5 ++++- src/pages/SignupCallback.tsx | 10 +++------- 3 files changed, 7 insertions(+), 8 deletions(-) create mode 100644 public/Streamline/stream-logo-dark.png diff --git a/public/Streamline/stream-logo-dark.png b/public/Streamline/stream-logo-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..a486c1e727ee6ec98c99138cca3cb6ebee18898e GIT binary patch literal 26259 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 { + close() + }) }; return ( diff --git a/src/pages/SignupCallback.tsx b/src/pages/SignupCallback.tsx index 78215f4..00b2c51 100644 --- a/src/pages/SignupCallback.tsx +++ b/src/pages/SignupCallback.tsx @@ -1,4 +1,4 @@ -import { useDeviceAPI, useUserAPI } from "hooks"; +import { useDeviceAPI, useMobile, useUserAPI } from "hooks"; import PageContainer from "./PageContainer"; import { useEffect, useState } from "react"; import { Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider } from "@mui/material"; @@ -15,6 +15,7 @@ export default function SignupCallback (props: Props) { const teamAPI = useTeamAPI() const deviceAPI = useDeviceAPI(); const binAPI = useBinAPI(); + const isMobile = useMobile() const [loading, setLoading] = useState(false) const [signupDialogOpen, setSignupDialogOpen] = useState(true) const [hasTeams, setHasTeams] = useState(false) @@ -25,14 +26,9 @@ export default function SignupCallback (props: Props) { const [doneTeams, setDoneTeams] = useState(false) const [doneDevices, setDoneDevices] = useState(false) - useEffect(() => { - console.log(hasTeams) - }, [hasTeams]) - useEffect(() => { setLoading(true) userAPI.completeSignup().then(resp => { - console.log(resp) teamAPI.listTeams(1, 0).then(resp => { setHasTeams(resp.data.teams.length > 0) }).finally(() => setDoneTeams(true)) @@ -68,7 +64,7 @@ export default function SignupCallback (props: Props) { return ( - + Finishing Signup From 67cafbe2ed9c189d2959083101e961114c07b013 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Mon, 7 Jul 2025 14:11:30 -0600 Subject: [PATCH 04/22] refactor the bin level over time the auto inventory stuff pulls from the measurements and the manual/hybrid stuff pulls from the history, the auto will also create a new chart underneath for the bin modes --- src/bin/graphs/BinLevelOverTime.tsx | 223 ++++++++++++++++------------ src/charts/BarGraph.tsx | 6 +- 2 files changed, 133 insertions(+), 96 deletions(-) diff --git a/src/bin/graphs/BinLevelOverTime.tsx b/src/bin/graphs/BinLevelOverTime.tsx index cefb99e..8aabf93 100644 --- a/src/bin/graphs/BinLevelOverTime.tsx +++ b/src/bin/graphs/BinLevelOverTime.tsx @@ -13,7 +13,7 @@ import { Bin } from "models"; import moment, { Moment } from "moment"; import { pond, quack } from "protobuf-ts/pond"; import { useBinAPI, useGlobalState } from "providers"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { Legend } from "recharts"; import { getGrainUnit } from "utils"; @@ -31,24 +31,13 @@ export default function BinLevelOverTime(props: Props) { const { bin, fertilizerBin, colour, startDate, endDate, customHeight } = props; const binAPI = useBinAPI(); const [{as}] = useGlobalState(); - const [showCable, setShowCable] = useState(false); - const [manualData, setManualData] = useState([]); - const [cableData, setCableData] = useState([]); + const [data, setData] = useState([]); const [modeAreas, setModeAreas] = useState([]); - // const defaultDateRange = GetDefaultDateRange(); - // const [startDate, setStartDate] = useState(defaultDateRange.start); - // const [endDate, setEndDate] = useState(defaultDateRange.end); const [dataLoading, setDataLoading] = useState(false); - const [cableDataLoading, setCableDataLoading] = useState(false); const [capacity, setCapacity] = useState(); - - // const updateDateRange = (newStartDate: any, newEndDate: any) => { - // let range = GetDefaultDateRange(); - // range.start = newStartDate; - // range.end = newEndDate; - // setStartDate(newStartDate); - // setEndDate(newEndDate); - // }; + + const [modeData, setModeData] = useState([]); + const [histDataLoading, setHistDataLoading] = useState(false); const getFill = (mode: pond.BinMode) => { let fill = ""; @@ -72,8 +61,8 @@ export default function BinLevelOverTime(props: Props) { return fill; }; - useEffect(() => { - if (dataLoading || bin.key() === "") return; + //loads the data from the bin history table + const loadHistoryData = useCallback(() => { setDataLoading(true); let cap = bin.settings.specs?.bushelCapacity; if (fertilizerBin && cap) { @@ -146,48 +135,125 @@ export default function BinLevelOverTime(props: Props) { fill: getFill(bin.settings.mode) }); } - setManualData(data); + setData(data); setModeAreas(modeAreas); }) .finally(() => { setDataLoading(false); }); - }, [binAPI, bin, startDate, endDate, fertilizerBin]); // eslint-disable-line react-hooks/exhaustive-deps + },[startDate, endDate, bin, fertilizerBin]) - useEffect(() => { - if (cableDataLoading || bin.key() === "") return; - setCableDataLoading(true); + //loads the data from the measuremnts table as a unit measurement + const loadMeasurementData = useCallback(() => { + setDataLoading(true); + let cap = bin.settings.specs?.bushelCapacity; + if (fertilizerBin && cap) { + cap = cap * 35.239; + } + if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && cap) { + cap = cap / bin.bushelsPerTonne(); + } + setCapacity(cap); + let autoBarData: BarData[] = []; + + //load the measurment data to build the bushel bars binAPI - .listBinMeasurements(bin.key(), startDate, endDate, 500, 0, "asc", undefined, undefined, undefined, undefined, as) + .listBinMeasurements(bin.key(), startDate, endDate, 500, 0, "asc") .then(resp => { + let lastBushels = -1; + resp.data.measurements.forEach(objMeasurement => { //set the bushel cable measurements let m = pond.UnitMeasurementsForObject.fromObject(objMeasurement); - if (m.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_BUSHEL_CABLE) { - let cableBarData: BarData[] = []; - let lastBushels = -1; - //val will be an array of only a single number since this is not a measurement that involves nodes - m.values.forEach((val, i) => { - if (val.values[0] && m.timestamps[i]) { - if (lastBushels !== val.values[0]) { - cableBarData.push({ - value: val.values[0], - timestamp: moment(m.timestamps[i]).valueOf() - }); - lastBushels = val.values[0]; - } + //val will be an array of only a single number since this is not a measurement that involves nodes + m.values.forEach((val, i) => { + if (val.values[0] && m.timestamps[i]) { + if (lastBushels !== val.values[0]) { + autoBarData.push({ + value: val.values[0], + timestamp: moment(m.timestamps[i]).valueOf(), + }); + lastBushels = val.values[0]; } - }); - setCableData(cableBarData); - } + } + }); }); + if (autoBarData.length === 0) { + let bushels = bin.settings.inventory?.grainBushels ?? 0; + let currentTime = moment().valueOf(); + autoBarData.push({ + value: fertilizerBin + ? Math.round(bushels * 35.239) + : getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT + ? Math.round((bushels / bin.bushelsPerTonne()) * 100) / 100 + : bushels, + timestamp: currentTime + }); + } + setData(autoBarData); }) .catch(err => {}) .finally(() => { - setCableDataLoading(false); + setDataLoading(false); }); - }, [binAPI, bin, startDate, endDate, fertilizerBin, as]); // eslint-disable-line react-hooks/exhaustive-deps + },[startDate, endDate, bin, fertilizerBin]) + + const modeDataFromHistory = useCallback(()=>{ + if (histDataLoading) return; + setHistDataLoading(true) + binAPI + .listHistoryBetween(bin.key(), 500, startDate.toISOString(), endDate.toISOString()) + .then(resp => { + //loop through the history finding all the times the bin mode changed + let currentMode: pond.BinMode | undefined = undefined + let modeData: BarData[] = [] + resp.data.history.forEach(hist => { + 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) + }) + } + }) + setModeData(modeData) + }) + .catch(err => { + + }) + .finally(()=>{ + setHistDataLoading(false) + }) + },[bin, startDate, endDate]) + + + useEffect(() => { + //dont call a load function if loading is already in progress + if (dataLoading || bin.key() === "") return; + //check what is controlling the bin inventory + let control = bin.inventoryControl() + //for automatic lidar and cables get the data from measurements as an object measurement + if(control === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR || + control === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC + ){ + //load the measurement data for the bar graph + loadMeasurementData() + //load the data for the storage mode changes + modeDataFromHistory() + + //for manual and hybrid (lidar or in the future, cables) get the data from bin_history + }else if(control === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_MANUAL || + control === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_HYBRID_LIDAR || + control === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_UNKNOWN + ){ + loadHistoryData() + } + + },[bin, loadMeasurementData, loadHistoryData]) const legend = () => { return ( @@ -203,15 +269,16 @@ export default function BinLevelOverTime(props: Props) { ); }; - const manualChart = () => { + const inventoryChart = () => { return ( 0 ? legend() : undefined} labelColour="white" labels useGradient @@ -219,62 +286,28 @@ export default function BinLevelOverTime(props: Props) { ); }; - const cableChart = () => { - if (cableData.length === 0) { - return ( - - - No cable estimates for selected time frame - - - ); - } + const autoBinModeChart = () => { return ( - - ); - }; + ) + } return ( - - - - Grain Levels Over Time - - - - { - setShowCable(checked); - }} - color="primary" - /> - } - label="Cable Estimates" - /> - - - - {dataLoading ? : showCable ? cableChart() : manualChart()} + + Grain Levels Over Time + + {dataLoading ? : inventoryChart()} + {/* when using auto will need to make a new chart just for the bin modes since the auto inventory comes from another place */} + {(bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR || + bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC) && autoBinModeChart()} ); } diff --git a/src/charts/BarGraph.tsx b/src/charts/BarGraph.tsx index 8e0ab82..f8bd48e 100644 --- a/src/charts/BarGraph.tsx +++ b/src/charts/BarGraph.tsx @@ -15,6 +15,7 @@ import { export interface BarData { timestamp: number; value: number; + fill?: string; } export interface RefArea { @@ -36,8 +37,9 @@ export interface AxisLabel { interface Props { data: BarData[]; - barColour: string; + barColour?: string; refAreas?: RefArea[]; + hideYAxis?: boolean; yMin?: number; yMax?: number; graphLegend?: JSX.Element; @@ -60,6 +62,7 @@ export default function BarGraph(props: Props) { data, barColour, refAreas, + hideYAxis, yMin, yMax, graphLegend, @@ -103,6 +106,7 @@ export default function BarGraph(props: Props) { {graphLegend} Date: Tue, 8 Jul 2025 14:07:01 -0600 Subject: [PATCH 05/22] added a welcome tour to the signupCallback page --- package-lock.json | 56 ++++++++++++ package.json | 1 + src/common/Tour.tsx | 30 +++++-- src/navigation/SideNavigator.tsx | 8 +- src/pages/Firmware.tsx | 4 +- src/pages/SignupCallback.tsx | 148 +++++++++++++++++++++++++++++-- src/pages/Users.tsx | 2 +- src/providers/auth.tsx | 27 +++--- src/providers/pond/userAPI.tsx | 1 - src/user/UserMenu.tsx | 2 +- 10 files changed, 247 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index 65ae1cc..840ea14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "react-color": "^2.19.3", "react-day-picker": "^9.6.5", "react-dom": "^18.3.1", + "react-emoji-render": "^2.0.1", "react-error-boundary": "^5.0.0", "react-full-screen": "^1.1.1", "react-horizontal-scrolling-menu": "^7.1.1", @@ -9188,6 +9189,15 @@ "node": ">=12" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -9971,6 +9981,18 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -11107,6 +11129,29 @@ "react": "^18.3.1" } }, + "node_modules/react-emoji-render": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/react-emoji-render/-/react-emoji-render-2.0.1.tgz", + "integrity": "sha512-SKtsdwgEf2BFNiE9y4UBFZBWjkRcyWmhMprVly52+J77/zxThcfaQ3sCA0+2LtGJIRMpm4DGWSvyLg72fd1rXQ==", + "license": "MIT", + "dependencies": { + "classnames": "^2.2.5", + "emoji-regex": "^8.0.0", + "lodash.flatten": "^4.4.0", + "prop-types": "^15.5.8", + "string-replace-to-array": "^1.0.1" + }, + "peerDependencies": { + "react": ">=0.14.0", + "react-dom": ">=0.14.0" + } + }, + "node_modules/react-emoji-render/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/react-error-boundary": { "version": "5.0.0", "integrity": "sha512-tnjAxG+IkpLephNcePNA7v6F/QpWLH8He65+DmedchDwg162JZqx4NmbXj0mlAYVVEd81OW7aFhmbsScYfiAFQ==", @@ -12408,6 +12453,17 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-replace-to-array": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string-replace-to-array/-/string-replace-to-array-1.0.3.tgz", + "integrity": "sha512-QIdKvmfXbdFvblXAAz6IIjR7A+C6SU6m2A+e7fE/0EYDC5yfeWNMJQ193fPsW7nG+9q52dv/UjnVrDaNVZXpmQ==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.1", + "lodash.flatten": "^4.2.0", + "lodash.isstring": "^4.0.1" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", diff --git a/package.json b/package.json index 67d05d8..afae078 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "react-color": "^2.19.3", "react-day-picker": "^9.6.5", "react-dom": "^18.3.1", + "react-emoji-render": "^2.0.1", "react-error-boundary": "^5.0.0", "react-full-screen": "^1.1.1", "react-horizontal-scrolling-menu": "^7.1.1", diff --git a/src/common/Tour.tsx b/src/common/Tour.tsx index 4781e21..9d328a7 100644 --- a/src/common/Tour.tsx +++ b/src/common/Tour.tsx @@ -1,4 +1,4 @@ -import Joyride, { Step, CallBackProps, STATUS, TooltipRenderProps, Locale } from "react-joyride"; +import Joyride, { Step, CallBackProps, STATUS, TooltipRenderProps, Locale, ACTIONS, EVENTS } from "react-joyride"; import { Button, Card, @@ -10,12 +10,13 @@ import { Tooltip, IconButton, Theme, - useTheme + useTheme, } from "@mui/material"; import { KeyboardArrowLeft, KeyboardArrowRight, Close as CloseIcon } from "@mui/icons-material"; import classNames from "classnames"; import { grey } from "@mui/material/colors"; import { makeStyles } from "@mui/styles"; +import { CSSProperties } from "react"; const useStyles = makeStyles((theme: Theme) => { return ({ @@ -46,6 +47,10 @@ const useStyles = makeStyles((theme: Theme) => { }) }); +export interface TourStep extends Step { + onNext?: () => void; // Define onNext as an optional function +} + const StepContainer = (props: TooltipRenderProps, numSteps: number, skip: () => void) => { const { continuous, @@ -114,18 +119,30 @@ const StepContainer = (props: TooltipRenderProps, numSteps: number, skip: () => interface Props { run: boolean; - steps: Step[]; + steps: TourStep[]; endTourCallback: () => void; + spotlightStyle?: CSSProperties, + setStepIndex?: React.Dispatch>, } export default function Tour(props: Props) { - const { run, steps, endTourCallback } = props; + const { run, steps, endTourCallback, setStepIndex, spotlightStyle } = props; const theme = useTheme(); const handleTourCallback = (data: CallBackProps) => { - const { status } = data; + const { action, index, status, type } = data; const finishedStatuses: string[] = [STATUS.FINISHED, STATUS.SKIPPED]; + if (setStepIndex) setStepIndex(index) + + if (action === ACTIONS.NEXT && type === EVENTS.STEP_AFTER) { + // Safely access the step and call onNext if it exists + const currentStep = steps[index]; + if (currentStep.onNext) { + currentStep.onNext(); + } + } + if (finishedStatuses.includes(status)) { endTourCallback(); } @@ -150,6 +167,9 @@ export default function Tour(props: Props) { primaryColor: theme.palette.primary.main, textColor: theme.palette.text.primary, zIndex: theme.zIndex.modal + }, + spotlight: { + ...spotlightStyle } }} /> diff --git a/src/navigation/SideNavigator.tsx b/src/navigation/SideNavigator.tsx index 38a10cb..f71ca9c 100644 --- a/src/navigation/SideNavigator.tsx +++ b/src/navigation/SideNavigator.tsx @@ -221,7 +221,7 @@ export default function SideNavigator(props: Props) { {(isAg || isStreamline || user.hasFeature("admin")) && ( goTo("/bins")} classes={getClasses("/bin")} > @@ -277,7 +277,7 @@ export default function SideNavigator(props: Props) { {(isMiVent || user.hasFeature("admin")) && ( goTo("/mines")} classes={getClasses("/mine")} > @@ -330,7 +330,7 @@ export default function SideNavigator(props: Props) { } goTo("/teams")} classes={getClasses("/team")} > @@ -342,7 +342,7 @@ export default function SideNavigator(props: Props) { goTo("/users")} classes={getClasses("/user")} > diff --git a/src/pages/Firmware.tsx b/src/pages/Firmware.tsx index b3a7c58..1ae97af 100644 --- a/src/pages/Firmware.tsx +++ b/src/pages/Firmware.tsx @@ -12,7 +12,7 @@ import { TypeBackground } from "@mui/material"; import { blue, green, orange, red } from "@mui/material/colors"; -import { createStyles, makeStyles } from "@mui/styles"; +import { makeStyles } from "@mui/styles"; //import { TypeBackground } from "@material-ui/core/styles/createPalette"; import { ArrowUpward, @@ -267,7 +267,7 @@ export default function Firmware() { if (selected) { firmwareAPI .removeFirmware(selected.settings.platform, selected.settings.version) - .then((response: any) => { + .then(() => { success("Successfully removed firmware"); }) .catch((err: any) => { diff --git a/src/pages/SignupCallback.tsx b/src/pages/SignupCallback.tsx index 00b2c51..a22dbfc 100644 --- a/src/pages/SignupCallback.tsx +++ b/src/pages/SignupCallback.tsx @@ -1,26 +1,36 @@ import { useDeviceAPI, useMobile, useUserAPI } from "hooks"; import PageContainer from "./PageContainer"; import { useEffect, useState } from "react"; -import { Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider } from "@mui/material"; -import { useBinAPI, useTeamAPI } from "providers"; +import { Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, Typography } from "@mui/material"; +import { useBinAPI, useGlobalState, useTeamAPI } from "providers"; import { CheckCircleOutline } from "@mui/icons-material"; import { green } from "@mui/material/colors"; +import Tour, { TourStep } from "common/Tour"; +import Emoji from "react-emoji-render"; +import { getWhitelabel, IsAdaptiveAgriculture } from "services/whiteLabel"; +// import { color } from "framer-motion"; -interface Props { +// interface Props { -} +// } -export default function SignupCallback (props: Props) { +// export default function SignupCallback (props: Props) { +export default function SignupCallback () { const userAPI = useUserAPI(); const teamAPI = useTeamAPI() const deviceAPI = useDeviceAPI(); const binAPI = useBinAPI(); const isMobile = useMobile() + const [{ user }] = useGlobalState(); + const whiteLabel = getWhitelabel() + const [loading, setLoading] = useState(false) const [signupDialogOpen, setSignupDialogOpen] = useState(true) const [hasTeams, setHasTeams] = useState(false) const [hasDevices, setHasDevices] = useState(false) const [hasBins, setHasBins] = useState(false) + const [isTourRunning, setIsTourRunning] = useState(false); + const [stepIndex, setStepIndex] = useState(0) const [doneBins, setDoneBins] = useState(false) const [doneTeams, setDoneTeams] = useState(false) @@ -28,7 +38,7 @@ export default function SignupCallback (props: Props) { useEffect(() => { setLoading(true) - userAPI.completeSignup().then(resp => { + userAPI.completeSignup().then(() => { teamAPI.listTeams(1, 0).then(resp => { setHasTeams(resp.data.teams.length > 0) }).finally(() => setDoneTeams(true)) @@ -55,13 +65,131 @@ export default function SignupCallback (props: Props) { } {hasTeams && "Teams found!"} +
{hasDevices && "Devices found!"} +
{hasBins && "Bins found!"}
) } + const getTourSteps = (): TourStep[] => { + let steps: TourStep[] = [ + { + title: ( + <> + Welcome to {whiteLabel.name} + + + ), + content: ( + <> + + {"Hello " + user.name()} + + + + Thank you for signing up! Let me give you the tour + + + + ), + target: "body", + placement: "center", + disableBeacon: true, + }, + { + title: "User Menu", + content: ( + <> + + In the user menu, you can make changes to your profile, and adjust unit preferences. + +
+ + If you are part of a team, you can select it here as well. + + + ), + target: "#tour-user-menu", + placement: "bottom", + disableBeacon: true, + // onNext: () => setDoneFirst(true) + }, + // { + // title: + // } + ]; + if (hasTeams) steps.push({ + title: "Teams", + content: ( + <> + + Here, you can view your team(s), access their settings, and choose your favourite. + + + ), + target: "#tour-teams", + placement: "right", + // spotlightPadding: 50, + }) + steps.push({ + title: "Devices", + content: ( + <> + + You can view your devices and their readings here. + +
+ + If you are viewing as a team, your team's devices will be displayed here instead. + + + ), + target: "#tour-dashboard", + placement: "right" + }) + if (IsAdaptiveAgriculture()) steps.push({ + title: "Bins", + content: ( + <> + + You can view your bins and their inventory here. + +
+ + If you are viewing as a team, your team's bins will be displayed here instead. + + + ), + target: "#tour-bins", + placement: "right", + disableBeacon: true, + }) + return steps; + }; + + const endTour = () => { + setIsTourRunning(false); + // if (user) { + // user.status.finishedBinIntro = moment().toJSON(); + // userAPI + // .updateUser(userID, user.protobuf()) + // .then(() => dispatch({ key: "user", value: user })) + // .catch((err: any) => error(err)); + // } + }; + + const closeDialog = () => { + setSignupDialogOpen(false) + setIsTourRunning(true) + } + + useEffect(() => { + console.log(stepIndex) + }, [stepIndex]) + return ( @@ -72,12 +200,18 @@ export default function SignupCallback (props: Props) { { content() } {loadingComplete() && - } + ) } \ No newline at end of file diff --git a/src/pages/Users.tsx b/src/pages/Users.tsx index 00ef9f7..296c60a 100644 --- a/src/pages/Users.tsx +++ b/src/pages/Users.tsx @@ -349,7 +349,7 @@ export default function Users() { } return ( - + title="Users" rows={rows} diff --git a/src/providers/auth.tsx b/src/providers/auth.tsx index f7ee585..2a8f6bb 100644 --- a/src/providers/auth.tsx +++ b/src/providers/auth.tsx @@ -9,7 +9,13 @@ interface Props extends PropsWithChildren{ export default function AuthWrapper(props: Props) { const { children, setToken } = props; - const { isLoading, loginWithRedirect, isAuthenticated, getAccessTokenSilently, getIdTokenClaims } = useAuth0(); + const { + isLoading, + loginWithRedirect, + isAuthenticated, + getAccessTokenSilently, + // getIdTokenClaims + } = useAuth0(); useEffect(() => { @@ -42,8 +48,8 @@ export default function AuthWrapper(props: Props) { }, [isAuthenticated, loginWithRedirect, isLoading]); useEffect(() => { - const url = new URL(window.location.href); - const isSignupCallback = url.pathname.includes("/signupCallback"); + // const url = new URL(window.location.href); + // const isSignupCallback = url.pathname.includes("/signupCallback"); if (isAuthenticated) { getAccessTokenSilently().then(t => { if (t.length < 1) { @@ -54,14 +60,13 @@ export default function AuthWrapper(props: Props) { // No token, go back to login loginWithRedirect() }) - if (isSignupCallback) { - console.log("IS SIGNUP") - getIdTokenClaims().then(id_claims => { - console.log(id_claims) - console.log(id_claims?.nickname) - if (id_claims) console.log(Object.keys(id_claims)) - }) - } + // if (isSignupCallback) { + // getIdTokenClaims().then(id_claims => { + // console.log(id_claims) + // console.log(id_claims?.nickname) + // if (id_claims) console.log(Object.keys(id_claims)) + // }) + // } } }, [setToken, isAuthenticated]) diff --git a/src/providers/pond/userAPI.tsx b/src/providers/pond/userAPI.tsx index 21befb2..0d33b2e 100644 --- a/src/providers/pond/userAPI.tsx +++ b/src/providers/pond/userAPI.tsx @@ -67,7 +67,6 @@ export default function UserProvider(props: PropsWithChildren) { request.id = user.id() request.name = user.name() post(pondURL("/signups"), request).then(res => { - console.log(res) resolve(pond.CompleteSignupUserResponse.create()) }) }) diff --git a/src/user/UserMenu.tsx b/src/user/UserMenu.tsx index 2b41a6f..c7b8ab3 100644 --- a/src/user/UserMenu.tsx +++ b/src/user/UserMenu.tsx @@ -228,7 +228,7 @@ export default function UserMenu() { */} {hasTeams && ( - + From 0127eeefc75d40e450eb6ab525a779f94a669191 Mon Sep 17 00:00:00 2001 From: Carter Date: Wed, 16 Jul 2025 13:32:08 -0600 Subject: [PATCH 06/22] view as team notification pops up if they don't have devices or bins but have a team --- src/pages/Bins.tsx | 76 +++++++++++++++++++++++++------------ src/pages/DeviceSupport.tsx | 5 ++- src/pages/Devices.tsx | 26 ++++++++++++- src/teams/TeamDialog.tsx | 58 +++++++++++++++++++++++++--- 4 files changed, 130 insertions(+), 35 deletions(-) diff --git a/src/pages/Bins.tsx b/src/pages/Bins.tsx index 8f77613..476b66f 100644 --- a/src/pages/Bins.tsx +++ b/src/pages/Bins.tsx @@ -18,7 +18,6 @@ import { ImageList, ImageListItem, InputLabel, - lighten, ListItemIcon, ListItemText, Menu, @@ -26,8 +25,6 @@ import { Select, Skeleton, Theme, - ToggleButton, - ToggleButtonGroup, Typography, useTheme, } from "@mui/material"; @@ -61,7 +58,8 @@ import { // useBinYardAPI, useGlobalState, useInteractionsAPI, - useSnackbar + useSnackbar, + useTeamAPI } from "providers"; import React, { SetStateAction, useCallback, useEffect, useState } from "react"; // import { getThemeType } from "theme"; @@ -72,7 +70,7 @@ import ObjectTable from "objects/ObjectTable"; import ResponsiveDialog from "common/ResponsiveDialog"; import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; import { green, yellow } from "@mui/material/colors"; -import { makeStyles, styled } from "@mui/styles"; +import { makeStyles } from "@mui/styles"; import { useNavigate } from "react-router-dom"; import BinSettings from "bin/BinSettings"; import AddBinFab from "bin/AddBinFab"; @@ -82,6 +80,7 @@ import GrainBagSettings from "grainBag/grainBagSettings"; import GrainBagList from "grainBag/grainBagList"; import BinYards from "bin/BinYards"; import ButtonGroup from "common/ButtonGroup"; +import TeamDialog from "teams/TeamDialog"; // import { useHistory } from "react-router"; const useStyles = makeStyles((theme: Theme) => { @@ -141,11 +140,13 @@ export default function Bins(props: Props) { const classes = useStyles(); const isMobile = useMobile(); const binAPI = useBinAPI(); + const teamAPI = useTeamAPI(); // const binyardAPI = useBinYardAPI(); const interactionsAPI = useInteractionsAPI(); const [{ user, as }] = useGlobalState(); const [allLoading, setAllLoading] = useState(false); const [binsLoading, setBinsLoading] = useState(false); + const [doneLoadingPage, setDoneLoadingPage] = useState(false) const [addBinOpen, setAddBinOpen] = useState(false); const [paginatedBins, setPaginatedBins] = useState({ bins: [], @@ -188,13 +189,16 @@ export default function Bins(props: Props) { const [qrKeyList, setQrKeyList] = useState([]); const [totalFanControllers, setTotalFanControllers] = useState(0); const [totalFanControllersOn, setTotalFanControllersOn] = useState(0); - const [yardsLoading, setYardsLoading] = useState(false); + // const [yardsLoading, setYardsLoading] = useState(false); const [openAddAlerts, setOpenAddAlerts] = useState(false); const [alertBins, setAlertBins] = useState([]); const { openSnack } = useSnackbar(); // const history = useHistory(); const navigate = useNavigate() const [cardValDisplay, setCardValDisplay] = useState<"high" | "low" | "average">("average"); + + const [teamsDialog, setTeamsDialog] = useState(false) + // const [scrollTranslations, setScrollTranslations] = useState({ // bins: 0, // empty: 0, @@ -252,6 +256,18 @@ export default function Bins(props: Props) { // selected: {} // })); + useEffect(() => { + if (doneLoadingPage && grainBins.length < 1 && grainBags.length < 1) { + teamAPI.listTeams(1, 0).then(resp => { + // console.log(resp.data.teams) + if (resp.data.teams.length > 0) { + // console.log("should?") + setTeamsDialog(true) + } + }) + } + }, [doneLoadingPage, grainBins, grainBags]) + useEffect(() => { let ebt = sessionStorage.getItem("expandBinTotal"); if (ebt === "true") { @@ -400,7 +416,7 @@ export default function Bins(props: Props) { const bags = resp.data.bags.map(b => GrainBag.create(b)); setGrainBags(bags); }) - .catch(err => { + .catch(() => { setPaginatedBins({ bins: [], binsOffset: 0, binsTotal: 0 }); setBinMetrics(pond.BinMetrics.create()); setYards([]); @@ -408,6 +424,7 @@ export default function Bins(props: Props) { .finally(() => { setAllLoading(false); setBinsLoading(false); + setDoneLoadingPage(true) }); }, [binAPI, yardFilter, order, orderBy, as, loadingAs, loadingFilter]); // eslint-disable-line react-hooks/exhaustive-deps @@ -454,7 +471,7 @@ export default function Bins(props: Props) { setFertilizerBins([...fert]); setQrKeyList([...newKeyList]); }) - .catch(err => { + .catch(() => { setPaginatedBins({ bins: [], binsOffset: 0, binsTotal: 0 }); }) .finally(() => { @@ -462,13 +479,13 @@ export default function Bins(props: Props) { }); }; - const searchYards = ( - search?: string, - limit?: number, - offset?: number, - order?: "asc" | "desc" - ) => { - setYardsLoading(true); + // const searchYards = ( + // search?: string, + // limit?: number, + // offset?: number, + // order?: "asc" | "desc" + // ) => { + // setYardsLoading(true); // binyardAPI.listBinYards(limit ?? binsLimit, offset ?? 0, order, "name", search).then(resp => { // let y: pond.BinYardSettings[] = []; // let p: Dictionary = {}; @@ -493,7 +510,7 @@ export default function Bins(props: Props) { // } // setYardsLoading(false); // }); - }; + // }; useEffect(() => { loadBins(); @@ -515,7 +532,7 @@ export default function Bins(props: Props) { } const duplicateBin = (bin: Bin) => { - binAPI.addBin(bin.settings, as).then(resp => { + binAPI.addBin(bin.settings, as).then(() => { loadBins(); }); }; @@ -556,7 +573,7 @@ export default function Bins(props: Props) { .then(resp => { openSnack("Successfully added " + resp.data.numAlertsSet + " alerts"); }) - .catch(err => { + .catch(() => { console.log("errors found"); }); }; @@ -1080,7 +1097,7 @@ export default function Bins(props: Props) { control={ { + onChange={(_, checked) => { setDisplayGrain(checked); }} /> @@ -1093,7 +1110,7 @@ export default function Bins(props: Props) { control={ { + onChange={(_, checked) => { setDisplayFert(checked); }} /> @@ -1106,7 +1123,7 @@ export default function Bins(props: Props) { control={ { + onChange={(_, checked) => { setDisplayEmpty(checked); }} /> @@ -1392,15 +1409,17 @@ export default function Bins(props: Props) { {addALertsDialog()} { - searchYards(search, limit, offset); - }} + // loadYards={(search, limit, offset) => { + // searchYards(search, limit, offset); + // }} + loadYards={() => {}} grainBags={grainBags} setShowing={(val: "all" | "bins" | "bags") => { setShowing(val); @@ -1473,6 +1492,13 @@ export default function Bins(props: Props) { /> {binMenu()} setAddBinOpen(true)} pulse={binsTotal < 1 && !allLoading} /> +
); }; diff --git a/src/pages/DeviceSupport.tsx b/src/pages/DeviceSupport.tsx index a6a19de..6c9a0db 100644 --- a/src/pages/DeviceSupport.tsx +++ b/src/pages/DeviceSupport.tsx @@ -3,7 +3,7 @@ import PageContainer from "./PageContainer"; import { pond } from "protobuf-ts/pond"; import LogsDisplay from "common/LogsDisplay"; import { useEffect, useState } from "react"; -import { useDeviceAPI, useSnackbar } from "hooks"; +import { useDeviceAPI, useMobile, useSnackbar } from "hooks"; import { useLocation, useParams } from "react-router-dom"; import { Component, Device, Interaction } from "models"; import { getContextKeys, getContextTypes } from "pbHelpers/Context"; @@ -43,6 +43,7 @@ export default function DeviceSupport() { const deviceAPI = useDeviceAPI() const snackbar = useSnackbar() const theme = useTheme() + const isMobile = useMobile() const { state } = useLocation(); const [{ user, as }] = useGlobalState() @@ -188,7 +189,7 @@ export default function DeviceSupport() { } return ( - + diff --git a/src/pages/Devices.tsx b/src/pages/Devices.tsx index fe47b3c..7721ec1 100644 --- a/src/pages/Devices.tsx +++ b/src/pages/Devices.tsx @@ -5,7 +5,7 @@ import { makeStyles } from "@mui/styles"; import ResponsiveTable, { Column } from "common/ResponsiveTable"; import ProvisionDevice from "device/ProvisionDevice"; import { pond } from "protobuf-ts/pond"; -import { useDeviceAPI, useGlobalState, useGroupAPI } from "providers"; +import { useDeviceAPI, useGlobalState, useGroupAPI, useTeamAPI } from "providers"; import { useCallback, useEffect, useState } from "react"; import PageContainer from "./PageContainer"; import { useMobile } from "hooks"; @@ -28,6 +28,7 @@ import StatusDust from "common/StatusDust"; import StatusGas from "common/StatusGas"; import { cloneDeep } from "lodash"; import LoadingScreen from "app/LoadingScreen"; +import TeamDialog from "teams/TeamDialog"; const useStyles = makeStyles((theme: Theme) => { return ({ @@ -98,9 +99,12 @@ export default function Devices() { const location = useLocation(); const deviceAPI = useDeviceAPI(); const groupAPI = useGroupAPI(); + const teamAPI = useTeamAPI(); const [devicesLoading, setDevicesLoading] = useState(false) const [limit, setLimit] = useState(10); const [page, setPage] = useState(0); + const [doneLoading, setDoneLoading] = useState(false) + const [teamsDialog, setTeamsDialog] = useState(false) const [{ user }] = useGlobalState() @@ -186,7 +190,7 @@ export default function Devices() { const [preferencesAnchor, setPreferencesAnchor] = useState(null) - const [{ as }] = useGlobalState() + const [{ as, team }] = useGlobalState() const updateGroups = (newGroups: Group[]) => { setGroups(newGroups); @@ -327,6 +331,7 @@ export default function Devices() { setTotal(resp.data.total) }).finally(() => { setDevicesLoading(false) + setDoneLoading(true) }) } @@ -338,6 +343,16 @@ export default function Devices() { loadGroups() }, [groupLimit, groupPage, orderGroup, orderGroupBy, searchGroup, as]) + useEffect(() => { + if (doneLoading && devices.length < 1 && groups.length < 1) { + teamAPI.listTeams(1, 0).then(resp => { + if (resp.data.teams.length > 0 && team.key().length < 1) { + setTeamsDialog(true); + } + }) + } + }, [doneLoading, devices, groups]) + const handleChange = (event: any) => { setLimit(event.target.value); }; @@ -826,6 +841,13 @@ export default function Devices() { addGroupCallback={addGroupCallback} /> {preferencesMenu()} + ) } \ No newline at end of file diff --git a/src/teams/TeamDialog.tsx b/src/teams/TeamDialog.tsx index 25ebb9c..edc2124 100644 --- a/src/teams/TeamDialog.tsx +++ b/src/teams/TeamDialog.tsx @@ -1,6 +1,9 @@ import { AppBar, Button, + Checkbox, + FormControlLabel, + FormGroup, IconButton, Theme, Toolbar, @@ -18,6 +21,9 @@ import { makeStyles } from "@mui/styles"; interface Props { open: boolean; setOpen: (value: React.SetStateAction) => void; + title?: string; + info?: string; + hideKey?: string; } const useStyles = makeStyles((theme: Theme) => { @@ -37,16 +43,22 @@ const useStyles = makeStyles((theme: Theme) => { }); export default function TeamDialog(props: Props) { - const { open, setOpen } = props; + const { open, setOpen, title, info, hideKey } = props; const teamAPI = useTeamAPI(); const userAPI = useUserAPI(); const [, dispatch] = useGlobalState(); const [teamKey, setTeamKey] = useState(""); const [{ team, user }] = useGlobalState(); + const [viewAsTeam, setViewAsTeam] = useState(user.settings.useTeam) + const [doNotShow, setDoNotShow] = useState(false) const classes = useStyles(); + const shouldHide = hideKey ? + (localStorage.getItem(hideKey) === "true" ? true : false) + : false + const setTeam = () => { - setOpen(false); + onClose() dispatch({ key: "as", value: teamKey }); if (teamKey === "" || teamKey.includes("auth")) { dispatch({ key: "team", value: Team.create() }); @@ -55,13 +67,19 @@ export default function TeamDialog(props: Props) { teamAPI.getTeam(teamKey).then(team => { dispatch({ key: "team", value: team }); user.settings.defaultTeam = team.key(); - user.settings.useTeam = true; //when selecting a team now set to view as team by default + user.settings.useTeam = viewAsTeam; //when selecting a team now set to view as team by default userAPI.updateUser(user.id(), user.protobuf()); }); }; + const onClose = () => { + console.log("close") + setOpen(false) + if (hideKey) localStorage.setItem(hideKey, ""+doNotShow) + } + return ( - setOpen(false)}> + - Select a Team to View As + { title ? title : "Select a Team to View As"} - @@ -84,12 +102,40 @@ export default function TeamDialog(props: Props) { Viewing as: {team.name()} )} + {info && + + {info} + + } + {/* setViewAsTeam(!viewAsTeam)} /> */} + + setViewAsTeam(!viewAsTeam)} + /> + } + label="View as team" + /> + + {hideKey && + setDoNotShow(!doNotShow)} + /> + } + label="Do not show again" + /> + } ); } From cd96d783ed3d8dcbd0d5176863fda8b775b9c312 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Fri, 18 Jul 2025 11:33:30 -0600 Subject: [PATCH 07/22] qick fix for the duplicate bin functionality on the dasboard, for admin users, so that it stop going to the bin page when you click on the duplicate button --- src/bin/BinsList.tsx | 8 ++++++-- src/pages/Bins.tsx | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/bin/BinsList.tsx b/src/bin/BinsList.tsx index 6779258..af14b88 100644 --- a/src/bin/BinsList.tsx +++ b/src/bin/BinsList.tsx @@ -123,7 +123,9 @@ export default function BinsList(props: Props) { }} key={i} className={classes.gridListTile} - onClick={() => goToBin(i)}> + onClick={() => { + !duplicate && goToBin(i) + }}> goToBin(i)}> + onClick={() => { + !duplicate && goToBin(i) + }}> { binAPI.addBin(bin.settings, as).then(resp => { - loadBins(); + loadMoreBins(contentFilter); }); }; From 6f02f9e562dca7a1ec72ea5a82d533ce500cc105 Mon Sep 17 00:00:00 2001 From: Carter Date: Fri, 18 Jul 2025 13:15:55 -0600 Subject: [PATCH 08/22] setting auth redirect to the url dynamically instead of just local host --- src/providers/auth.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/auth.tsx b/src/providers/auth.tsx index 2a8f6bb..f539ff3 100644 --- a/src/providers/auth.tsx +++ b/src/providers/auth.tsx @@ -38,7 +38,7 @@ export default function AuthWrapper(props: Props) { options.authorizationParams.login_hint = parsed.email.toString(); // prefill email } - options.authorizationParams.redirect_uri = "http://localhost:5173/signupCallback" + options.authorizationParams.redirect_uri = url + "/signupCallback" loginWithRedirect(options) From bc1cf27ad43d58cb38ae3c4aaebff895c55f938d Mon Sep 17 00:00:00 2001 From: Carter Date: Fri, 18 Jul 2025 14:31:48 -0600 Subject: [PATCH 09/22] debug output :) --- src/providers/auth.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/providers/auth.tsx b/src/providers/auth.tsx index f539ff3..9138061 100644 --- a/src/providers/auth.tsx +++ b/src/providers/auth.tsx @@ -39,6 +39,7 @@ export default function AuthWrapper(props: Props) { } options.authorizationParams.redirect_uri = url + "/signupCallback" + console.log(options.authorizationParams.redirect_uri) loginWithRedirect(options) From b9d2635f99bd2b9d488b86afb0be8270361c7455 Mon Sep 17 00:00:00 2001 From: Carter Date: Fri, 18 Jul 2025 15:05:49 -0600 Subject: [PATCH 10/22] using hostname instead of url --- src/providers/auth.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/auth.tsx b/src/providers/auth.tsx index 9138061..4b49e5d 100644 --- a/src/providers/auth.tsx +++ b/src/providers/auth.tsx @@ -38,7 +38,7 @@ export default function AuthWrapper(props: Props) { options.authorizationParams.login_hint = parsed.email.toString(); // prefill email } - options.authorizationParams.redirect_uri = url + "/signupCallback" + options.authorizationParams.redirect_uri = url.hostname + "/signupCallback" console.log(options.authorizationParams.redirect_uri) loginWithRedirect(options) From 84ed63a7659d4de5c67f2e27c3c7e7b35aac067e Mon Sep 17 00:00:00 2001 From: Carter Date: Fri, 18 Jul 2025 15:28:41 -0600 Subject: [PATCH 11/22] added https:// to the hostname to make a valid uri --- src/providers/auth.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/auth.tsx b/src/providers/auth.tsx index 4b49e5d..bc116ff 100644 --- a/src/providers/auth.tsx +++ b/src/providers/auth.tsx @@ -38,7 +38,7 @@ export default function AuthWrapper(props: Props) { options.authorizationParams.login_hint = parsed.email.toString(); // prefill email } - options.authorizationParams.redirect_uri = url.hostname + "/signupCallback" + options.authorizationParams.redirect_uri = "https://" + url.hostname + "/signupCallback" console.log(options.authorizationParams.redirect_uri) loginWithRedirect(options) From ed8ba4b3e4217c085a3b14aaa748c0ea0e0dd590 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Mon, 21 Jul 2025 13:21:19 -0600 Subject: [PATCH 12/22] added dry beans --- package-lock.json | 4 ++-- package.json | 2 +- src/grain/GrainDescriber.ts | 31 +++++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index af61e6b..222fcf3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,7 @@ "mui-tel-input": "^7.0.0", "notistack": "^3.0.1", "openweathermap-ts": "^1.2.10", - "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging", + "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#beans", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", @@ -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#4523ffef42032eb4a923bd50d4cfcccbb1397dbd", + "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#f09196cbc38911bab69fb35c16b1c8f691a4413f", "dependencies": { "protobufjs": "^6.8.8" } diff --git a/package.json b/package.json index fc7a1ac..5a27ce1 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "mui-tel-input": "^7.0.0", "notistack": "^3.0.1", "openweathermap-ts": "^1.2.10", - "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging", + "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#beans", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", diff --git a/src/grain/GrainDescriber.ts b/src/grain/GrainDescriber.ts index 00f9bb0..d112e90 100644 --- a/src/grain/GrainDescriber.ts +++ b/src/grain/GrainDescriber.ts @@ -475,6 +475,37 @@ export const GrainExtensions: Map = new Map([ weightConversionKg: 27.2158214642112, bushelsPerTonne: 36.744 } + ], + [ + pond.Grain.GRAIN_DRY_BEANS_RED, + { + name: "Dry Beans Red", + group: "Dry Beans", + equation: Equation.halsey, + a: 4.2669, + b: -0.013382, + c: 1.6933, + setTempC: defaultSetTemp, + targetMC: 15.0, + colour: "red", + weightConversionKg: 27.2155, + bushelsPerTonne: 36.744 + } + ],[ + pond.Grain.GRAIN_CUSTOM, + { + name: "Dry Beans Black", + group: "Dry Beans", + equation: Equation.halsey, + a: 5.2003, + b: -0.022685, + c: 1.9656, + setTempC: defaultSetTemp, + targetMC: 15.0, + colour: "grey", + weightConversionKg: 27.2155, + bushelsPerTonne: 36.744 + } ] ]); From 0266776e77ac25df51ce2a15ed3fae9e32748d8f Mon Sep 17 00:00:00 2001 From: csawatzky Date: Mon, 21 Jul 2025 13:21:30 -0600 Subject: [PATCH 13/22] missed change --- src/grain/GrainDescriber.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/grain/GrainDescriber.ts b/src/grain/GrainDescriber.ts index d112e90..ce394b3 100644 --- a/src/grain/GrainDescriber.ts +++ b/src/grain/GrainDescriber.ts @@ -492,7 +492,7 @@ export const GrainExtensions: Map = new Map([ bushelsPerTonne: 36.744 } ],[ - pond.Grain.GRAIN_CUSTOM, + pond.Grain.GRAIN_DRY_BEANS_BLACK, { name: "Dry Beans Black", group: "Dry Beans", From 2d5f21c9b5cf53584b936fe78fdcfae4fe25f4fe Mon Sep 17 00:00:00 2001 From: csawatzky Date: Tue, 22 Jul 2025 10:42:55 -0600 Subject: [PATCH 14/22] set the detect I2C feature to not be available yet as we have changed how it works on dev so will want to wait for those changes before it goes live --- src/models/Device.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/models/Device.ts b/src/models/Device.ts index 6311981..e45f7aa 100644 --- a/src/models/Device.ts +++ b/src/models/Device.ts @@ -36,17 +36,17 @@ const featureVersions: Map = new Map([ ],[ "detectI2C", { - photon: "NA", - electron: "NA", - v2Wifi: "2.1.6", - v2Cell: "2.1.6", - v2WifiS3: "2.1.6", - v2CellS3: "2.1.6", - v2CellBlack: "2.1.6", - v2CellGreen: "2.1.6", - v2WifiBlue: "2.1.6", - v2CellBlue: "2.1.6", - v2EthBlue: "2.1.6" + photon: "N/A", + electron: "N/A", + v2Wifi: "N/A", + v2Cell: "N/A", + v2WifiS3: "N/A", + v2CellS3: "N/A", + v2CellBlack: "N/A", + v2CellGreen: "N/A", + v2WifiBlue: "N/A", + v2CellBlue: "N/A", + v2EthBlue: "N/A" } ] ]); From 6996ab0e77b302f3b908613b9e984d65f78a26ec Mon Sep 17 00:00:00 2001 From: Carter Date: Tue, 22 Jul 2025 15:55:16 -0600 Subject: [PATCH 15/22] made the bins page use url context, added bins link to team page --- src/pages/Bins.tsx | 13 ++++++++++++- src/pages/Team.tsx | 20 ++++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/pages/Bins.tsx b/src/pages/Bins.tsx index 476b66f..5bda174 100644 --- a/src/pages/Bins.tsx +++ b/src/pages/Bins.tsx @@ -81,6 +81,7 @@ import GrainBagList from "grainBag/grainBagList"; import BinYards from "bin/BinYards"; import ButtonGroup from "common/ButtonGroup"; import TeamDialog from "teams/TeamDialog"; +import { getContextKeys, getContextTypes } from "pbHelpers/Context"; // import { useHistory } from "react-router"; const useStyles = makeStyles((theme: Theme) => { @@ -311,6 +312,16 @@ export default function Bins(props: Props) { } }, [props.yardFilter]); + const getKeys = () => { + if (as) return undefined + return getContextKeys() + } + + const getTypes = () => { + if (as) return undefined + return getContextTypes() + } + const loadBins = useCallback(() => { //let filter = grainFilter; let filter = yardFilter; @@ -325,7 +336,7 @@ export default function Bins(props: Props) { setDisplayFert(false); binAPI - .listBinsAndData(binsLimit, 0, order, orderBy, filter, as, false) + .listBinsAndData(binsLimit, 0, order, orderBy, filter, as, false, getKeys(), getTypes()) .then(resp => { let grain: Bin[] = []; let empty: Bin[] = []; diff --git a/src/pages/Team.tsx b/src/pages/Team.tsx index dd88fdd..1cf19c9 100644 --- a/src/pages/Team.tsx +++ b/src/pages/Team.tsx @@ -12,10 +12,10 @@ import { appendToUrl } from "navigation/Router"; import PageContainer from "pages/PageContainer"; import { getContextKeys, getContextTypes } from "pbHelpers/Context"; import { pond } from "protobuf-ts/pond"; -import { useSnackbar, useTeamAPI, useUserAPI } from "providers"; +import { useGlobalState, useSnackbar, useTeamAPI, useUserAPI } from "providers"; import { useEffect, useState } from "react"; import { useLocation, useNavigate, useParams } from 'react-router-dom'; -import { getSignatureAccentColour } from "services/whiteLabel"; +import { getSignatureAccentColour, IsAdaptiveAgriculture, IsStreamline } from "services/whiteLabel"; import TeamActions from "teams/TeamActions"; import TeamKeyManager from "teams/TeamKeyManager"; import ObjectUsers from "user/ObjectUsers"; @@ -88,6 +88,7 @@ export default function TeamPage() { const [loadingUsers, setLoadingUsers] = useState(false) const [userDialog, setUserDialog] = useState(false) const teamKey = teamID ? teamID : ""; + const [{ user }] = useGlobalState() const getTeam = () => { @@ -212,6 +213,13 @@ export default function TeamPage() { const toGroups = () => { navigate(appendToUrl("groups")) } + + const toBins = () => { + navigate(appendToUrl("bins")) + } + + const isAg = IsAdaptiveAgriculture() + const isStreamline = IsStreamline() return ( @@ -255,6 +263,14 @@ export default function TeamPage() { Groups + {((isAg || isStreamline) || user.hasFeature("admin")) && + <> + + + Bins + + + } From e5d4aee4a1f6181f756c929bcbee46abccb6ec55 Mon Sep 17 00:00:00 2001 From: Carter Date: Wed, 23 Jul 2025 09:54:42 -0600 Subject: [PATCH 16/22] relative pathing for bins page --- src/bin/BinsList.tsx | 2 +- src/pages/Bins.tsx | 5 +++-- src/pages/Team.tsx | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/bin/BinsList.tsx b/src/bin/BinsList.tsx index 6779258..540877a 100644 --- a/src/bin/BinsList.tsx +++ b/src/bin/BinsList.tsx @@ -49,7 +49,7 @@ export default function BinsList(props: Props) { //const scrollRef = useRef(null); const goToBin = (i: number) => { - let path = "/bins/" + bins[i].key(); + let path = bins[i].key(); navigate(path, { state: {bin: bins[i]} }); }; diff --git a/src/pages/Bins.tsx b/src/pages/Bins.tsx index 5bda174..71675f8 100644 --- a/src/pages/Bins.tsx +++ b/src/pages/Bins.tsx @@ -634,8 +634,9 @@ export default function Bins(props: Props) { preLoadedData={paginatedBins.bins} rowClickFunction={data => { let bin = data as Bin; - let path = "/bins/" + bin.key(); - navigate(path, { state: { bin: bin }}); + // let path = "bins/" + bin.key(); + // navigate(path, { state: { bin: bin }}); + navigate(bin.key(), { state: { bin: bin }, relative: "path"}); }} customButtons={[ { diff --git a/src/pages/Team.tsx b/src/pages/Team.tsx index 1cf19c9..329c95d 100644 --- a/src/pages/Team.tsx +++ b/src/pages/Team.tsx @@ -215,7 +215,8 @@ export default function TeamPage() { } const toBins = () => { - navigate(appendToUrl("bins")) + // navigate(appendToUrl("bins")) + navigate("bins") } const isAg = IsAdaptiveAgriculture() From c677e8ec40133123c3677dc88646279c67272c9f Mon Sep 17 00:00:00 2001 From: Carter Date: Wed, 23 Jul 2025 11:34:32 -0600 Subject: [PATCH 17/22] disabled imitation on team page to prevent share error while viewing as team --- src/pages/Team.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/Team.tsx b/src/pages/Team.tsx index 329c95d..5d2d7ad 100644 --- a/src/pages/Team.tsx +++ b/src/pages/Team.tsx @@ -246,6 +246,7 @@ export default function TeamPage() { isDialogOpen={userDialog} closeDialogCallback={() => setUserDialog(false)} refreshCallback={() => {}} + useImitation={false} /> From e806633a8dfa9f0f05af640ad1feb689fe3d5eda Mon Sep 17 00:00:00 2001 From: Carter Date: Wed, 23 Jul 2025 12:10:01 -0600 Subject: [PATCH 18/22] using url.origin in the redirect uri to be more consistent --- src/providers/auth.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/providers/auth.tsx b/src/providers/auth.tsx index bc116ff..0a7a619 100644 --- a/src/providers/auth.tsx +++ b/src/providers/auth.tsx @@ -38,7 +38,8 @@ export default function AuthWrapper(props: Props) { options.authorizationParams.login_hint = parsed.email.toString(); // prefill email } - options.authorizationParams.redirect_uri = "https://" + url.hostname + "/signupCallback" + console.log(url) + options.authorizationParams.redirect_uri = url.origin + "/signupCallback" console.log(options.authorizationParams.redirect_uri) loginWithRedirect(options) From 8d97d27097c724df5f5f281ab5df65d512303c1f Mon Sep 17 00:00:00 2001 From: Carter Date: Wed, 23 Jul 2025 14:11:07 -0600 Subject: [PATCH 19/22] merged staging and updated master proto --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 222fcf3..97cf2f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,7 @@ "mui-tel-input": "^7.0.0", "notistack": "^3.0.1", "openweathermap-ts": "^1.2.10", - "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#beans", + "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#master", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", @@ -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#f09196cbc38911bab69fb35c16b1c8f691a4413f", + "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#ea5c01a80b896e8a22e7930780b1b3479e449c90", "dependencies": { "protobufjs": "^6.8.8" } diff --git a/package.json b/package.json index 5a27ce1..ee0e9b7 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "mui-tel-input": "^7.0.0", "notistack": "^3.0.1", "openweathermap-ts": "^1.2.10", - "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#beans", + "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#master", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", From 38ae04f40caa512150041bc0c80a0669aee5d0b0 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Thu, 24 Jul 2025 15:56:23 -0600 Subject: [PATCH 20/22] added the airlfow subtypes --- src/pbHelpers/ComponentTypes/Airflow.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/pbHelpers/ComponentTypes/Airflow.ts b/src/pbHelpers/ComponentTypes/Airflow.ts index fde83e2..2e46725 100644 --- a/src/pbHelpers/ComponentTypes/Airflow.ts +++ b/src/pbHelpers/ComponentTypes/Airflow.ts @@ -25,7 +25,18 @@ export function Airflow(subtype: number = 0): ComponentTypeExtension { ); return { type: quack.ComponentType.COMPONENT_TYPE_AIRFLOW, - subtypes: [], + subtypes: [ + { + key: quack.AirFlowSubtype.AIR_FLOW_SUBTYPE_PROSENSE, + value: "AIR_FLOW_SUBTYPE_PROSENSE", + friendlyName: "Prosense" + }, + { + key: quack.AirFlowSubtype.AIR_FLOW_SUBTYPE_ULTRASONIC, + value: "AIR_FLOW_SUBTYPE_ULTRASONIC", + friendlyName: "Ultrasonic" + }, + ], friendlyName: "Airflow", description: "Measure the flow of air though a specified area", isController: false, From 8a53f9689af227fd03557b70bb25ae66417f98c9 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Fri, 25 Jul 2025 09:45:32 -0600 Subject: [PATCH 21/22] changed the speed to be m/s in metric rather than km/h and ft/m in imperial and added the measurement type for speed/velocity to the airflow component --- package.json | 2 +- src/models/UnitMeasurement.ts | 10 +++++++ src/pbHelpers/ComponentType.tsx | 5 ++-- src/pbHelpers/ComponentTypes/Airflow.ts | 38 +++++++++++++++++++------ src/pbHelpers/MeasurementDescriber.ts | 3 +- 5 files changed, 46 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index ee0e9b7..59e639d 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "mui-tel-input": "^7.0.0", "notistack": "^3.0.1", "openweathermap-ts": "^1.2.10", - "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#master", + "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#airflow", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", diff --git a/src/models/UnitMeasurement.ts b/src/models/UnitMeasurement.ts index 966b545..344abe0 100644 --- a/src/models/UnitMeasurement.ts +++ b/src/models/UnitMeasurement.ts @@ -256,5 +256,15 @@ function unitConversion( }); } } + if (type === quack.MeasurementType.MEASUREMENT_TYPE_SPEED) { + if (user.settings.distanceUnit === pond.DistanceUnit.DISTANCE_UNIT_FEET) { + newVals.forEach((val, i) => { + val.values.forEach((v, j) => { + //convert m/s to ft/m by multiplying by 196.9 + newVals[i].values[j] = v * 196.9 + }); + }); + } + } return newVals; } diff --git a/src/pbHelpers/ComponentType.tsx b/src/pbHelpers/ComponentType.tsx index 2d66c9d..e03a1ce 100644 --- a/src/pbHelpers/ComponentType.tsx +++ b/src/pbHelpers/ComponentType.tsx @@ -146,7 +146,7 @@ export interface ComponentTypeExtension { interactionResultTypes: Array; states: Array; measurements: Array; - measurementSummary: Function; //Deprecated: this summary used the old measurement structure + measurementSummary?: Function; //Deprecated: this summary used the old measurement structure unitMeasurementSummary: ( measurements: convertedUnitMeasurement, excludedNodes?: number[] @@ -382,7 +382,8 @@ export async function getMeasurementSummary( measurement: quack.IMeasurement, filters: GraphFilters ): Promise> { - return extension(type, subtype).measurementSummary(measurement, filters); + let sumFunc = extension(type, subtype).measurementSummary + return sumFunc ? sumFunc(measurement, filters) : []; } const validNodes = (nodeVals: number[], filters?: GraphFilters) => { diff --git a/src/pbHelpers/ComponentTypes/Airflow.ts b/src/pbHelpers/ComponentTypes/Airflow.ts index 2e46725..acc52fe 100644 --- a/src/pbHelpers/ComponentTypes/Airflow.ts +++ b/src/pbHelpers/ComponentTypes/Airflow.ts @@ -1,14 +1,15 @@ import { ComponentTypeExtension, Summary, - simpleMeasurements, - simpleSummaries, + // simpleMeasurements, + // simpleSummaries, unitMeasurementSummaries, AreaChartData, GraphFilters, simpleAreaChartData, LineChartData, - simpleLineChartData + simpleLineChartData, + ComponentMeasurement } from "pbHelpers/ComponentType"; import PressureDarkIcon from "assets/components/pressureDark.png"; import PressureLightIcon from "assets/components/pressureLight.png"; @@ -18,11 +19,32 @@ import { convertedUnitMeasurement } from "models/UnitMeasurement"; import { pond } from "protobuf-ts/pond"; export function Airflow(subtype: number = 0): ComponentTypeExtension { - let airflow = describeMeasurement( + let cfm = describeMeasurement( quack.MeasurementType.MEASUREMENT_TYPE_CFM, quack.ComponentType.COMPONENT_TYPE_AIRFLOW, subtype ); + + let velocity = describeMeasurement( + quack.MeasurementType.MEASUREMENT_TYPE_CFM, + quack.ComponentType.COMPONENT_TYPE_AIRFLOW, + subtype + ); + + let measurementTypes = [ + { + measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PPM, + label: cfm.label(), + colour: cfm.colour(), + graphType: cfm.graph() + } as ComponentMeasurement, + { + measurementType: quack.MeasurementType.MEASUREMENT_TYPE_VOLTAGE, + label: velocity.label(), + colour: velocity.colour(), + graphType: velocity.graph() + } as ComponentMeasurement + ]; return { type: quack.ComponentType.COMPONENT_TYPE_AIRFLOW, subtypes: [ @@ -47,10 +69,10 @@ export function Airflow(subtype: number = 0): ComponentTypeExtension { addressTypes: [quack.AddressType.ADDRESS_TYPE_I2C], interactionResultTypes: [], states: [], - measurements: simpleMeasurements(airflow), - measurementSummary: async function(measurement: quack.Measurement): Promise> { - return simpleSummaries(measurement, airflow); - }, + measurements: measurementTypes, + // measurementSummary: async function(measurement: quack.Measurement): Promise> { + // return simpleSummaries(measurement, airflow); + // }, unitMeasurementSummary: ( measurements: convertedUnitMeasurement, excludedNodes?: number[] diff --git a/src/pbHelpers/MeasurementDescriber.ts b/src/pbHelpers/MeasurementDescriber.ts index a364b77..1fc2751 100644 --- a/src/pbHelpers/MeasurementDescriber.ts +++ b/src/pbHelpers/MeasurementDescriber.ts @@ -450,8 +450,9 @@ export class MeasurementDescriber { this.details.max = 100000; break; case quack.MeasurementType.MEASUREMENT_TYPE_SPEED: + let speedUnit = getDistanceUnit() this.details.label = "Speed"; - this.details.unit = "km/h"; + this.details.unit = speedUnit === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "m/s" : "ft/m"; this.details.colour = green["500"]; this.details.path = "edgeTriggered.rises"; this.details.max = 500; From 7d24cb7df41dc2f2afc28d54105496ce4f3b832b Mon Sep 17 00:00:00 2001 From: csawatzky Date: Mon, 28 Jul 2025 12:04:20 -0600 Subject: [PATCH 22/22] updating the address for airflow --- src/pbHelpers/AddressTypes/I2C.ts | 2 +- src/pbHelpers/DeviceAvailability.ts | 2 +- src/products/MiVent/MiVentAvailability.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pbHelpers/AddressTypes/I2C.ts b/src/pbHelpers/AddressTypes/I2C.ts index c17c743..3c15ced 100644 --- a/src/pbHelpers/AddressTypes/I2C.ts +++ b/src/pbHelpers/AddressTypes/I2C.ts @@ -16,7 +16,7 @@ export const I2C: AddressTypeExtension = { [quack.ComponentType.COMPONENT_TYPE_CAPACITANCE, 0x4f], [quack.ComponentType.COMPONENT_TYPE_CAPACITOR_CABLE, 0x4f], [quack.ComponentType.COMPONENT_TYPE_SEN5X, 0x68], - [quack.ComponentType.COMPONENT_TYPE_AIRFLOW, 0x29] + [quack.ComponentType.COMPONENT_TYPE_AIRFLOW, 0x6b] ]); const offset = offsets.get(componentType); diff --git a/src/pbHelpers/DeviceAvailability.ts b/src/pbHelpers/DeviceAvailability.ts index c50a2a4..3d09a7f 100644 --- a/src/pbHelpers/DeviceAvailability.ts +++ b/src/pbHelpers/DeviceAvailability.ts @@ -60,7 +60,7 @@ const DefaultAvailability: DeviceAvailabilityMap = new Map