frontend/src/bin/BinTableExpanded.tsx

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>
)
}