From b19b0337fa8dc0deda0d8c7cfb4ceda7d2d5942a Mon Sep 17 00:00:00 2001 From: Carter Date: Tue, 27 Jan 2026 10:56:20 -0600 Subject: [PATCH 01/26] switch to master proto --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fc7a1ac..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#staging", + "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 41aa84d28fa3a40de9cafcfd647cf893c06c62ac Mon Sep 17 00:00:00 2001 From: csawatzky Date: Tue, 27 Jan 2026 12:17:21 -0600 Subject: [PATCH 02/26] hotfix for the new whitelabel not getting the correct client id --- src/services/whiteLabel.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/whiteLabel.ts b/src/services/whiteLabel.ts index d9aac95..ca8bbf1 100644 --- a/src/services/whiteLabel.ts +++ b/src/services/whiteLabel.ts @@ -323,7 +323,8 @@ const whitelabels = new Map([ ["mivent", MIVENT_WHITE_LABEL], ["adaptiveconstruction", ADAPTIVE_CONSTRUCTION_WHITE_LABEL], ["omniair", OMNIAIR_WHITE_LABEL], - ["mipca", MIPCA_WHITE_LABEL] + ["mipca", MIPCA_WHITE_LABEL], + ["mionetech", MIPCA_WHITE_LABEL] ]); export function getWhitelabel(): WhiteLabel { From f02aa3a6e05b40b95f8d59158424046bb7f775de Mon Sep 17 00:00:00 2001 From: csawatzky Date: Wed, 28 Jan 2026 13:04:34 -0600 Subject: [PATCH 03/26] 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 04/26] 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 21/26] 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 22/26] 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 23/26] 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 24/26] 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 25/26] 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 26/26] 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) {