From c3fbe3acf6c2495141a42caeca998b5e94728332 Mon Sep 17 00:00:00 2001 From: Carter Date: Mon, 15 Dec 2025 13:23:57 -0600 Subject: [PATCH 01/43] added tidio chat to the app with a script tag in the index.html --- index.html | 33 +++++++++++++++++++++++++++++++++ indexV2.html | 33 +++++++++++++++++++++++++++++++++ package-lock.json | 12 ++++++------ src/app/App.css | 12 ++++++++++++ 4 files changed, 84 insertions(+), 6 deletions(-) diff --git a/index.html b/index.html index 31b4ca1..8209efe 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,40 @@ Adaptive Dashboard + + +
diff --git a/indexV2.html b/indexV2.html index 31b4ca1..8209efe 100644 --- a/indexV2.html +++ b/indexV2.html @@ -6,7 +6,40 @@ Adaptive Dashboard + + +
diff --git a/package-lock.json b/package-lock.json index abf6c85..753db8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9727,9 +9727,9 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -13757,9 +13757,9 @@ } }, "node_modules/vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/app/App.css b/src/app/App.css index 89db113..93468b3 100644 --- a/src/app/App.css +++ b/src/app/App.css @@ -44,4 +44,16 @@ html, body, #root { .MuiPopover-root { z-index: 1501 !important; +} + +#tidio-chat-iframe { + bottom: 100px !important; /* Adjust this value – higher number = moved further up from the bottom */ + right: 20px !important; /* Optional: tweak horizontal position if needed */ +} + +/* Optional: Different position on mobile (to avoid overlaps there) */ +@media only screen and (max-width: 980px) { + #tidio-chat-iframe { + bottom: 80px !important; /* Example for mobile – test and adjust */ + } } \ No newline at end of file From 4e6af08eee9b34761f67e45b456da61a7cbc9df5 Mon Sep 17 00:00:00 2001 From: Carter Date: Wed, 17 Dec 2025 14:18:24 -0600 Subject: [PATCH 02/43] added Crisp chat instead of tideo --- index.html | 34 +--------------------------------- indexV2.html | 34 +--------------------------------- 2 files changed, 2 insertions(+), 66 deletions(-) diff --git a/index.html b/index.html index 8209efe..0090fcc 100644 --- a/index.html +++ b/index.html @@ -6,40 +6,8 @@ Adaptive Dashboard - - - +
diff --git a/indexV2.html b/indexV2.html index 8209efe..0090fcc 100644 --- a/indexV2.html +++ b/indexV2.html @@ -6,40 +6,8 @@ Adaptive Dashboard - - - +
From 6737db1149d5af3aba71dc6c4779e85282301907 Mon Sep 17 00:00:00 2001 From: Carter Date: Mon, 22 Dec 2025 11:54:03 -0600 Subject: [PATCH 03/43] giving the user's email to the crisp session automatically --- package-lock.json | 7 +++++++ package.json | 9 +++++---- src/app/UserWrapper.tsx | 16 ++++++++++++++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 753db8c..5d86a13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "@turf/turf": "^7.2.0", "@types/classnames": "^2.3.0", "axios": "^1.7.7", + "crisp-sdk-web": "^1.0.26", "dayjs": "^1.11.13", "deep-object-diff": "^1.1.9", "framer-motion": "^12.6.2", @@ -7365,6 +7366,12 @@ "node": ">= 6" } }, + "node_modules/crisp-sdk-web": { + "version": "1.0.26", + "resolved": "https://registry.npmjs.org/crisp-sdk-web/-/crisp-sdk-web-1.0.26.tgz", + "integrity": "sha512-MBK/+tCNJmt/l9ZYHb0cUUaaF02rCKeqLw1cruY/HU48tPOOnhUM0JdvuPaeq1d5VB2DCILY4rGYE4NeJbXvzg==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", diff --git a/package.json b/package.json index ee0e9b7..7f5044c 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,10 @@ "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:streamline": "VITE_AUTH0_CLIENT_ID=HwUV0hHNdVvU96zuMBTAU8i7nFdwwgIX VITE_AUTH0_CLIENT_DOMAIN=brandxtech.auth0.com VITE_AUTH0_AUDIENCE=streamline.api.adaptiveagriculture.ca VITE_APP_API_URL=https://streamline.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_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_CRISP_WEBSITE_ID={CRISP_WEBSITE_ID} 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_CRISP_WEBSITE_ID={CRISP_WEBSITE_ID} 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_CRISP_WEBSITE_ID={CRISP_WEBSITE_ID} 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:streamline": "VITE_CRISP_WEBSITE_ID={CRISP_WEBSITE_ID} VITE_AUTH0_CLIENT_ID=HwUV0hHNdVvU96zuMBTAU8i7nFdwwgIX VITE_AUTH0_CLIENT_DOMAIN=brandxtech.auth0.com VITE_AUTH0_AUDIENCE=streamline.api.adaptiveagriculture.ca VITE_APP_API_URL=https://streamline.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_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" }, @@ -42,6 +42,7 @@ "@turf/turf": "^7.2.0", "@types/classnames": "^2.3.0", "axios": "^1.7.7", + "crisp-sdk-web": "^1.0.26", "dayjs": "^1.11.13", "deep-object-diff": "^1.1.9", "framer-motion": "^12.6.2", diff --git a/src/app/UserWrapper.tsx b/src/app/UserWrapper.tsx index cb4cdf8..74d55df 100644 --- a/src/app/UserWrapper.tsx +++ b/src/app/UserWrapper.tsx @@ -12,6 +12,7 @@ import { makeStyles } from '@mui/styles' import { CssBaseline, Theme } from '@mui/material' import { AppThemeProvider } from 'theme/AppThemeProvider' import HTTPProvider from 'providers/http' +import { Crisp } from "crisp-sdk-web"; // import FirmwareLoader from './FirmwareLoader' const reducer = (state: GlobalState, action: GlobalStateAction): GlobalState => { @@ -67,6 +68,9 @@ export default function UserWrapper(props: Props) { const user_id = or(useAuth.user?.sub, "") + window.CRISP_RUNTIME_CONFIG = { session_merge: true }; + Crisp.configure(import.meta.env.VITE_CRISP_WEBSITE_ID); + const loadUser = useCallback(() => { if (!userAPI.getUserWithTeam) return; if (hasFetched.current) return; @@ -100,6 +104,18 @@ export default function UserWrapper(props: Props) { .finally(() => { setLoading(false) hasFetched.current = true; + if (global?.user && global?.user.settings.email) { // Assuming you have user object from your auth/settings + Crisp.user.setEmail(global?.user.settings.email); + // Optional: Set nickname or other info + Crisp.user.setNickname(global?.user.settings.name || "User"); + Crisp.user.setPhone(global?.user.settings.phoneNumber); + + // For extra security (marks email as verified, prevents spoofing): + // Crisp.user.setEmail(user.email, "your-hmac-signature-here"); // See Crisp docs for signing + + // Optional: For session continuity across devices/browsers + Crisp.setTokenId(global?.user.id()); // e.g., your DB user ID + } }) }, [setGlobal]) From 3952bb747884511c3726d14c679d90dd0ed52e9a Mon Sep 17 00:00:00 2001 From: Carter Date: Mon, 22 Dec 2025 11:55:17 -0600 Subject: [PATCH 04/43] setting runtime config for crisp before i load it --- index.html | 5 +++++ indexV2.html | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/index.html b/index.html index 0090fcc..45dc87b 100644 --- a/index.html +++ b/index.html @@ -7,6 +7,11 @@ Adaptive Dashboard +
diff --git a/indexV2.html b/indexV2.html index 0090fcc..45dc87b 100644 --- a/indexV2.html +++ b/indexV2.html @@ -7,6 +7,11 @@ Adaptive Dashboard +
From 193bf0b7a6a4ddc52eb9e52d643f5802f08d4b96 Mon Sep 17 00:00:00 2001 From: Carter Date: Mon, 22 Dec 2025 15:41:47 -0600 Subject: [PATCH 05/43] updating crips settings in a useEffect when we know it's ready --- src/app/UserWrapper.tsx | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/app/UserWrapper.tsx b/src/app/UserWrapper.tsx index 74d55df..8905875 100644 --- a/src/app/UserWrapper.tsx +++ b/src/app/UserWrapper.tsx @@ -68,7 +68,7 @@ export default function UserWrapper(props: Props) { const user_id = or(useAuth.user?.sub, "") - window.CRISP_RUNTIME_CONFIG = { session_merge: true }; + const crispInitialized = useRef(false); Crisp.configure(import.meta.env.VITE_CRISP_WEBSITE_ID); const loadUser = useCallback(() => { @@ -104,20 +104,26 @@ export default function UserWrapper(props: Props) { .finally(() => { setLoading(false) hasFetched.current = true; - if (global?.user && global?.user.settings.email) { // Assuming you have user object from your auth/settings - Crisp.user.setEmail(global?.user.settings.email); - // Optional: Set nickname or other info - Crisp.user.setNickname(global?.user.settings.name || "User"); - Crisp.user.setPhone(global?.user.settings.phoneNumber); - - // For extra security (marks email as verified, prevents spoofing): - // Crisp.user.setEmail(user.email, "your-hmac-signature-here"); // See Crisp docs for signing - - // Optional: For session continuity across devices/browsers - Crisp.setTokenId(global?.user.id()); // e.g., your DB user ID - } }) }, [setGlobal]) + + useEffect(() => { + if (!crispInitialized.current && global?.user) { + Crisp.configure(import.meta.env.VITE_CRISP_WEBSITE_ID); + crispInitialized.current = true; + Crisp.session.reset(); + Crisp.user.setEmail(global?.user.settings.email); + // Optional: Set nickname or other info + Crisp.user.setNickname(global?.user.settings.name || global?.user.settings.email); + Crisp.user.setPhone(global?.user.settings.phoneNumber); + + // For extra security (marks email as verified, prevents spoofing): + // Crisp.user.setEmail(user.email, "your-hmac-signature-here"); // See Crisp docs for signing + + // Optional: For session continuity across devices/browsers + Crisp.setTokenId(global?.user.id()); // e.g., your DB user ID + } + }, [global]); useEffect(() => { if (loading) return; From d7562bc1bdb14fe5ab08768e3b179eb621a8e0e5 Mon Sep 17 00:00:00 2001 From: Carter Date: Fri, 9 Jan 2026 14:13:11 -0600 Subject: [PATCH 06/43] moved the add bin bottom out ffrom under the chatbox button --- src/bin/AddBinFab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bin/AddBinFab.tsx b/src/bin/AddBinFab.tsx index 61a3a3e..a90b5d6 100644 --- a/src/bin/AddBinFab.tsx +++ b/src/bin/AddBinFab.tsx @@ -27,7 +27,7 @@ const useStyles = makeStyles((theme: Theme) => { }, position: "fixed", bottom: theme.spacing(8), //for mobile navigator - right: theme.spacing(2), + right: theme.spacing(11), [theme.breakpoints.up("sm")]: { bottom: theme.spacing(2) } From 472519dc996deab6f448acb09ebee5f650b3cc4f Mon Sep 17 00:00:00 2001 From: Carter Date: Mon, 19 Jan 2026 14:53:03 -0600 Subject: [PATCH 07/43] added changes that prevent nginx welcome error --- nginx.conf | 10 +++++----- vite.config.ts | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/nginx.conf b/nginx.conf index 1537b32..4411199 100644 --- a/nginx.conf +++ b/nginx.conf @@ -78,18 +78,18 @@ http { } # Redirect old /index.html requests to new file - location = /index.html { - return 301 /indexV2.html; - } + # location = /index.html { + # return 301 /indexV2.html; + # } # Serve indexV2.html directly without redirect loop - location = /indexV2.html { + location = /index.html { root /usr/share/nginx/html; } # Fallback for SPA routing — all unmatched routes go to indexV2.html location / { - try_files $uri /indexV2.html; + try_files $uri /index.html; } } diff --git a/vite.config.ts b/vite.config.ts index acc8cb4..f48db4a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -17,7 +17,7 @@ export default defineConfig({ globPatterns: ['**/*.{js,css,html,png,jpg,svg}'], // Cache all common asset types maximumFileSizeToCacheInBytes: 20 * 1024 * 1024, // 20 MiB cleanupOutdatedCaches: true, - navigateFallback: "/indexV2.html", + navigateFallback: "/index.html", runtimeCaching: [ { urlPattern: ({ request }) => request.destination === 'document', @@ -53,7 +53,7 @@ export default defineConfig({ target: 'esnext', rollupOptions: { input: { - main: path.resolve(__dirname, 'indexV2.html') + main: path.resolve(__dirname, 'index.html') } } }, From f02aa3a6e05b40b95f8d59158424046bb7f775de Mon Sep 17 00:00:00 2001 From: csawatzky Date: Wed, 28 Jan 2026 13:04:34 -0600 Subject: [PATCH 08/43] added more of the status for the gate to the gates dashboard, also changed the sensor graphs to have them in a specific order --- package-lock.json | 4 +- package.json | 2 +- src/gate/GateDevice.tsx | 20 ++++++ src/gate/GateGraphs.tsx | 91 +++++++++++++++++--------- src/gate/GateList.tsx | 139 +++++++++++++++++++++++++++------------- src/pages/Gate.tsx | 14 ++-- 6 files changed, 183 insertions(+), 87 deletions(-) diff --git a/package-lock.json b/package-lock.json index c150062..3d7a074 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#gate_dashboard", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", @@ -10953,7 +10953,7 @@ }, "node_modules/protobuf-ts": { "version": "1.0.0", - "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#c9ef7906fd97cda8ef4bd149ec4a796159a7c067", + "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#7b1d4cc9b83dbd50ce5fd2bc751f4f8f68471861", "dependencies": { "protobufjs": "^6.8.8" } diff --git a/package.json b/package.json index ee0e9b7..03913bd 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#gate_dashboard", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", diff --git a/src/gate/GateDevice.tsx b/src/gate/GateDevice.tsx index f7a8746..63ec92c 100644 --- a/src/gate/GateDevice.tsx +++ b/src/gate/GateDevice.tsx @@ -44,6 +44,8 @@ export default function GateDevice(props: Props) { const [tempKey, setTempKey] = useState(""); const [pressureKey, setPressureKey] = useState(""); const [ambientKey, setAmbientKey] = useState(""); + const [greenLightKey, setGreenLightKey] = useState(""); + const [redLightKey, setRedLightKey] = useState(""); const { openSnack } = useSnackbar(); const [{ user }] = useGlobalState(); const [interactionDialog, setInteractionDialog] = useState(false); @@ -57,8 +59,18 @@ export default function GateDevice(props: Props) { const [lastPressures, setLastPressures] = useState({ p1: 0, p2: 0 }); useEffect(() => { + //addresses of controller LEDs on a V1 device + let redAddr = "3-1-512"; + let greenAddr = "3-1-1024"; if (comprehensiveDevice.device) { setDevice(Device.any(comprehensiveDevice.device)); + + //check if the device is a MiPCA V2 device since the lEDs will have different addresses + let dev = Device.create(comprehensiveDevice.device); + if (dev.settings.product === pond.DeviceProduct.DEVICE_PRODUCT_MIPCA_V2) { + redAddr = "3-1-256"; + greenAddr = "3-1-512"; + } } if (comprehensiveDevice.components) { let components: Map = new Map(); @@ -100,6 +112,11 @@ export default function GateDevice(props: Props) { break; } } + if (c.locationString() === redAddr && c.subType() === 1) { + setRedLightKey(c.key()); + } else if (c.locationString() === greenAddr && c.subType() === 1) { + setGreenLightKey(c.key()); + } }); setComponentOptions(components); } @@ -362,6 +379,9 @@ export default function GateDevice(props: Props) { }} ambient={ambientKey} pressure={pressureKey} + tempChain={tempKey} + redKey={redLightKey} + greenKey={greenLightKey} device={device.id()} /> diff --git a/src/gate/GateGraphs.tsx b/src/gate/GateGraphs.tsx index 40c47d8..286abee 100644 --- a/src/gate/GateGraphs.tsx +++ b/src/gate/GateGraphs.tsx @@ -36,6 +36,9 @@ interface Props { device: string | number; pressure: string; ambient: string; + tempChain: string; + redKey: string; + greenKey: string; setPCAState: (state: boolean) => void; } @@ -60,7 +63,7 @@ const useStyles = makeStyles((theme: Theme) => ({ ); export default function GateGraphs(props: Props) { - const { gate, display, compMap, device, pressure, ambient, setPCAState } = props; + const { gate, display, compMap, device, pressure, ambient, tempChain, greenKey, redKey, setPCAState } = props; const [{ as }] = useGlobalState(); const [xDomain, setXDomain] = useState(["dataMin", "dataMax"]); const [zoomed, setZoomed] = useState(false); @@ -270,42 +273,68 @@ export default function GateGraphs(props: Props) { ); }; - const sensorGraphs = () => { + const graphCard = (component: Component, unitMeasurements: UnitMeasurement[]) => { return ( - - {Array.from(compMeasurements.values()).map((compMeasurement, i) => { - let c = Component.create(); - if (compMeasurement[0]) { - c = compMap.get(compMeasurement[0].componentId) ?? Component.create(); - } - if (compMap.get(c.key())) { - return ( - - {graphHeader(c)} - {compMeasurement.map(um => { - if (um.values[0] && um.values[0].values.length > 1) { - return areaGraph( - um, - describeMeasurement(um.type, c.type(), c.subType()), - c.settings.smoothingAverages - ); - } else { - return lineGraph( - um, - describeMeasurement(um.type, c.type(), c.subType()), - c.settings.smoothingAverages - ); - } - })} - + + {graphHeader(component)} + {unitMeasurements.map(um => { + if (um.values[0] && um.values[0].values.length > 1) { + return areaGraph( + um, + describeMeasurement(um.type, component.type(), component.subType()), + component.settings.smoothingAverages ); } else { - return ; + return lineGraph( + um, + describeMeasurement(um.type, component.type(), component.subType()), + component.settings.smoothingAverages + ); } })} - + ); - }; + } + + /** + * This function creates the charts for the gate page using the MAIN components on the gate, it DOES NOT show any other components that could be on the device + * @returns list of cards for the charts + */ + const sensorGraphs = () => { + let graphs: JSX.Element[] = [] + + //pressure + let pressureComp = compMap.get(pressure) + let pressureReadings = compMeasurements.get(pressure) + if(pressureComp && pressureReadings){ + graphs.push(graphCard(pressureComp, pressureReadings)) + } + //T/H chain + let tempComp = compMap.get(tempChain) + let tempReadings = compMeasurements.get(tempChain) + if(tempComp && tempReadings){ + graphs.push(graphCard(tempComp, tempReadings)) + } + //ambient + let ambientComp = compMap.get(ambient) + let ambientReadings = compMeasurements.get(ambient) + if (ambientComp && ambientReadings){ + graphs.push(graphCard(ambientComp, ambientReadings)) + } + //green + let greenComp = compMap.get(greenKey) + let greenReadings = compMeasurements.get(greenKey) + if (greenComp && greenReadings){ + graphs.push(graphCard(greenComp, greenReadings)) + } + //red + let redComp = compMap.get(redKey) + let redReadings = compMeasurements.get(redKey) + if (redComp && redReadings){ + graphs.push(graphCard(redComp, redReadings)) + } + return graphs + } const analyticGraphs = () => { return ( diff --git a/src/gate/GateList.tsx b/src/gate/GateList.tsx index 3e3b330..cd316c4 100644 --- a/src/gate/GateList.tsx +++ b/src/gate/GateList.tsx @@ -15,6 +15,10 @@ import { CheckCircleOutline, DoNotDisturb, ErrorOutline, HelpOutlineOutlined, Se import { cloneDeep } from "lodash"; import moment from "moment"; import { pond } from "protobuf-ts/pond"; +import { UnitMeasurement } from "models/UnitMeasurement"; +import { quack } from "protobuf-ts/quack"; +import { getTemperatureUnit } from "utils"; +import { teal } from "@mui/material/colors"; interface Props { //gates: Gate[]; @@ -47,6 +51,7 @@ export default function GateList(props: Props) { const navigate = useNavigate(); const isMobile = useMobile(); const classes = useStyles(); + const [{user}] = useGlobalState() const [gateDialog, setGateDialog] = useState(false); const [tablePage, setTablePage] = useState(0) const [pageSize, setPageSize] = useState(10) @@ -104,6 +109,38 @@ export default function GateList(props: Props) { } } + const lastOutletReading = (reading: pond.UnitMeasurementsForComponent[]) => { + let outletDisplay = "--" + let timeSince = "No Reading Yet" + let color = "" + reading.forEach(um => { + let clone = cloneDeep(um) + let measurement = UnitMeasurement.create(clone, user) + if(measurement.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE || measurement.type === quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE){ + let nodes = measurement.values[measurement.values.length-1].values + outletDisplay = nodes[nodes.length-1].toFixed(2) + " " + measurement.unit + timeSince = moment(measurement.timestamps[measurement.timestamps.length-1]).fromNow() + color = measurement.colour + } + }) + return ( + + + Value: + {outletDisplay} + + + Time: + {timeSince} + + + ) + } + + const CtoF = (celsius: number) => { + return Math.round((celsius * (9 / 5) + 32) * 100) / 100; + }; + const desktopCols = (): Column[] => { return [ { @@ -126,68 +163,78 @@ export default function GateList(props: Props) { ) }, - { - title: "Duct Type", - render: gate => ( - - - {gate.settings.ductName} - - - ) - }, - { - title: "Duct Size(mm)", - render: gate => ( - - - {gate.ductDiameter()} - - - ) - }, - { - title: "Duct Length(m)", - render: gate => ( - - - {gate.settings.ductLength} - - - ) - }, - { - title: "PCA Unit", - render: gate => ( - - - {gate.settings.pcaType} - - - ) - }, { title: "PCA Status", - render: gate => ( - + render: gate => { + return ( {displayPCAStatus(gate.status.pcaState)} + ) + } + }, + { + title: "Conditioning", + render: gate => { + let display = "" + if(gate.status.pcaState === pond.PCAState.PCA_STATE_OFF){ + display = "Inactive" + } else if (gate.status.pcaState === pond.PCAState.PCA_STATE_UNKNOWN){ + display = "--" + } else { + display = gate.status.heating ? "Heating" : "Cooling" + } + return ( + + + {display} + - ) + )} + }, + { + title: "Estimated Temp", + render: gate => { + let display = "--" + if(gate.status.pcaState !== pond.PCAState.PCA_STATE_OFF){ + //only display it if the final temp is between -4 and 60 C, is a valid number, and is not exactly 0 + //0 is technically a valid number but the likely hood of the calculation being exactly 0 is negligible + if (gate.status.finalTemp < 60 && gate.status.finalTemp > -40 && !isNaN(gate.status.finalTemp) && gate.status.finalTemp !== 0){ + display = getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? CtoF(gate.status.finalTemp).toFixed(2) + "°F" : gate.status.finalTemp.toFixed(2) + "°C" + } + } + return ( + + + {display} + + + )} }, { title: "Last FLow", render: gate => ( - Last Flow: - {gate.status.lastMassAirflow.toFixed(2)} + Flow: + {gate.status.lastMassAirflow.toFixed(2)} kg/s - Last Reading: + Read: {gate.status.lastUpdate !== "" ? moment(gate.status.lastUpdate).fromNow() : "No Reading Yet"} ) + }, + { + title: "Outlet Temperature", + render: gate => ( + {lastOutletReading(gate.status.lastTempReading)} + ) + }, + { + title: "Outlet Pressure", + render: gate => ( + {lastOutletReading(gate.status.lastPressureReading)} + ) } ] } diff --git a/src/pages/Gate.tsx b/src/pages/Gate.tsx index 767bb22..780600a 100644 --- a/src/pages/Gate.tsx +++ b/src/pages/Gate.tsx @@ -192,7 +192,9 @@ export default function Gate(props: Props) { const deviceDrawer = () => { return ( - + + + Connect Device + - - - Connect Device - - Click here to add a device to the gate. To add an additional device to a gate use the "Link Device" icon on the top right side of this page From e42f1a3fb08fe702673ebfab6db6b1cbee71dd43 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Wed, 28 Jan 2026 14:59:32 -0600 Subject: [PATCH 09/43] updated the gate page to use the pca state in the gates status for the lights on the visualizer and the pca fan on/off, this also allows me to get rid of setting those base on the data loaded for the graph which will fix the bug where the light could change based on what date range you loaded --- src/gate/GateDevice.tsx | 28 ++++++++++++++-------------- src/gate/GateFlowGraph.tsx | 24 ++++++++++++------------ src/gate/GateGraphs.tsx | 10 +++++----- src/gate/GateSVG.tsx | 30 ++++++++++++++++++++---------- src/pages/Gate.tsx | 2 +- 5 files changed, 52 insertions(+), 42 deletions(-) diff --git a/src/gate/GateDevice.tsx b/src/gate/GateDevice.tsx index 63ec92c..bfc1641 100644 --- a/src/gate/GateDevice.tsx +++ b/src/gate/GateDevice.tsx @@ -51,8 +51,8 @@ export default function GateDevice(props: Props) { const [interactionDialog, setInteractionDialog] = useState(false); const [densityTemp, setDensityTemp] = useState(0); const isMobile = useMobile(); - const [pcaState, setPCAState] = useState(false); - const [pcaFanOn, setPCAFanOn] = useState(false); + // const [pcaState, setPCAState] = useState(false); + // const [pcaFanOn, setPCAFanOn] = useState(false); const [detail, setDetail] = useState<"sensors" | "analytics">("analytics"); const [lastAmbient, setLastAmbient] = useState(0); const [lastTemps, setLastTemps] = useState({ t1: 0, t2: 0 }); @@ -99,13 +99,13 @@ export default function GateDevice(props: Props) { break; case pond.GateComponentType.GATE_COMPONENT_TYPE_PRESSURE: setPressureKey(c.key()); - if ( - c.status.measurement[0] && - c.status.measurement[0].values[0] && - c.status.measurement[0].values[0].values[1] - ) { - setPCAFanOn(c.status.measurement[0].values[0].values[1] > 996); //apx 4 iwg - } + // if ( + // c.status.measurement[0] && + // c.status.measurement[0].values[0] && + // c.status.measurement[0].values[0].values[1] + // ) { + // setPCAFanOn(c.status.measurement[0].values[0].values[1] > 996); //apx 4 iwg + // } break; default: // type is unknown, do nothing @@ -345,8 +345,8 @@ export default function GateDevice(props: Props) { ambientSelector={ambientSelector} tempChainSelector={tempSelector} pressureChainSelector={pressureSelector} - pcaState={pcaState} - pcaFanState={pcaFanOn} + pcaState={gate.status.pcaState} + // pcaFanState={pcaFanOn} checkInTime={device.status.lastActive} /> + From f6e53aff1c618d1175495848570d90cbd3ef6355 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Thu, 12 Feb 2026 14:55:43 -0600 Subject: [PATCH 26/43] added some minor functionality to the field page so clicking a harvest plan from the table updated the overview with it, also fixed a bug that came up regarding that with the settings using the plan permissions when they should be using the permissions passed in --- src/field/FieldList.tsx | 20 ++------------------ src/harvestPlan/HarvestPlanActions.tsx | 1 + src/harvestPlan/HarvestPlanTable.tsx | 6 +++++- src/harvestPlan/HarvestSettings.tsx | 9 ++++++--- src/pages/Field.tsx | 4 ++-- src/tasks/TaskCard.tsx | 10 +++++++--- 6 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/field/FieldList.tsx b/src/field/FieldList.tsx index b437840..f8ad8ef 100644 --- a/src/field/FieldList.tsx +++ b/src/field/FieldList.tsx @@ -6,26 +6,17 @@ import { Button, Grid2, IconButton, - Paper, Stack, Tab, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, Tabs, Typography } from "@mui/material"; import React, { useCallback, useEffect, useState } from "react"; -import { useMobile, useSnackbar, useUserAPI } from "hooks"; +import { useMobile, useSnackbar } from "hooks"; import { useGlobalState, useFieldAPI, useJohnDeereProxyAPI, useCNHiProxyAPI } from "providers"; //import HarvestTable from "harvestPlan/HarvestTable"; -import { Field, fieldScope, teamScope } from "models"; +import { Field } from "models"; import GrainDescriber from "grain/GrainDescriber"; -import { pond } from "protobuf-ts/pond"; -import HarvestSettings from "harvestPlan/HarvestSettings"; import ResponsiveTable, { Column } from "common/ResponsiveTable"; import FieldMinimap from "./Fieldminimap"; import FieldActions from "./FieldActions"; @@ -36,12 +27,6 @@ import ShareAllFields from "./ShareAllFields"; import EventBlocker from "common/EventBlocker"; import { useNavigate } from "react-router-dom"; -interface TabPanelProps { - children?: React.ReactNode; - index: any; - value: any; -} - export default function FieldList() { const [{ as }] = useGlobalState(); const isMobile = useMobile(); @@ -50,7 +35,6 @@ export default function FieldList() { const cnhAPI = useCNHiProxyAPI(); const { openSnack } = useSnackbar(); const [tabValue, setTabValue] = React.useState(0); - const [openHarvestSettings, setOpenHarvestSettings] = useState(false); // const [fieldForPlan, setFieldForPlan] = useState(Field.create()); const [fields, setFields] = useState([]); const [total, setTotal] = useState(0) diff --git a/src/harvestPlan/HarvestPlanActions.tsx b/src/harvestPlan/HarvestPlanActions.tsx index f29c1e4..a3de850 100644 --- a/src/harvestPlan/HarvestPlanActions.tsx +++ b/src/harvestPlan/HarvestPlanActions.tsx @@ -161,6 +161,7 @@ export default function HarvestPlanActions(props: Props) { update={updatePlan} open={openState.settings} plan={plan.key() !== "" ? plan : undefined} + permissions={permissions} close={(refresh, updatedPlan) => { setOpenState({ ...openState, settings: false }); if (refresh) { diff --git a/src/harvestPlan/HarvestPlanTable.tsx b/src/harvestPlan/HarvestPlanTable.tsx index a4f48c1..645b804 100644 --- a/src/harvestPlan/HarvestPlanTable.tsx +++ b/src/harvestPlan/HarvestPlanTable.tsx @@ -11,6 +11,7 @@ import { getThemeType } from "theme" interface Props { field: Field + planSelected: (plan: HarvestPlan) => void } const useStyles = makeStyles((theme: Theme) => ({ @@ -25,7 +26,7 @@ const useStyles = makeStyles((theme: Theme) => ({ })); export default function HarvestPlanTable(props: Props){ - const {field} = props + const {field, planSelected} = props const harvestAPI = useHarvestPlanAPI() const [page, setPage] = useState(0) const [pageSize, setPageSize] = useState(10) @@ -114,6 +115,9 @@ export default function HarvestPlanTable(props: Props){ title={"Harvest Plans"} resizeable page={page} + onRowClick={(row) => { + planSelected(row) + }} pageSize={pageSize} rows={loadedPlans} total={total} diff --git a/src/harvestPlan/HarvestSettings.tsx b/src/harvestPlan/HarvestSettings.tsx index a33e0eb..966c528 100644 --- a/src/harvestPlan/HarvestSettings.tsx +++ b/src/harvestPlan/HarvestSettings.tsx @@ -35,6 +35,7 @@ interface Props { field: Field; update?: boolean; plan?: HarvestPlan; + permissions?: pond.Permission[] } const cropOptions = [ @@ -160,7 +161,7 @@ const useStyles = makeStyles((theme: Theme) => ({ const steps = ["Pre-Seeding", "Seeding", "Post-Seeding", "Harvest", "Fall"]; export default function HarvestSettings(props: Props) { - const { open, plan, close, field, update } = props; + const { open, plan, close, field, update, permissions } = props; const [{as}] = useGlobalState() const [planKey, setPlanKey] = useState(); const [newPlan, setNewPlan] = useState(HarvestPlan.create()); @@ -184,6 +185,8 @@ export default function HarvestSettings(props: Props) { const { openSnack } = useSnackbar(); const [deleteOpen, setDeleteOpen] = useState(false); + + const loadTasks = useCallback(() => { if (planKey) { taskAPI.getMultiTasks([planKey], as).then(resp => { @@ -201,7 +204,7 @@ export default function HarvestSettings(props: Props) { }, [loadTasks]); useEffect(() => { - if (!plan || (plan && plan.permissions.includes(pond.Permission.PERMISSION_WRITE))) { + if (!plan || (permissions && permissions.includes(pond.Permission.PERMISSION_WRITE))) { setActiveStep(0); } else { setActiveStep(1); @@ -415,7 +418,7 @@ export default function HarvestSettings(props: Props) { key={0} classes={{ root: classes.tab }} disabled={ - !planKey || !(plan && plan.permissions.includes(pond.Permission.PERMISSION_WRITE)) + !planKey || !(permissions && permissions.includes(pond.Permission.PERMISSION_WRITE)) } label={"General"} aria-label={"general"} diff --git a/src/pages/Field.tsx b/src/pages/Field.tsx index 19472ae..15c29db 100644 --- a/src/pages/Field.tsx +++ b/src/pages/Field.tsx @@ -227,7 +227,7 @@ export default function FieldPage() { - + {setHPlan(plan)}}/> @@ -276,7 +276,7 @@ export default function FieldPage() { - + {setHPlan(plan)}}/> diff --git a/src/tasks/TaskCard.tsx b/src/tasks/TaskCard.tsx index 6151b01..bac70e0 100644 --- a/src/tasks/TaskCard.tsx +++ b/src/tasks/TaskCard.tsx @@ -20,7 +20,7 @@ import Edit from "@mui/icons-material/Edit"; import DeleteIcon from "@mui/icons-material/Delete"; import { red } from "@mui/material/colors"; import { pond } from "protobuf-ts/pond"; -import { taskScope } from "models/Scope"; +import { taskScope, teamScope } from "models/Scope"; import EventBlocker from "common/EventBlocker"; interface Props { @@ -58,19 +58,22 @@ export default function TaskCard(props: Props) { const [day, setDay] = useState(0); const classes = useStyles(); const [permissions, setPermissions] = useState([]); - const [{ user }] = useGlobalState(); + const [{ user, as }] = useGlobalState(); const userAPI = useUserAPI(); const [menuAnchorEl, setMenuAnchorEl] = useState(null); const loadPermissions = useCallback(() => { let scope = taskScope(props.task.key); + if(as){//if viewing as a team use the permissions to the team, and not the task itself + scope = teamScope(as) + } userAPI .getUser(user.id(), scope) .then(resp => { setPermissions(resp.permissions); }) .catch(err => {}); - }, [props.task, userAPI, user]); + }, [props.task, userAPI, user, as]); useEffect(() => { loadPermissions(); @@ -176,3 +179,4 @@ export default function TaskCard(props: Props) { ); } + From 73a4d6afea4288a5f652cfe7f5882cab5c3d13d4 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Thu, 12 Feb 2026 14:59:40 -0600 Subject: [PATCH 27/43] fixing airflow component measurement types --- src/pbHelpers/ComponentTypes/Airflow.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pbHelpers/ComponentTypes/Airflow.ts b/src/pbHelpers/ComponentTypes/Airflow.ts index b441256..ddfbc48 100644 --- a/src/pbHelpers/ComponentTypes/Airflow.ts +++ b/src/pbHelpers/ComponentTypes/Airflow.ts @@ -33,13 +33,13 @@ export function Airflow(subtype: number = 0): ComponentTypeExtension { let measurementTypes = [ { - measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PPM, + measurementType: quack.MeasurementType.MEASUREMENT_TYPE_CFM, label: cfm.label(), colour: cfm.colour(), graphType: cfm.graph() } as ComponentMeasurement, { - measurementType: quack.MeasurementType.MEASUREMENT_TYPE_VOLTAGE, + measurementType: quack.MeasurementType.MEASUREMENT_TYPE_SPEED, label: velocity.label(), colour: velocity.colour(), graphType: velocity.graph() From e41670be8f319af19470783a204706a5520b377f Mon Sep 17 00:00:00 2001 From: csawatzky Date: Thu, 12 Feb 2026 15:02:43 -0600 Subject: [PATCH 28/43] added comment in the measurement describer --- src/pbHelpers/MeasurementDescriber.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pbHelpers/MeasurementDescriber.ts b/src/pbHelpers/MeasurementDescriber.ts index 790b9a9..67a5b38 100644 --- a/src/pbHelpers/MeasurementDescriber.ts +++ b/src/pbHelpers/MeasurementDescriber.ts @@ -453,7 +453,7 @@ export class MeasurementDescriber { case quack.MeasurementType.MEASUREMENT_TYPE_SPEED: let speedUnit = getDistanceUnit() this.details.label = "Speed"; - this.details.unit = speedUnit === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "m/s" : "ft/m"; + this.details.unit = speedUnit === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "m/s" : "ft/m"; //yes this is meters per second and feet per minute this.details.colour = green["500"]; this.details.path = "edgeTriggered.rises"; this.details.max = 500; From b5f6139bc0aa730bbbd4e9d6084184db953eb4c9 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Thu, 12 Feb 2026 15:43:29 -0600 Subject: [PATCH 29/43] added place to set the actual yield for harvest plans at their completion --- src/harvestPlan/HarvestSettings.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/harvestPlan/HarvestSettings.tsx b/src/harvestPlan/HarvestSettings.tsx index 966c528..ca5802e 100644 --- a/src/harvestPlan/HarvestSettings.tsx +++ b/src/harvestPlan/HarvestSettings.tsx @@ -178,6 +178,7 @@ export default function HarvestSettings(props: Props) { const [variety, setVariety] = useState(""); const [harvestYear, setHarvestYear] = useState(new Date().getFullYear()); const [targetYield, setTargetYield] = useState(0); + const [actualYield, setActualYield] = useState(0); const [price, setPrice] = useState(""); const [newTaskDialog, setNewTaskDialog] = useState(false); const [type, setType] = useState(""); @@ -365,6 +366,7 @@ export default function HarvestSettings(props: Props) { tempPlan.settings.grainType = variety; tempPlan.settings.harvestYear = harvestYear; tempPlan.settings.yieldTarget = targetYield; + tempPlan.settings.actualYield = tempPlan.settings.bushelPrice = !isNaN(parseFloat(price)) ? Math.round(parseFloat(price) * 100) / 100 : 0; @@ -501,6 +503,16 @@ export default function HarvestSettings(props: Props) { !isNaN(+e.target.value) && setTargetYield(+e.target.value); }} /> + { + !isNaN(+e.target.value) && setActualYield(+e.target.value); + }} + /> Date: Thu, 12 Feb 2026 16:16:53 -0600 Subject: [PATCH 30/43] fixed bug where the linear progress never went away because the loading boolean was never being reset when no contracts were loaded, also added to the note in the contracts that the names of custom types have to match --- src/contract/contractSettings.tsx | 2 +- src/pages/Contracts.tsx | 22 ++++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/contract/contractSettings.tsx b/src/contract/contractSettings.tsx index d52fb15..097c9d5 100644 --- a/src/contract/contractSettings.tsx +++ b/src/contract/contractSettings.tsx @@ -402,7 +402,7 @@ export default function ContractSettings(props: Props) { borderRadius: 5, margin: 20 }}> - Note: When delivering grain on contract from a field or a bin the Grain Types must match + Note: When delivering grain on contract from a field or a bin the Grain Types must match, if it is a custom type the names must match. { let contracts: Contract[] = []; let contractPermissions: Map = new Map(); - resp.data.contracts.forEach(contract => { - let c = Contract.create(contract); - contracts.push(c); - let p = pond.EvaluatePermissionsResponse.fromObject( - resp.data.contractPermissions[c.key()] - ); - contractPermissions.set(c.key(), p.permissions); - }); - setContracts(contracts); - setContractPermissions(contractPermissions); + if (resp.data.contracts){ + resp.data.contracts.forEach(contract => { + let c = Contract.create(contract); + contracts.push(c); + let p = pond.EvaluatePermissionsResponse.fromObject( + resp.data.contractPermissions[c.key()] + ); + contractPermissions.set(c.key(), p.permissions); + }); + setContracts(contracts); + setContractPermissions(contractPermissions); + } setLoading(false); }) .catch(err => {}); From 762a630659aaa6691889d49a9ce33259c779fd6f Mon Sep 17 00:00:00 2001 From: csawatzky Date: Fri, 13 Feb 2026 12:51:26 -0600 Subject: [PATCH 31/43] some permission changes the team permission in the global state was coming back empty, so am now retrieving the team permissions for file uploads in grain transacitons, also adjusted the bin visualizer to disable the slider and inventory button when they do not have write permissions so they cant even open the transaction window, this should help prevent a case in the possible future where we allow bin transactions to have uploadable files from having a bin shared to them and uploading a file but not being able to create the transaction that it would be linked to thus causing the file to be uploaded to digital ocean, but not connected in our backend to anything --- src/bin/BinVisualizerV2.tsx | 7 +++++-- src/common/FileUploads/FileSelector.tsx | 11 ++++++----- src/common/FileUploads/FileUploader.tsx | 5 ++++- src/grain/GrainTransaction.tsx | 23 ++++++++++++++++++++--- 4 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/bin/BinVisualizerV2.tsx b/src/bin/BinVisualizerV2.tsx index d91f698..9997927 100644 --- a/src/bin/BinVisualizerV2.tsx +++ b/src/bin/BinVisualizerV2.tsx @@ -1434,6 +1434,7 @@ export default function BinVisualizer(props: Props) { const controls = () => { if (bin.settings.inventory?.empty === true) return null; + const canEdit = permissions ? permissions.includes(pond.Permission.PERMISSION_WRITE) : false; return ( { setGrainUpdate(true); }} style={{ margin: "auto", - backgroundColor: "gold", + backgroundColor: canEdit ? "gold" : "grey", color: "black", height: "100%", width: isMobile ? 35 : 40 @@ -1468,6 +1470,7 @@ export default function BinVisualizer(props: Props) { alignContent="flex-end"> {fillPercentage !== null && ( { @@ -53,8 +54,8 @@ const useStyles = makeStyles((theme: Theme) => { }); export default function FileSelector(props: Props) { - const { uploadEnd, uploadStart, uniqueID, toAttach, keys, types } = props; - const [{ userTeamPermissions }] = useGlobalState(); + const { uploadEnd, uploadStart, uniqueID, toAttach, keys, types, hasFilePermission } = props; + // const [{ userTeamPermissions }] = useGlobalState(); const [fileList, setFileList] = useState(); const classes = useStyles(); const fileAPI = useFileControllerAPI(); @@ -142,7 +143,7 @@ export default function FileSelector(props: Props) { disabled={ uploading || (!uploadFailed && fileID !== "") || - !userTeamPermissions.includes(pond.Permission.PERMISSION_FILE_MANAGEMENT) + !hasFilePermission } multiple={false} name={uniqueID || "fileInput"} @@ -163,7 +164,7 @@ export default function FileSelector(props: Props) {