From dc52ef77a7a37bbf3e6a5734489b48cc3f41b56f Mon Sep 17 00:00:00 2001 From: csawatzky Date: Mon, 1 Jun 2026 15:12:13 -0600 Subject: [PATCH] finished the expanded cables dialog, worked on the scrolling for bin cables in the pages table view, did some mobile view edits --- src/bin/BinTableExpanded.tsx | 222 ++++++++++++++++++ src/bin/binSensorsDisplay.tsx | 195 +++++++++------ .../binSummary/components/bin3dVisualizer.tsx | 83 +++++-- src/bin/binSummary/components/binControls.tsx | 4 +- src/bin/binTableView.tsx | 134 +++++++++-- src/pages/BinV2.tsx | 91 ++++++- 6 files changed, 617 insertions(+), 112 deletions(-) create mode 100644 src/bin/BinTableExpanded.tsx diff --git a/src/bin/BinTableExpanded.tsx b/src/bin/BinTableExpanded.tsx new file mode 100644 index 0000000..5f180eb --- /dev/null +++ b/src/bin/BinTableExpanded.tsx @@ -0,0 +1,222 @@ +import { Box, Table, TableBody, TableCell, TableHead, TableRow, Theme, Typography } from "@mui/material"; +import { pond } from "protobuf-ts/pond"; +import { makeStyles } from "@mui/styles"; +import { useEffect, useMemo, useState } from "react"; +import { avg, celsiusToFahrenheit } from "utils"; +import { useGlobalState } from "providers"; +import { darken } from "@mui/material"; +import React from "react"; + +const useStyles = makeStyles((theme: Theme) => ({ + tableStyle: { borderCollapse: "collapse" }, + headerCellStyle: { + fontWeight: 650, + backgroundColor: darken(theme.palette.background.paper, 0.2), + fontSize: 15, + textAlign: "center", + border: "1px solid black" + }, + cellStyle: { + padding: 2, + fontWeight: 650, + fontSize: 17, + textAlign: "center", + border: "1px solid black", + }, +})); + +// interface Column { +// label: string // e.g. "Cable 1 Temp", "Cable 2 Moisture" +// cableIndex: number +// type: "temp" | "moisture" +// } + +interface Props { + cables: pond.GrainCable[] + targetTemp: number // in Celsius, for colouring + targetMoisture: number // for colouring + inBounds?: string + caution?: string + warning?: string + +} + +export default function BinTableExpanded(props: Props) { + const { cables, targetTemp, targetMoisture, inBounds = "#43a047", caution = "#f9a825", warning = "#c62828" } = props + const classes = useStyles() + const [{ user }] = useGlobalState() + + const useFahrenheit = user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT + + // Build column definitions by iterating cables once + // const columns = useMemo(() => { + // const cols: Column[] = [] + // cables.forEach((cable, cableIndex) => { + // cols.push({ label: `Cable ${cableIndex + 1} Temp`, cableIndex, type: "temp" }) + // // Same moisture-validity check as BinTableView + // if (cable.moisture.length > 0 && avg(cable.moisture) > 0) { + // cols.push({ label: `Cable ${cableIndex + 1} Moisture`, cableIndex, type: "moisture" }) + // } + // }) + // return cols + // }, [cables]) + + // Row count driven by the longest cable + const rowCount = useMemo( + () => Math.max(0, ...cables.map(c => c.celcius.length)), + [cables] + ) + + // Render a single cell value + background colour + const renderCell = (cable: pond.GrainCable, nodeIndex: number, type: "temp" | "moisture") => { + if (type === "temp") { + const raw = cable.celcius[nodeIndex] + if (raw === undefined) return { display: "--", color: "" } + + const isExcluded = cable.excludedNodes.includes(nodeIndex) + if (isExcluded) return { display: "--", color: "" } + + let display: string + let color = "" + const inGrain = nodeIndex < cable.topNode + + if (inGrain) { + if (raw > targetTemp + 5) color = warning + else if (raw > targetTemp) color = caution + else color = inBounds + } + + const converted = useFahrenheit ? celsiusToFahrenheit(raw) : raw + display = converted.toFixed(1) + (useFahrenheit ? " °F" : " °C") + return { display, color } + } + + // moisture + const raw = cable.moisture[nodeIndex] + if (raw === undefined || raw === 0) return { display: "--", color: "" } + + const isExcluded = cable.excludedNodes.includes(nodeIndex) + if (isExcluded) return { display: "--", color: "" } + + let color = "" + const inGrain = nodeIndex < cable.topNode + if (inGrain) { + if (raw > targetMoisture + 1.5) color = warning + else if (raw > targetMoisture) color = caution + else color = inBounds + } + return { display: raw.toFixed(2) + "%", color } + } + + const cableTable = (cable: pond.GrainCable) => { + return ( + + + + + Level + + + Temperature + + + Moisture + + + + + {/* Rows are reversed so highest node is at top, matching BinTableView */} + {Array.from({ length: rowCount }, (_, i) => rowCount - 1 - i).map(nodeIndex => { + + const tempData = renderCell(cable, nodeIndex, "temp") + const moistureData = renderCell(cable, nodeIndex, "moisture") + return ( + + + {nodeIndex + 1} + + + {tempData.display} + + + {moistureData.display} + + + )})} + +
+ ) + } + + return ( + // + // + // + // Level + // {columns.map((col, i) => ( + // + // {col.label} + // + // ))} + // + // + // + // {/* Rows are reversed so highest node is at top, matching BinTableView */} + // {Array.from({ length: rowCount }, (_, i) => rowCount - 1 - i).map(nodeIndex => ( + // + // + // {nodeIndex + 1} + // + // {columns.map((col, colI) => { + // const cable = cables[col.cableIndex] + // const { display, color } = renderCell(cable, nodeIndex, col.type) + // return ( + // + // {display} + // + // ) + // })} + // + // ))} + // + //
+ + {/* Table Legend */} + + + + On or below target + + + + {user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "+10 °F" : "+5 °C"} above target + + + + {user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "+20 °F" : "+10 °C"} above target + + + + {cables.map((cable, i) => ( + + + {cable.name} + + {cableTable(cable)} + + ))} + + + ) +} \ No newline at end of file diff --git a/src/bin/binSensorsDisplay.tsx b/src/bin/binSensorsDisplay.tsx index 09b5a3d..a2085e1 100644 --- a/src/bin/binSensorsDisplay.tsx +++ b/src/bin/binSensorsDisplay.tsx @@ -3,7 +3,7 @@ import { Avatar, Box, Button, Card, Checkbox, FormControlLabel, Grid2, IconButto import BinSensorCard from "bin/BinSensorCard"; import { GetDefaultDateRange } from "common/time/DateRange"; import { ExtractMoisture } from "grain"; -import { useThemeType } from "hooks"; +import { useMobile, useThemeType } from "hooks"; import { Bin, Component } from "models"; import { Ambient } from "models/Ambient"; import { CO2 } from "models/CO2"; @@ -72,6 +72,7 @@ export default function BinSensorsDisplay(props: Props){ const [startDate, setStartDate] = useState(defaultDateRange.start); const [endDate, setEndDate] = useState(defaultDateRange.end); const [filterNodes, setFilterNodes] = useState(true) + const isMobile = useMobile() //organize the components into the correct 'positions' using the bins preferences useEffect(()=>{ @@ -596,16 +597,17 @@ export default function BinSensorsDisplay(props: Props){ controller.settings.defaultOutputState = mode let device = componentDevices.get(controller.key()) if(device){ - componentAPI.update(device, controller.settings).then(resp => { - console.log("updated controller") - if (prefType === pond.BinComponent.BIN_COMPONENT_FAN) { - setFans(prev => [...prev]) - } else { - setHeaters(prev => [...prev]) - } - }).catch(err => { - console.log("there was a problem updating the controller") - }) + componentAPI.update(device, controller.settings).then(resp => { + controller.mode = mode // ← update the field the UI reads + + if (prefType === pond.BinComponent.BIN_COMPONENT_FAN) { + setFans(prev => prev.map(f => f.key() === controller.key() ? controller : f)) + } else { + setHeaters(prev => prev.map(h => h.key() === controller.key() ? controller : h)) + } + }).catch(err => { + console.log("there was a problem updating the controller") + }) } }} @@ -693,7 +695,7 @@ export default function BinSensorsDisplay(props: Props){ }}> {showGraphs ? "Show Cards" : "Show Graphs"} - {showGraphs && + {showGraphs && !isMobile && } {cables.length > 0 && @@ -719,50 +721,20 @@ export default function BinSensorsDisplay(props: Props){ {assignmentMenu()} {sensorCount > 0 && - + Sensors {graphControls()} + {showGraphs && isMobile && + + } - {cables.map(cable => { - let device = componentDevices.get(cable.key()) - if(showGraphs && device){ - let icon = GetComponentIcon(cable.type(), cable.subType(), themeType) - return( - - { - setComponentUnnassigned(false); - setSelectedComponentType(cable.type()); - setSelectedComponentKey(cable.key()); - setSelectedComponentTopNode(cable.settings.grainFilledTo); - setAnchorEl(event.currentTarget); - }} - /> - - ) - } - return( - - {cableCard(cable)} - - ) - } - )} {plenums.map(plenum => { let device = componentDevices.get(plenum.key()) if(showGraphs && device){ let icon = GetComponentIcon(plenum.type(), plenum.subType(), themeType) return( - + + {tempHumidCard(plenum)} ) @@ -793,7 +765,7 @@ export default function BinSensorsDisplay(props: Props){ if(showGraphs && device){ let icon = GetComponentIcon(pressure.type(), pressure.subType(), themeType) return( - + + {pressureCard(pressure)} ) @@ -823,7 +795,7 @@ export default function BinSensorsDisplay(props: Props){ if(showGraphs && device){ let icon = GetComponentIcon(ambient.type(), ambient.subType(), themeType) return( - + + {tempHumidCard(ambient)} ) @@ -854,7 +826,7 @@ export default function BinSensorsDisplay(props: Props){ if(showGraphs && device){ let icon = GetComponentIcon(h.type(), h.subType(), themeType) return( - + + {tempHumidCard(h)} ) @@ -885,13 +857,27 @@ export default function BinSensorsDisplay(props: Props){ if(showGraphs && device){ let icon = GetComponentIcon(co2.type(), co2.subType(), themeType) return( - - + + { + setComponentUnnassigned(false); + setSelectedComponentType(co2.type()); + setSelectedComponentKey(co2.key()); + setAnchorEl(event.currentTarget); + }} + /> ) } return( - + {co2Card(co2)} ) @@ -899,18 +885,65 @@ export default function BinSensorsDisplay(props: Props){ {headspaceLidar.map(lidar => { let device = componentDevices.get(lidar.key()) if(showGraphs && device){ + let icon = GetComponentIcon(lidar.type(), lidar.subType(), themeType) return( - - + + { + setComponentUnnassigned(false); + setSelectedComponentType(lidar.type()); + setSelectedComponentKey(lidar.key()); + setAnchorEl(event.currentTarget); + }} + /> ) } return ( - + {lidarCard(lidar)} ) })} + {cables.map(cable => { + let device = componentDevices.get(cable.key()) + if(showGraphs && device){ + let icon = GetComponentIcon(cable.type(), cable.subType(), themeType) + return( + + { + setComponentUnnassigned(false); + setSelectedComponentType(cable.type()); + setSelectedComponentKey(cable.key()); + setSelectedComponentTopNode(cable.settings.grainFilledTo); + setAnchorEl(event.currentTarget); + }} + /> + + ) + } + return( + + {cableCard(cable)} + + ) + })} } @@ -922,14 +955,29 @@ export default function BinSensorsDisplay(props: Props){ {heaters.map(heater => { let device = componentDevices.get(heater.key()) if(showGraphs && device){ + let icon = GetComponentIcon(heater.type(), heater.subType(), themeType) return( - - + + { + setComponentUnnassigned(false); + setSelectedComponentType(heater.type()); + setSelectedComponentKey(heater.key()); + setAnchorEl(event.currentTarget); + }} + /> ) } return ( - + {controllerCard(heater)} ) @@ -937,14 +985,29 @@ export default function BinSensorsDisplay(props: Props){ {fans.map(fan => { let device = componentDevices.get(fan.key()) if(showGraphs && device){ + let icon = GetComponentIcon(fan.type(), fan.subType(), themeType) return( - - + + { + setComponentUnnassigned(false); + setSelectedComponentType(fan.type()); + setSelectedComponentKey(fan.key()); + setAnchorEl(event.currentTarget); + }} + /> ) } return ( - + {controllerCard(fan)} ) diff --git a/src/bin/binSummary/components/bin3dVisualizer.tsx b/src/bin/binSummary/components/bin3dVisualizer.tsx index 996e1aa..a2b4b82 100644 --- a/src/bin/binSummary/components/bin3dVisualizer.tsx +++ b/src/bin/binSummary/components/bin3dVisualizer.tsx @@ -10,6 +10,7 @@ import { NodeData } from "bin/3dView/Data/BuildNodeData"; import { CableData } from "bin/3dView/Data/BuildCableData"; import ModeChangeDialog from "bin/conditioning/modeChangeDialog"; import { cloneDeep } from "lodash"; +import { useMobile } from "hooks"; interface Props { bin: Bin @@ -41,6 +42,7 @@ export default function bin3dVisualizer(props: Props){ const [openNodeControls, setOpenNodeControls] = useState(false) const [devMap, setDevMap] = useState>(new Map()) const [modeChangeInProgress, setModeChangeInProgress] = useState(false) + const isMobile = useMobile() useEffect(()=>{ let newMap: Map = new Map() @@ -65,7 +67,7 @@ export default function bin3dVisualizer(props: Props){ const binModeControl = () => { return ( - {bin.name()} + {bin.name()} { return ( - + Controllers {fans && fans.map(fan => { @@ -178,9 +180,9 @@ export default function bin3dVisualizer(props: Props){ py: 1, }} > - {fan.name()} + {fan.name()} - + {isOn ? "ON" : "OFF"} - {heater.name()} + {heater.name()} - + {isOn ? "ON" : "OFF"} { + return ( + + + {binModeControl()} + {heatmapDisplay()} + {controllerDisplay()} + + + ) + } + + const mobileHUD = () => { + return ( + + {/* Top-left: Bin Name / Mode */} + + + {binModeControl()} + + + + {/* Top-right: Heatmap Display */} + + + {heatmapDisplay()} + + + + {/* Bottom-left: Controller Status */} + + + {controllerDisplay()} + + + + ) + } + return ( {(selectedCable && selectedNode) && @@ -325,15 +384,9 @@ export default function bin3dVisualizer(props: Props){ showMoisture={showMoisture} // showGrain={showFill} // showHotspots={showHotspots} - xOffset={-150} + xOffset={isMobile ? undefined : -150} /> - - - {binModeControl()} - {heatmapDisplay()} - {controllerDisplay()} - - + {isMobile ? mobileHUD() : desktopHUD()} ) diff --git a/src/bin/binSummary/components/binControls.tsx b/src/bin/binSummary/components/binControls.tsx index 5d400c2..a1a4cde 100644 --- a/src/bin/binSummary/components/binControls.tsx +++ b/src/bin/binSummary/components/binControls.tsx @@ -146,7 +146,9 @@ export default function BinControls(props: Props) { setControls(controls); }).catch(err => { console.error("Interaction fetch error:", err) - }) + }).finally(() => { + setLoading(false) + }) } },[linkedComponents, interactionsAPI, componentDevices, as]) diff --git a/src/bin/binTableView.tsx b/src/bin/binTableView.tsx index c5ebc5a..d7eb26d 100644 --- a/src/bin/binTableView.tsx +++ b/src/bin/binTableView.tsx @@ -1,27 +1,36 @@ -import { Edit, GppGood, Save } from "@mui/icons-material"; -import { Box, darken, IconButton, Table, TableBody, TableCell, TableHead, TableRow, TextField, Theme, Typography } from "@mui/material"; +import { Edit, GppGood, Info, Save } from "@mui/icons-material"; +import { Box, Button, darken, DialogActions, IconButton, Table, TableBody, TableCell, TableHead, TableRow, TextField, Theme, Tooltip, Typography } from "@mui/material"; import { green, grey, orange, red, yellow } from "@mui/material/colors"; import { makeStyles } from "@mui/styles"; +import ResponsiveDialog from "common/ResponsiveDialog"; import HumidityIcon from "component/HumidityIcon"; import TemperatureIcon from "component/TemperatureIcon"; import { cloneDeep } from "lodash"; import { Bin } from "models"; import { pond } from "protobuf-ts/pond"; import { useGlobalState } from "providers"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { useMemo } from "react"; import { avg, celsiusToFahrenheit } from "utils"; +import BinTableExpanded from "./BinTableExpanded"; +import { useMobile } from "hooks"; const useStyles = makeStyles((theme: Theme) => ({ tableStyle: { - borderCollapse: "collapse", + borderCollapse: "separate", + borderSpacing: 0, // removes the default gap that "separate" introduces }, headerCellStyle: { - fontWeight: 650, + fontWeight: 650, backgroundColor: darken(theme.palette.background.paper, 0.2), - fontSize: 15, - textAlign: "center", - border: "1px solid black" + fontSize: 15, + textAlign: "center", + borderTop: "1px solid black", + borderLeft: "1px solid black", + borderBottom: "1px solid black", + "&:last-child": { + borderRight: "1px solid black", + } }, cellStyle: { padding: 2, @@ -30,11 +39,6 @@ const useStyles = makeStyles((theme: Theme) => ({ textAlign: "center", border: "1px solid black", - }, - thresholdCell: { - fontWeight: 650, - fontSize: 15, - textAlign: "center", } }) ); @@ -62,9 +66,19 @@ export default function BinTableView(props: Props) { const [tempEntry, setTempEntry] = useState("") const [moistureEntry, setMoistureEntry] = useState("") const [moistureTarget, setMoistureTarget] = useState(0) + const [openExpandedCables, setOpenExpandedCables] = useState(false) + const mobileView = useMobile() const inBounds = green[600] const caution = yellow[800] const warning = red[700] + const headerRow1Ref = useRef(null) + const [firstRowHeight, setFirstRowHeight] = useState(0) + + useEffect(() => { + if (headerRow1Ref.current) { + setFirstRowHeight(headerRow1Ref.current.getBoundingClientRect().height) + } + }, [enterTemp, enterMoisture]) // re-measure when the row height might change due to TextField appearing useEffect(()=>{ let val = bin.targetTemp() @@ -250,15 +264,54 @@ export default function BinTableView(props: Props) { ) } + const expandedTableDialog = () => { + return ( + {setOpenExpandedCables(false)}}> + + + + + + ) + } + return ( - + + {expandedTableDialog()} {/* Level Table */} + + Grain Table + + + + + + + + + {/* this box is to make the whole table scrollable */} + - - + + @@ -266,7 +319,7 @@ export default function BinTableView(props: Props) { - + {enterTemp ? @@ -296,7 +349,7 @@ export default function BinTableView(props: Props) { } - + {enterMoisture ? @@ -328,11 +381,12 @@ export default function BinTableView(props: Props) { - Level - Temperature - Moisture + Level + Temperature + Moisture + {binLevels.map((level, i) => ( @@ -340,10 +394,46 @@ export default function BinTableView(props: Props) { {levelDisplay(level)} ))} + + TESTING + SCROLLING + ROWS + + + TESTING + SCROLLING + ROWS + + + TESTING + SCROLLING + ROWS + + + TESTING + SCROLLING + ROWS + + + TESTING + SCROLLING + ROWS + + + TESTING + SCROLLING + ROWS + + + TESTING + SCROLLING + ROWS +
+
{/* Table Legend */} - + On or below target diff --git a/src/pages/BinV2.tsx b/src/pages/BinV2.tsx index 22abf2b..819dfda 100644 --- a/src/pages/BinV2.tsx +++ b/src/pages/BinV2.tsx @@ -14,7 +14,7 @@ import { Pressure } from "models/Pressure"; import { CO2 } from "models/CO2"; import moment from "moment"; import { quack } from "protobuf-ts/quack"; -import { Box, Drawer, Grid2 as Grid, IconButton, ListItemIcon, ListItemText, Menu, MenuItem, Theme, Tooltip } from "@mui/material"; +import { Box, Drawer, Grid2 as Grid, IconButton, ListItemIcon, ListItemText, Menu, MenuItem, Tab, Tabs, Theme, Tooltip } from "@mui/material"; import { makeStyles } from "@mui/styles"; import NotesIcon from "@mui/icons-material/Notes"; import TasksIcon from "products/Construction/TasksIcon"; @@ -22,10 +22,34 @@ import BindaptIcon from "products/Bindapt/BindaptIcon"; import FieldsIcon from "products/AgIcons/FieldsIcon"; import BinActions from "bin/BinActions"; import BinSummary from "bin/binSummary/BinSummary"; +import { Close } from "@mui/icons-material"; +import React from "react"; +import BinHistory from "bin/BinHistory"; +import Chat from "chat/Chat"; +import TaskViewer from "tasks/TaskViewer"; + +interface TabPanelProps { + children?: React.ReactNode; + index: any; + value: any; +} + +function TabPanelMine(props: TabPanelProps) { + const { children, value, index, ...other } = props; + + return ( + + ); +} const useStyles = makeStyles((theme: Theme) => { const themeType = theme.palette.mode; - const activeBG = theme.palette.secondary.main; return ({ spacer: { width: "32px", @@ -46,6 +70,14 @@ const useStyles = makeStyles((theme: Theme) => { menuPaper: { borderRadius: "20px" }, + drawerPaperDesktop: { + height: "100%", + width: "30%" + }, + drawerPaperMobile: { + height: "50%", + width: "100%" + }, }) }) @@ -76,6 +108,7 @@ export default function BinV2(){ const [fans, setFans] = useState([]); const [compositionNameMap, setCompositionNameMap] = useState>(new Map()); const [anchorEl, setAnchorEl] = useState(null); + const [noteTab, setNoteTab] = useState(0) const [componentDevices, setComponentDevices] = useState>( @@ -438,21 +471,63 @@ export default function BinV2(){ ) } + const handleChange = (_event: React.ChangeEvent<{}>, newValue: number) => { + setNoteTab(newValue); + }; + //DRAWERS const noteDrawer = () => { return ( - - - + setNoteDrawerOpen(false)}> + setNoteDrawerOpen(false)}> + + + + + + + + {/* */} + + {/* */} + + + + + ) } const taskDrawer = () => { return ( - - - + setTaskDrawerOpen(false)}> + + setTaskDrawerOpen(false)}> + + + + + ) }