finished the expanded cables dialog, worked on the scrolling for bin cables in the pages table view, did some mobile view edits

This commit is contained in:
csawatzky 2026-06-01 15:12:13 -06:00
parent c3ee4a518c
commit dc52ef77a7
6 changed files with 617 additions and 112 deletions

View file

@ -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<Column[]>(() => {
// 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 (
<Table>
<TableHead>
<TableRow>
<TableCell className={classes.headerCellStyle}>
Level
</TableCell>
<TableCell className={classes.headerCellStyle}>
Temperature
</TableCell>
<TableCell className={classes.headerCellStyle}>
Moisture
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{/* 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 (
<TableRow key={nodeIndex}>
<TableCell padding="none" className={classes.cellStyle}>
{nodeIndex + 1}
</TableCell>
<TableCell
padding="none"
className={classes.cellStyle}
sx={{ backgroundColor: tempData.color || undefined }}>
{tempData.display}
</TableCell>
<TableCell
padding="none"
className={classes.cellStyle}
sx={{ backgroundColor: moistureData.color || undefined }}>
{moistureData.display}
</TableCell>
</TableRow>
)})}
</TableBody>
</Table>
)
}
return (
// <Table className={classes.tableStyle}>
// <TableHead>
// <TableRow>
// <TableCell className={classes.headerCellStyle}>Level</TableCell>
// {columns.map((col, i) => (
// <TableCell key={i} className={classes.headerCellStyle}>
// {col.label}
// </TableCell>
// ))}
// </TableRow>
// </TableHead>
// <TableBody>
// {/* Rows are reversed so highest node is at top, matching BinTableView */}
// {Array.from({ length: rowCount }, (_, i) => rowCount - 1 - i).map(nodeIndex => (
// <TableRow key={nodeIndex}>
// <TableCell padding="none" className={classes.cellStyle}>
// {nodeIndex + 1}
// </TableCell>
// {columns.map((col, colI) => {
// const cable = cables[col.cableIndex]
// const { display, color } = renderCell(cable, nodeIndex, col.type)
// return (
// <TableCell
// key={colI}
// padding="none"
// className={classes.cellStyle}
// sx={{ backgroundColor: color || undefined }}
// >
// {display}
// </TableCell>
// )
// })}
// </TableRow>
// ))}
// </TableBody>
// </Table>
<Box padding={1}>
{/* Table Legend */}
<Box display="flex" alignItems="center" justifyContent={"space-between"} marginY={2}>
<Box display="flex" alignItems="center" gap={1}>
<Box sx={{ width: 40, height: 20, backgroundColor: inBounds, borderRadius: 1, flexShrink: 0 }} />
<Typography sx={{fontWeight: 600, fontSize: 10}}>On or below target</Typography>
</Box>
<Box display="flex" alignItems="center" gap={1}>
<Box sx={{ width: 40, height: 20, backgroundColor: caution, borderRadius: 1, flexShrink: 0 }} />
<Typography sx={{fontWeight: 600, fontSize: 10}}>{user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "+10 °F" : "+5 °C"} above target</Typography>
</Box>
<Box display="flex" alignItems="center" gap={1}>
<Box sx={{ width: 40, height: 20, backgroundColor: warning, borderRadius: 1, flexShrink: 0 }} />
<Typography sx={{fontWeight: 600, fontSize: 10}}>{user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "+20 °F" : "+10 °C"} above target</Typography>
</Box>
</Box>
<Box sx={{ display: "flex", flexDirection: "row", overflowX: "auto", gap: 2}}>
{cables.map((cable, i) => (
<Box key={i} sx={{ flexShrink: 0 }}>
<Typography className={classes.headerCellStyle} sx={{ textAlign: "center" }}>
{cable.name}
</Typography>
{cableTable(cable)}
</Box>
))}
</Box>
</Box>
)
}

View file

@ -3,7 +3,7 @@ import { Avatar, Box, Button, Card, Checkbox, FormControlLabel, Grid2, IconButto
import BinSensorCard from "bin/BinSensorCard"; import BinSensorCard from "bin/BinSensorCard";
import { GetDefaultDateRange } from "common/time/DateRange"; import { GetDefaultDateRange } from "common/time/DateRange";
import { ExtractMoisture } from "grain"; import { ExtractMoisture } from "grain";
import { useThemeType } from "hooks"; import { useMobile, useThemeType } from "hooks";
import { Bin, Component } from "models"; import { Bin, Component } from "models";
import { Ambient } from "models/Ambient"; import { Ambient } from "models/Ambient";
import { CO2 } from "models/CO2"; import { CO2 } from "models/CO2";
@ -72,6 +72,7 @@ export default function BinSensorsDisplay(props: Props){
const [startDate, setStartDate] = useState<Moment>(defaultDateRange.start); const [startDate, setStartDate] = useState<Moment>(defaultDateRange.start);
const [endDate, setEndDate] = useState<Moment>(defaultDateRange.end); const [endDate, setEndDate] = useState<Moment>(defaultDateRange.end);
const [filterNodes, setFilterNodes] = useState(true) const [filterNodes, setFilterNodes] = useState(true)
const isMobile = useMobile()
//organize the components into the correct 'positions' using the bins preferences //organize the components into the correct 'positions' using the bins preferences
useEffect(()=>{ useEffect(()=>{
@ -596,16 +597,17 @@ export default function BinSensorsDisplay(props: Props){
controller.settings.defaultOutputState = mode controller.settings.defaultOutputState = mode
let device = componentDevices.get(controller.key()) let device = componentDevices.get(controller.key())
if(device){ if(device){
componentAPI.update(device, controller.settings).then(resp => { componentAPI.update(device, controller.settings).then(resp => {
console.log("updated controller") controller.mode = mode // ← update the field the UI reads
if (prefType === pond.BinComponent.BIN_COMPONENT_FAN) {
setFans(prev => [...prev]) if (prefType === pond.BinComponent.BIN_COMPONENT_FAN) {
} else { setFans(prev => prev.map(f => f.key() === controller.key() ? controller : f))
setHeaters(prev => [...prev]) } else {
} setHeaters(prev => prev.map(h => h.key() === controller.key() ? controller : h))
}).catch(err => { }
console.log("there was a problem updating the controller") }).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 ? "Show Cards" : "Show Graphs"}
</Button> </Button>
{showGraphs && {showGraphs && !isMobile &&
<TimeBar updateDateRange={updateDateRange} startDate={startDate} endDate={endDate} /> <TimeBar updateDateRange={updateDateRange} startDate={startDate} endDate={endDate} />
} }
{cables.length > 0 && {cables.length > 0 &&
@ -719,50 +721,20 @@ export default function BinSensorsDisplay(props: Props){
{assignmentMenu()} {assignmentMenu()}
{sensorCount > 0 && {sensorCount > 0 &&
<Box> <Box>
<Box display="flex" gap={2} alignItems="center"> <Box display="flex" gap={2} alignItems="center" marginBottom={1}>
<Typography sx={{fontWeight: 650, fontSize: 25}}>Sensors</Typography> <Typography sx={{fontWeight: 650, fontSize: 25}}>Sensors</Typography>
{graphControls()} {graphControls()}
</Box> </Box>
{showGraphs && isMobile &&
<TimeBar updateDateRange={updateDateRange} startDate={startDate} endDate={endDate} />
}
<Grid2 container spacing={2}> <Grid2 container spacing={2}>
{cables.map(cable => {
let device = componentDevices.get(cable.key())
if(showGraphs && device){
let icon = GetComponentIcon(cable.type(), cable.subType(), themeType)
return(
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
<BinSensorGraph
icon={icon}
title={cable.name()}
tag={"Cable Averages"}
device={device}
component={components.get(cable.key())!}
startDate={startDate}
endDate={endDate}
allNodes={!filterNodes}
onMenuClick={(event) => {
setComponentUnnassigned(false);
setSelectedComponentType(cable.type());
setSelectedComponentKey(cable.key());
setSelectedComponentTopNode(cable.settings.grainFilledTo);
setAnchorEl(event.currentTarget);
}}
/>
</Grid2>
)
}
return(
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
{cableCard(cable)}
</Grid2>
)
}
)}
{plenums.map(plenum => { {plenums.map(plenum => {
let device = componentDevices.get(plenum.key()) let device = componentDevices.get(plenum.key())
if(showGraphs && device){ if(showGraphs && device){
let icon = GetComponentIcon(plenum.type(), plenum.subType(), themeType) let icon = GetComponentIcon(plenum.type(), plenum.subType(), themeType)
return( return(
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}> <Grid2 size={{xs: 12, sm: 6, md: 4, lg: 3}}>
<BinSensorGraph <BinSensorGraph
icon={icon} icon={icon}
title={plenum.name()} title={plenum.name()}
@ -782,7 +754,7 @@ export default function BinSensorsDisplay(props: Props){
) )
} }
return( return(
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}> <Grid2 size={{xs: 12, sm: 6, md: 4, lg: 3}}>
{tempHumidCard(plenum)} {tempHumidCard(plenum)}
</Grid2> </Grid2>
) )
@ -793,7 +765,7 @@ export default function BinSensorsDisplay(props: Props){
if(showGraphs && device){ if(showGraphs && device){
let icon = GetComponentIcon(pressure.type(), pressure.subType(), themeType) let icon = GetComponentIcon(pressure.type(), pressure.subType(), themeType)
return( return(
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}> <Grid2 size={{xs: 12, sm: 6, md: 4, lg: 3}}>
<BinSensorGraph <BinSensorGraph
icon={icon} icon={icon}
title={pressure.name()} title={pressure.name()}
@ -813,7 +785,7 @@ export default function BinSensorsDisplay(props: Props){
) )
} }
return( return(
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}> <Grid2 size={{xs: 12, sm: 6, md: 4, lg: 3}}>
{pressureCard(pressure)} {pressureCard(pressure)}
</Grid2> </Grid2>
) )
@ -823,7 +795,7 @@ export default function BinSensorsDisplay(props: Props){
if(showGraphs && device){ if(showGraphs && device){
let icon = GetComponentIcon(ambient.type(), ambient.subType(), themeType) let icon = GetComponentIcon(ambient.type(), ambient.subType(), themeType)
return( return(
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}> <Grid2 size={{xs: 12, sm: 6, md: 4, lg: 3}}>
<BinSensorGraph <BinSensorGraph
icon={icon} icon={icon}
title={ambient.name()} title={ambient.name()}
@ -843,7 +815,7 @@ export default function BinSensorsDisplay(props: Props){
) )
} }
return( return(
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}> <Grid2 size={{xs: 12, sm: 6, md: 4, lg: 3}}>
{tempHumidCard(ambient)} {tempHumidCard(ambient)}
</Grid2> </Grid2>
) )
@ -854,7 +826,7 @@ export default function BinSensorsDisplay(props: Props){
if(showGraphs && device){ if(showGraphs && device){
let icon = GetComponentIcon(h.type(), h.subType(), themeType) let icon = GetComponentIcon(h.type(), h.subType(), themeType)
return( return(
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}> <Grid2 size={{xs: 12, sm: 6, md: 4, lg: 3}}>
<BinSensorGraph <BinSensorGraph
icon={icon} icon={icon}
title={h.name()} title={h.name()}
@ -874,7 +846,7 @@ export default function BinSensorsDisplay(props: Props){
) )
} }
return( return(
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}> <Grid2 size={{xs: 12, sm: 6, md: 4, lg: 3}}>
{tempHumidCard(h)} {tempHumidCard(h)}
</Grid2> </Grid2>
) )
@ -885,13 +857,27 @@ export default function BinSensorsDisplay(props: Props){
if(showGraphs && device){ if(showGraphs && device){
let icon = GetComponentIcon(co2.type(), co2.subType(), themeType) let icon = GetComponentIcon(co2.type(), co2.subType(), themeType)
return( return(
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}> <Grid2 size={{xs: 12, sm: 6, md: 4, lg: 3}}>
<BinSensorGraph device={device} component={components.get(co2.key())!} startDate={startDate} endDate={endDate} /> <BinSensorGraph
icon={icon}
title={co2.name()}
tag={"Headspace"}
device={device}
component={components.get(co2.key())!}
startDate={startDate}
endDate={endDate}
onMenuClick={(event) => {
setComponentUnnassigned(false);
setSelectedComponentType(co2.type());
setSelectedComponentKey(co2.key());
setAnchorEl(event.currentTarget);
}}
/>
</Grid2> </Grid2>
) )
} }
return( return(
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}> <Grid2 size={{xs: 12, sm: 6, md: 4, lg: 3}}>
{co2Card(co2)} {co2Card(co2)}
</Grid2> </Grid2>
) )
@ -899,18 +885,65 @@ export default function BinSensorsDisplay(props: Props){
{headspaceLidar.map(lidar => { {headspaceLidar.map(lidar => {
let device = componentDevices.get(lidar.key()) let device = componentDevices.get(lidar.key())
if(showGraphs && device){ if(showGraphs && device){
let icon = GetComponentIcon(lidar.type(), lidar.subType(), themeType)
return( return(
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}> <Grid2 size={{xs: 12, sm: 6, md: 4, lg: 3}}>
<BinSensorGraph device={device} component={components.get(lidar.key())!} startDate={startDate} endDate={endDate} /> <BinSensorGraph
icon={icon}
title={lidar.name()}
tag={"Headspace"}
device={device}
component={components.get(lidar.key())!}
startDate={startDate}
endDate={endDate}
onMenuClick={(event) => {
setComponentUnnassigned(false);
setSelectedComponentType(lidar.type());
setSelectedComponentKey(lidar.key());
setAnchorEl(event.currentTarget);
}}
/>
</Grid2> </Grid2>
) )
} }
return ( return (
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}> <Grid2 size={{xs: 12, sm: 6, md: 4, lg: 3}}>
{lidarCard(lidar)} {lidarCard(lidar)}
</Grid2> </Grid2>
) )
})} })}
{cables.map(cable => {
let device = componentDevices.get(cable.key())
if(showGraphs && device){
let icon = GetComponentIcon(cable.type(), cable.subType(), themeType)
return(
<Grid2 size={{xs: 12, sm: 6, md: 4, lg: 3}}>
<BinSensorGraph
icon={icon}
title={cable.name()}
tag={"Cable Averages"}
device={device}
component={components.get(cable.key())!}
startDate={startDate}
endDate={endDate}
allNodes={!filterNodes}
onMenuClick={(event) => {
setComponentUnnassigned(false);
setSelectedComponentType(cable.type());
setSelectedComponentKey(cable.key());
setSelectedComponentTopNode(cable.settings.grainFilledTo);
setAnchorEl(event.currentTarget);
}}
/>
</Grid2>
)
}
return(
<Grid2 size={{xs: 12, sm: 6, md: 4, lg: 3}}>
{cableCard(cable)}
</Grid2>
)
})}
</Grid2> </Grid2>
</Box> </Box>
} }
@ -922,14 +955,29 @@ export default function BinSensorsDisplay(props: Props){
{heaters.map(heater => { {heaters.map(heater => {
let device = componentDevices.get(heater.key()) let device = componentDevices.get(heater.key())
if(showGraphs && device){ if(showGraphs && device){
let icon = GetComponentIcon(heater.type(), heater.subType(), themeType)
return( return(
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}> <Grid2 size={{xs: 12, sm: 6, md: 4, lg: 3}}>
<BinSensorGraph device={device} component={components.get(heater.key())!} startDate={startDate} endDate={endDate} /> <BinSensorGraph
device={device}
icon={icon}
title={heater.name()}
tag={"Heater"}
component={components.get(heater.key())!}
startDate={startDate}
endDate={endDate}
onMenuClick={(event) => {
setComponentUnnassigned(false);
setSelectedComponentType(heater.type());
setSelectedComponentKey(heater.key());
setAnchorEl(event.currentTarget);
}}
/>
</Grid2> </Grid2>
) )
} }
return ( return (
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}> <Grid2 size={{xs: 12, sm: 6, md: 4, lg: 3}}>
{controllerCard(heater)} {controllerCard(heater)}
</Grid2> </Grid2>
) )
@ -937,14 +985,29 @@ export default function BinSensorsDisplay(props: Props){
{fans.map(fan => { {fans.map(fan => {
let device = componentDevices.get(fan.key()) let device = componentDevices.get(fan.key())
if(showGraphs && device){ if(showGraphs && device){
let icon = GetComponentIcon(fan.type(), fan.subType(), themeType)
return( return(
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}> <Grid2 size={{xs: 12, sm: 6, md: 4, lg: 3}}>
<BinSensorGraph device={device} component={components.get(fan.key())!} startDate={startDate} endDate={endDate} /> <BinSensorGraph
device={device}
icon={icon}
title={fan.name()}
tag={"Fan"}
component={components.get(fan.key())!}
startDate={startDate}
endDate={endDate}
onMenuClick={(event) => {
setComponentUnnassigned(false);
setSelectedComponentType(fan.type());
setSelectedComponentKey(fan.key());
setAnchorEl(event.currentTarget);
}}
/>
</Grid2> </Grid2>
) )
} }
return ( return (
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}> <Grid2 size={{xs: 12, sm: 6, md: 4, lg: 3}}>
{controllerCard(fan)} {controllerCard(fan)}
</Grid2> </Grid2>
) )

View file

@ -10,6 +10,7 @@ import { NodeData } from "bin/3dView/Data/BuildNodeData";
import { CableData } from "bin/3dView/Data/BuildCableData"; import { CableData } from "bin/3dView/Data/BuildCableData";
import ModeChangeDialog from "bin/conditioning/modeChangeDialog"; import ModeChangeDialog from "bin/conditioning/modeChangeDialog";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { useMobile } from "hooks";
interface Props { interface Props {
bin: Bin bin: Bin
@ -41,6 +42,7 @@ export default function bin3dVisualizer(props: Props){
const [openNodeControls, setOpenNodeControls] = useState(false) const [openNodeControls, setOpenNodeControls] = useState(false)
const [devMap, setDevMap] = useState<Map<number, Device>>(new Map()) const [devMap, setDevMap] = useState<Map<number, Device>>(new Map())
const [modeChangeInProgress, setModeChangeInProgress] = useState(false) const [modeChangeInProgress, setModeChangeInProgress] = useState(false)
const isMobile = useMobile()
useEffect(()=>{ useEffect(()=>{
let newMap: Map<number, Device> = new Map() let newMap: Map<number, Device> = new Map()
@ -65,7 +67,7 @@ export default function bin3dVisualizer(props: Props){
const binModeControl = () => { const binModeControl = () => {
return ( return (
<Box> <Box>
<Typography sx={{fontWeight: 650, fontSize: 20}}>{bin.name()}</Typography> <Typography sx={{fontWeight: 650, fontSize: isMobile ? 13 : 20}}>{bin.name()}</Typography>
<FormControl fullWidth> <FormControl fullWidth>
<Select <Select
value={binMode} value={binMode}
@ -100,7 +102,7 @@ export default function bin3dVisualizer(props: Props){
const heatmapDisplay = () => { const heatmapDisplay = () => {
return ( return (
<Box> <Box>
<Typography sx={{fontWeight: 650, fontSize: 20}}>Heatmap Display</Typography> <Typography sx={{fontWeight: 650, fontSize: isMobile ? 13 : 20}}>Heatmap Display</Typography>
<FormControl fullWidth> <FormControl fullWidth>
{/* <InputLabel>Display</InputLabel> */} {/* <InputLabel>Display</InputLabel> */}
<Select <Select
@ -160,7 +162,7 @@ export default function bin3dVisualizer(props: Props){
const controllerDisplay = () => { const controllerDisplay = () => {
return ( return (
<Box> <Box>
<Typography sx={{fontWeight: 650, fontSize: 20}}> <Typography sx={{fontWeight: 650, fontSize: isMobile ? 13 : 20}}>
Controllers Controllers
</Typography> </Typography>
{fans && fans.map(fan => { {fans && fans.map(fan => {
@ -178,9 +180,9 @@ export default function bin3dVisualizer(props: Props){
py: 1, py: 1,
}} }}
> >
<Typography variant="body2">{fan.name()}</Typography> <Typography sx={{fontSize: isMobile ? 13 : 15}}>{fan.name()}</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body2"> <Typography sx={{fontWeight: 650, fontSize: isMobile ? 13 : 15}}>
{isOn ? "ON" : "OFF"} {isOn ? "ON" : "OFF"}
</Typography> </Typography>
<Box sx={{ <Box sx={{
@ -207,9 +209,9 @@ export default function bin3dVisualizer(props: Props){
py: 1, py: 1,
}} }}
> >
<Typography variant="body2">{heater.name()}</Typography> <Typography sx={{fontSize: isMobile ? 13 : 15}}>{heater.name()}</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body2"> <Typography sx={{fontWeight: 650, fontSize: isMobile ? 13 : 15}}>
{isOn ? "ON" : "OFF"} {isOn ? "ON" : "OFF"}
</Typography> </Typography>
<Box sx={{ <Box sx={{
@ -227,6 +229,63 @@ export default function bin3dVisualizer(props: Props){
) )
} }
const desktopHUD = () => {
return (
<Box position={"absolute"} top={20} left={30} height={"80%"} width={"30%"} padding={2}>
<Stack direction={"column"} justifyContent={"space-between"} sx={{height: "100%", width: "100%"}}>
{binModeControl()}
{heatmapDisplay()}
{controllerDisplay()}
</Stack>
</Box>
)
}
const mobileHUD = () => {
return (
<React.Fragment>
{/* Top-left: Bin Name / Mode */}
<Box
position="absolute"
top={12}
left={12}
width="40%"
sx={{ pointerEvents: 'none' }}
>
<Box sx={{ pointerEvents: 'auto' }}>
{binModeControl()}
</Box>
</Box>
{/* Top-right: Heatmap Display */}
<Box
position="absolute"
top={12}
right={12}
width="40%"
sx={{ pointerEvents: 'none' }}
>
<Box sx={{ pointerEvents: 'auto' }}>
{heatmapDisplay()}
</Box>
</Box>
{/* Bottom-left: Controller Status */}
<Box
position="absolute"
bottom={12}
left={12}
width="45%"
sx={{ pointerEvents: 'none' }}
>
<Box sx={{ pointerEvents: 'auto' }}>
{controllerDisplay()}
</Box>
</Box>
</React.Fragment>
)
}
return ( return (
<React.Fragment> <React.Fragment>
{(selectedCable && selectedNode) && {(selectedCable && selectedNode) &&
@ -325,15 +384,9 @@ export default function bin3dVisualizer(props: Props){
showMoisture={showMoisture} showMoisture={showMoisture}
// showGrain={showFill} // showGrain={showFill}
// showHotspots={showHotspots} // showHotspots={showHotspots}
xOffset={-150} xOffset={isMobile ? undefined : -150}
/> />
<Box position={"absolute"} top={20} left={30} height={"80%"} width={"30%"} padding={2}> {isMobile ? mobileHUD() : desktopHUD()}
<Stack direction={"column"} justifyContent={"space-between"} sx={{height: "100%", width: "100%"}}>
{binModeControl()}
{heatmapDisplay()}
{controllerDisplay()}
</Stack>
</Box>
</Box> </Box>
</React.Fragment> </React.Fragment>
) )

View file

@ -146,7 +146,9 @@ export default function BinControls(props: Props) {
setControls(controls); setControls(controls);
}).catch(err => { }).catch(err => {
console.error("Interaction fetch error:", err) console.error("Interaction fetch error:", err)
}) }).finally(() => {
setLoading(false)
})
} }
},[linkedComponents, interactionsAPI, componentDevices, as]) },[linkedComponents, interactionsAPI, componentDevices, as])

View file

@ -1,27 +1,36 @@
import { Edit, GppGood, Save } from "@mui/icons-material"; import { Edit, GppGood, Info, Save } from "@mui/icons-material";
import { Box, darken, IconButton, Table, TableBody, TableCell, TableHead, TableRow, TextField, Theme, Typography } from "@mui/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 { green, grey, orange, red, yellow } from "@mui/material/colors";
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
import ResponsiveDialog from "common/ResponsiveDialog";
import HumidityIcon from "component/HumidityIcon"; import HumidityIcon from "component/HumidityIcon";
import TemperatureIcon from "component/TemperatureIcon"; import TemperatureIcon from "component/TemperatureIcon";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { Bin } from "models"; import { Bin } from "models";
import { pond } from "protobuf-ts/pond"; import { pond } from "protobuf-ts/pond";
import { useGlobalState } from "providers"; import { useGlobalState } from "providers";
import React, { useEffect, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { useMemo } from "react"; import { useMemo } from "react";
import { avg, celsiusToFahrenheit } from "utils"; import { avg, celsiusToFahrenheit } from "utils";
import BinTableExpanded from "./BinTableExpanded";
import { useMobile } from "hooks";
const useStyles = makeStyles((theme: Theme) => ({ const useStyles = makeStyles((theme: Theme) => ({
tableStyle: { tableStyle: {
borderCollapse: "collapse", borderCollapse: "separate",
borderSpacing: 0, // removes the default gap that "separate" introduces
}, },
headerCellStyle: { headerCellStyle: {
fontWeight: 650, fontWeight: 650,
backgroundColor: darken(theme.palette.background.paper, 0.2), backgroundColor: darken(theme.palette.background.paper, 0.2),
fontSize: 15, fontSize: 15,
textAlign: "center", textAlign: "center",
border: "1px solid black" borderTop: "1px solid black",
borderLeft: "1px solid black",
borderBottom: "1px solid black",
"&:last-child": {
borderRight: "1px solid black",
}
}, },
cellStyle: { cellStyle: {
padding: 2, padding: 2,
@ -30,11 +39,6 @@ const useStyles = makeStyles((theme: Theme) => ({
textAlign: "center", textAlign: "center",
border: "1px solid black", 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 [tempEntry, setTempEntry] = useState("")
const [moistureEntry, setMoistureEntry] = useState("") const [moistureEntry, setMoistureEntry] = useState("")
const [moistureTarget, setMoistureTarget] = useState(0) const [moistureTarget, setMoistureTarget] = useState(0)
const [openExpandedCables, setOpenExpandedCables] = useState(false)
const mobileView = useMobile()
const inBounds = green[600] const inBounds = green[600]
const caution = yellow[800] const caution = yellow[800]
const warning = red[700] const warning = red[700]
const headerRow1Ref = useRef<HTMLTableRowElement>(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(()=>{ useEffect(()=>{
let val = bin.targetTemp() let val = bin.targetTemp()
@ -250,15 +264,54 @@ export default function BinTableView(props: Props) {
) )
} }
const expandedTableDialog = () => {
return (
<ResponsiveDialog open={openExpandedCables} onClose={()=>{setOpenExpandedCables(false)}}>
<BinTableExpanded
cables={bin.status.grainCables}
targetMoisture={bin.targetMoisture()}
targetTemp={bin.targetTemp()}
inBounds={inBounds}
caution={caution}
warning={warning}
/>
<DialogActions>
<Button onClick={()=> {setOpenExpandedCables(false)}}>
Close
</Button>
</DialogActions>
</ResponsiveDialog>
)
}
return ( return (
<Box padding={2}> <Box padding={2} sx={{ display: "flex", flexDirection: "column", height: "100%" }}>
{expandedTableDialog()}
{/* Level Table */} {/* Level Table */}
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography>Grain Table</Typography>
<Box display="flex" alignItems="center" gap={1}>
<Button
variant="contained"
onClick={() => {
setOpenExpandedCables(true)
}}>
Show All Cables
</Button>
<Tooltip title="testing">
<Info />
</Tooltip>
</Box>
</Box>
{/* this box is to make the whole table scrollable */}
<Box sx={{ overflowY: "auto", flex: 1, minHeight: 0 }}>
<Table className={classes.tableStyle}> <Table className={classes.tableStyle}>
<TableHead> <TableHead>
<TableRow> <TableRow ref={headerRow1Ref}>
<TableCell className={classes.thresholdCell}> <TableCell className={classes.headerCellStyle} sx={{ top: 0, position: "sticky", zIndex: 3 }}>
<Box justifyContent={"center"} sx={{ display: "flex", alignItems: "center", gap: 1 }}> <Box justifyContent={"center"} sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<GppGood /> <GppGood />
<Typography fontWeight={650}> <Typography fontWeight={650}>
@ -266,7 +319,7 @@ export default function BinTableView(props: Props) {
</Typography> </Typography>
</Box> </Box>
</TableCell> </TableCell>
<TableCell className={classes.thresholdCell}> <TableCell className={classes.headerCellStyle} sx={{ top: 0, position: "sticky", zIndex: 3 }}>
<Box justifyContent={"center"} sx={{ display: "flex", alignItems: "center", gap: 1 }}> <Box justifyContent={"center"} sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<TemperatureIcon heightWidth={30}/> <TemperatureIcon heightWidth={30}/>
{enterTemp ? {enterTemp ?
@ -296,7 +349,7 @@ export default function BinTableView(props: Props) {
} }
</Box> </Box>
</TableCell> </TableCell>
<TableCell className={classes.thresholdCell}> <TableCell className={classes.headerCellStyle} sx={{ top: 0, position: "sticky", zIndex: 3 }}>
<Box justifyContent={"center"} sx={{ display: "flex", alignItems: "center", gap: 1 }}> <Box justifyContent={"center"} sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<HumidityIcon height={30} width={20}/> <HumidityIcon height={30} width={20}/>
{enterMoisture ? {enterMoisture ?
@ -328,11 +381,12 @@ export default function BinTableView(props: Props) {
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow> <TableRow>
<TableCell className={classes.headerCellStyle}>Level</TableCell> <TableCell className={classes.headerCellStyle} sx={{ top: firstRowHeight, position: "sticky", zIndex: 3 }}>Level</TableCell>
<TableCell className={classes.headerCellStyle}>Temperature</TableCell> <TableCell className={classes.headerCellStyle} sx={{ top: firstRowHeight, position: "sticky", zIndex: 3 }}>Temperature</TableCell>
<TableCell className={classes.headerCellStyle}>Moisture</TableCell> <TableCell className={classes.headerCellStyle} sx={{ top: firstRowHeight, position: "sticky", zIndex: 3 }}>Moisture</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{binLevels.map((level, i) => ( {binLevels.map((level, i) => (
<TableRow key={i}> <TableRow key={i}>
@ -340,10 +394,46 @@ export default function BinTableView(props: Props) {
{levelDisplay(level)} {levelDisplay(level)}
</TableRow> </TableRow>
))} ))}
<TableRow>
<TableCell>TESTING</TableCell>
<TableCell>SCROLLING</TableCell>
<TableCell>ROWS</TableCell>
</TableRow>
<TableRow>
<TableCell>TESTING</TableCell>
<TableCell>SCROLLING</TableCell>
<TableCell>ROWS</TableCell>
</TableRow>
<TableRow>
<TableCell>TESTING</TableCell>
<TableCell>SCROLLING</TableCell>
<TableCell>ROWS</TableCell>
</TableRow>
<TableRow>
<TableCell>TESTING</TableCell>
<TableCell>SCROLLING</TableCell>
<TableCell>ROWS</TableCell>
</TableRow>
<TableRow>
<TableCell>TESTING</TableCell>
<TableCell>SCROLLING</TableCell>
<TableCell>ROWS</TableCell>
</TableRow>
<TableRow>
<TableCell>TESTING</TableCell>
<TableCell>SCROLLING</TableCell>
<TableCell>ROWS</TableCell>
</TableRow>
<TableRow>
<TableCell>TESTING</TableCell>
<TableCell>SCROLLING</TableCell>
<TableCell>ROWS</TableCell>
</TableRow>
</TableBody> </TableBody>
</Table> </Table>
</Box>
{/* Table Legend */} {/* Table Legend */}
<Box display="flex" alignItems="center" justifyContent={"space-between"} marginTop={2}> <Box display="flex" alignItems="center" justifyContent="space-between" marginTop={2} sx={{ flexShrink: 0 }}>
<Box display="flex" alignItems="center" gap={1}> <Box display="flex" alignItems="center" gap={1}>
<LegendSwatch color={inBounds} /> <LegendSwatch color={inBounds} />
<Typography sx={{fontWeight: 600, fontSize: 10}}>On or below target</Typography> <Typography sx={{fontWeight: 600, fontSize: 10}}>On or below target</Typography>

View file

@ -14,7 +14,7 @@ import { Pressure } from "models/Pressure";
import { CO2 } from "models/CO2"; import { CO2 } from "models/CO2";
import moment from "moment"; import moment from "moment";
import { quack } from "protobuf-ts/quack"; 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 { makeStyles } from "@mui/styles";
import NotesIcon from "@mui/icons-material/Notes"; import NotesIcon from "@mui/icons-material/Notes";
import TasksIcon from "products/Construction/TasksIcon"; import TasksIcon from "products/Construction/TasksIcon";
@ -22,10 +22,34 @@ import BindaptIcon from "products/Bindapt/BindaptIcon";
import FieldsIcon from "products/AgIcons/FieldsIcon"; import FieldsIcon from "products/AgIcons/FieldsIcon";
import BinActions from "bin/BinActions"; import BinActions from "bin/BinActions";
import BinSummary from "bin/binSummary/BinSummary"; 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 (
<div
role="tabpanel"
hidden={value !== index}
aria-labelledby={`simple-tab-${index}`}
{...other}>
{value === index && <React.Fragment>{children}</React.Fragment>}
</div>
);
}
const useStyles = makeStyles((theme: Theme) => { const useStyles = makeStyles((theme: Theme) => {
const themeType = theme.palette.mode; const themeType = theme.palette.mode;
const activeBG = theme.palette.secondary.main;
return ({ return ({
spacer: { spacer: {
width: "32px", width: "32px",
@ -46,6 +70,14 @@ const useStyles = makeStyles((theme: Theme) => {
menuPaper: { menuPaper: {
borderRadius: "20px" borderRadius: "20px"
}, },
drawerPaperDesktop: {
height: "100%",
width: "30%"
},
drawerPaperMobile: {
height: "50%",
width: "100%"
},
}) })
}) })
@ -76,6 +108,7 @@ export default function BinV2(){
const [fans, setFans] = useState<Controller[]>([]); const [fans, setFans] = useState<Controller[]>([]);
const [compositionNameMap, setCompositionNameMap] = useState<Map<string, string>>(new Map()); const [compositionNameMap, setCompositionNameMap] = useState<Map<string, string>>(new Map());
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [noteTab, setNoteTab] = useState(0)
const [componentDevices, setComponentDevices] = useState<Map<string, number>>( const [componentDevices, setComponentDevices] = useState<Map<string, number>>(
@ -438,21 +471,63 @@ export default function BinV2(){
) )
} }
const handleChange = (_event: React.ChangeEvent<{}>, newValue: number) => {
setNoteTab(newValue);
};
//DRAWERS //DRAWERS
const noteDrawer = () => { const noteDrawer = () => {
return ( return (
<Drawer open={noteDrawerOpen}> <Drawer
open={noteDrawerOpen}
</Drawer> anchor={isMobile ? "bottom" : "right"}
classes={{ paper: isMobile ? classes.drawerPaperMobile : classes.drawerPaperDesktop }}
onClose={() => setNoteDrawerOpen(false)}>
<IconButton style={{ width: 50 }} onClick={() => setNoteDrawerOpen(false)}>
<Close />
</IconButton>
<Tabs
value={noteTab}
indicatorColor="primary"
textColor="primary"
onChange={handleChange}
aria-label="disabled tabs example"
// centered={!(isMobile || displayMobile)}
style={{
marginLeft: "auto"
//marginTop: isMobile || displayMobile || drawer ? "-3rem" : "0.25rem"
}}>
<Tab label="Notes" />
<Tab label="History" />
</Tabs>
<TabPanelMine value={noteTab} index={0}>
{/* <Box height={isMobile || displayMobile ? "80vh" : "90vh"} padding={2}> */}
<Chat parent={binID} parentType={"bin"} type={pond.NoteType.NOTE_TYPE_BIN} />
{/* </Box> */}
</TabPanelMine>
<TabPanelMine value={noteTab} index={1}>
<BinHistory drawer binID={binID} />
</TabPanelMine>
</Drawer>
) )
} }
const taskDrawer = () => { const taskDrawer = () => {
return ( return (
<Drawer open={taskDrawerOpen}> <Drawer
style={{ zIndex: 1099 }}
</Drawer> open={taskDrawerOpen}
anchor={isMobile ? "bottom" : "right"}
classes={{ paper: isMobile ? classes.drawerPaperMobile : classes.drawerPaperDesktop }}
onClose={() => setTaskDrawerOpen(false)}>
<Box>
<IconButton style={{ width: 50 }} onClick={() => setTaskDrawerOpen(false)}>
<Close />
</IconButton>
<TaskViewer drawerView keys={[binID]} types={["bin"]} overlayButton />
</Box>
</Drawer>
) )
} }