222 lines
No EOL
9.4 KiB
TypeScript
222 lines
No EOL
9.4 KiB
TypeScript
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, paddingBottom: 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>
|
|
)
|
|
} |