Merge branch 'staging_environment' of gitlab.com:brandx/bxt-app into staging_environment

This commit is contained in:
Carter 2026-02-23 14:45:27 -06:00
commit 2413cc36d1
28 changed files with 652 additions and 227 deletions

2
package-lock.json generated
View file

@ -11967,7 +11967,7 @@
}, },
"node_modules/protobuf-ts": { "node_modules/protobuf-ts": {
"version": "1.0.0", "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#ab9e8d4fd38f9af7802398403d38fddda5eb01da",
"dependencies": { "dependencies": {
"protobufjs": "^6.8.8" "protobufjs": "^6.8.8"
} }

View file

@ -1434,6 +1434,7 @@ export default function BinVisualizer(props: Props) {
const controls = () => { const controls = () => {
if (bin.settings.inventory?.empty === true) return null; if (bin.settings.inventory?.empty === true) return null;
const canEdit = permissions ? permissions.includes(pond.Permission.PERMISSION_WRITE) : false;
return ( return (
<Box <Box
height={1} height={1}
@ -1447,12 +1448,13 @@ export default function BinVisualizer(props: Props) {
<React.Fragment> <React.Fragment>
<Box display="flex" height={isMobile ? 35 : 40} marginBottom={1}> <Box display="flex" height={isMobile ? 35 : 40} marginBottom={1}>
<IconButton <IconButton
disabled={!canEdit}
onClick={() => { onClick={() => {
setGrainUpdate(true); setGrainUpdate(true);
}} }}
style={{ style={{
margin: "auto", margin: "auto",
backgroundColor: "gold", backgroundColor: canEdit ? "gold" : "grey",
color: "black", color: "black",
height: "100%", height: "100%",
width: isMobile ? 35 : 40 width: isMobile ? 35 : 40
@ -1468,6 +1470,7 @@ export default function BinVisualizer(props: Props) {
alignContent="flex-end"> alignContent="flex-end">
{fillPercentage !== null && ( {fillPercentage !== null && (
<Slider <Slider
disabled={!canEdit}
orientation="vertical" orientation="vertical"
value={fillPercentage} value={fillPercentage}
classes={{ classes={{
@ -1477,7 +1480,7 @@ export default function BinVisualizer(props: Props) {
// thumb: isMobile ? classes.mobileSliderThumb : classes.sliderThumb, // thumb: isMobile ? classes.mobileSliderThumb : classes.sliderThumb,
valueLabel: classes.sliderValLabel, valueLabel: classes.sliderValLabel,
}} }}
style={{ height: "100%", color: sliderColour }} style={{ height: "100%", color: canEdit ? sliderColour : "gray" }}
min={0} min={0}
max={100} max={100}
valueLabelDisplay="auto" valueLabelDisplay="auto"

View file

@ -29,7 +29,7 @@ export default function BinUtilizationChart(props: Props) {
const theme = useTheme(); const theme = useTheme();
const grainColour = customColour ? customColour : GrainColour(grain); const grainColour = customColour ? customColour : GrainColour(grain);
return ( return (
<Box height={1} width={1}> <Box height={200} width={200}>
<CardActionArea <CardActionArea
onClick={onClick} onClick={onClick}
style={{ height: "100%", borderRadius: theme.shape.borderRadius }}> style={{ height: "100%", borderRadius: theme.shape.borderRadius }}>

View file

@ -1,9 +1,10 @@
import { useTheme } from "@mui/material"; import { useTheme } from "@mui/material";
import moment from "moment"; import moment from "moment";
import React, { useEffect, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { import {
Area, Area,
AreaChart, AreaChart,
Legend,
ReferenceArea, ReferenceArea,
ReferenceLine, ReferenceLine,
ResponsiveContainer, ResponsiveContainer,
@ -13,25 +14,38 @@ import {
YAxis YAxis
} from "recharts"; } from "recharts";
import MaterialChartTooltip from "./MaterialChartTooltip"; import MaterialChartTooltip from "./MaterialChartTooltip";
import { blue, orange } from "@mui/material/colors";
import { Payload } from "recharts/types/component/DefaultLegendContent";
export interface SSAreaDataPoint { export interface SSAreaDataPoint {
timestamp: number; timestamp: number;
value: number; value: number;
} }
export interface ColourData {
colour: string,
label: string
}
interface Props { interface Props {
data: SSAreaDataPoint[]; data: SSAreaDataPoint[];
tooltipLabel: string
tooltipUnit?: string
yAxisLabel: string
maxRef?: number; maxRef?: number;
minRef?: number; minRef?: number;
newXDomain?: number[] | string[]; newXDomain?: number[] | string[];
multiGraphZoom?: (domain: number[] | string[]) => void; multiGraphZoom?: (domain: number[] | string[]) => void;
colourAboveZero?: ColourData
colourBelowZero?: ColourData
} }
export default function SingleSetAreaChart(props: Props) { export default function SingleSetAreaChart(props: Props) {
const { data, maxRef, minRef, newXDomain, multiGraphZoom } = props; const { data, maxRef, minRef, newXDomain, multiGraphZoom, yAxisLabel, colourAboveZero, colourBelowZero, tooltipLabel, tooltipUnit } = props;
const [xDomain, setXDomain] = useState<string[] | number[]>(["dataMin", "dataMax"]); const [xDomain, setXDomain] = useState<string[] | number[]>(["dataMin", "dataMax"]);
const [refLeft, setRefLeft] = useState<number | undefined>(); const [refLeft, setRefLeft] = useState<number | undefined>();
const [refRight, setRefRight] = useState<number | undefined>(); const [refRight, setRefRight] = useState<number | undefined>();
const [legendPayload, setLegendPayload] = useState<Payload[]>([])
const theme = useTheme(); const theme = useTheme();
const now = moment(); const now = moment();
@ -41,6 +55,40 @@ export default function SingleSetAreaChart(props: Props) {
} }
}, [newXDomain]); }, [newXDomain]);
useEffect(() => {
let legend: Payload[] = []
if(colourAboveZero){
legend.push({
value: colourAboveZero.label,
color: colourAboveZero.colour,
id: colourAboveZero.label,
type: "square"
})
}
if(colourBelowZero){
legend.push({
value: colourBelowZero.label,
color: colourBelowZero.colour,
id: colourBelowZero.label,
type: "square"
})
}
setLegendPayload(legend)
}, [colourAboveZero, colourBelowZero])
const gradientOffset = useMemo(() => {
if (!data.length) return 0;
const values = data.map(p => p.value);
const max = Math.max(...values);
const min = Math.min(...values);
if (max <= 0) return 0;
if (min >= 0) return 1;
return max / (max - min);
}, [data]);
const zoom = () => { const zoom = () => {
let newDomain: number[] | string[] = ["dataMin", "dataMax"]; let newDomain: number[] | string[] = ["dataMin", "dataMax"];
if (refLeft && refRight && refLeft !== refRight) { if (refLeft && refRight && refLeft !== refRight) {
@ -75,12 +123,16 @@ export default function SingleSetAreaChart(props: Props) {
setRefRight(undefined); setRefRight(undefined);
zoom(); zoom();
}}> }}>
<Legend
verticalAlign="top"
payload={legendPayload}
/>
<Tooltip <Tooltip
animationEasing="ease-out" animationEasing="ease-out"
cursor={{ fill: theme.palette.text.primary, opacity: "0.15" }} cursor={{ fill: theme.palette.text.primary, opacity: "0.15" }}
labelFormatter={timestamp => moment(timestamp).format("lll")} labelFormatter={timestamp => moment(timestamp).format("lll")}
content={(props: TooltipProps<any, any>) => ( content={(props: TooltipProps<any, any>) => (
<MaterialChartTooltip {...props} valueFormatter={value => `${value}`} /> <MaterialChartTooltip {...props} valueFormatter={value => `${value}` + (tooltipUnit ? tooltipUnit : "")} />
)} )}
/> />
<XAxis <XAxis
@ -102,13 +154,26 @@ export default function SingleSetAreaChart(props: Props) {
type="number" type="number"
domain={["auto", "auto"]} domain={["auto", "auto"]}
label={{ label={{
value: "Mass Flow (kg/s)", value: yAxisLabel,
position: "insideLeft", position: "insideLeft",
angle: -90, angle: -90,
fill: theme.palette.text.primary fill: theme.palette.text.primary
}} }}
/> />
<Area dataKey={"value"} strokeWidth={3} /> <defs>
{colourAboveZero && colourBelowZero &&
<linearGradient id="solidColour" x1="0" y1="0" x2="0" y2="1">
<stop offset={gradientOffset} stopColor={colourAboveZero.colour} stopOpacity="75%" />
<stop offset={gradientOffset} stopColor={colourBelowZero.colour} stopOpacity="75%" />
</linearGradient>
}
</defs>
<Area dataKey={"value"} strokeWidth={3}
type="monotone"
name={tooltipLabel}
stroke={(colourAboveZero && colourBelowZero) && "url(#solidColour)"}
fill={(colourAboveZero && colourBelowZero) && "url(#solidColour)"}
/>
<ReferenceLine <ReferenceLine
ifOverflow="extendDomain" ifOverflow="extendDomain"
y={maxRef} y={maxRef}

View file

@ -9,7 +9,7 @@ import {
Typography Typography
} from "@mui/material"; } from "@mui/material";
import { useFileControllerAPI, useGlobalState } from "providers"; import { useFileControllerAPI, useGlobalState } from "providers";
import { useState } from "react"; import { useEffect, useState } from "react";
import ErrorIcon from "@mui/icons-material/Error"; import ErrorIcon from "@mui/icons-material/Error";
import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline"; import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline";
import { getThemeType } from "theme"; import { getThemeType } from "theme";
@ -25,6 +25,7 @@ interface Props {
uniqueID?: string; uniqueID?: string;
keys?: string[]; keys?: string[];
types?: string[]; types?: string[];
hasFilePermission: boolean;
} }
const useStyles = makeStyles((theme: Theme) => { const useStyles = makeStyles((theme: Theme) => {
@ -53,8 +54,8 @@ const useStyles = makeStyles((theme: Theme) => {
}); });
export default function FileSelector(props: Props) { export default function FileSelector(props: Props) {
const { uploadEnd, uploadStart, uniqueID, toAttach, keys, types } = props; const { uploadEnd, uploadStart, uniqueID, toAttach, keys, types, hasFilePermission } = props;
const [{ userTeamPermissions }] = useGlobalState(); // const [{ userTeamPermissions }] = useGlobalState();
const [fileList, setFileList] = useState<FileList | undefined>(); const [fileList, setFileList] = useState<FileList | undefined>();
const classes = useStyles(); const classes = useStyles();
const fileAPI = useFileControllerAPI(); const fileAPI = useFileControllerAPI();
@ -142,7 +143,7 @@ export default function FileSelector(props: Props) {
disabled={ disabled={
uploading || uploading ||
(!uploadFailed && fileID !== "") || (!uploadFailed && fileID !== "") ||
!userTeamPermissions.includes(pond.Permission.PERMISSION_FILE_MANAGEMENT) !hasFilePermission
} }
multiple={false} multiple={false}
name={uniqueID || "fileInput"} name={uniqueID || "fileInput"}
@ -163,7 +164,7 @@ export default function FileSelector(props: Props) {
<label <label
htmlFor={uniqueID || "fileInput"} htmlFor={uniqueID || "fileInput"}
className={ className={
userTeamPermissions.includes(pond.Permission.PERMISSION_FILE_MANAGEMENT) hasFilePermission
? classes.fileSelect ? classes.fileSelect
: classes.fileDisabled : classes.fileDisabled
}> }>

View file

@ -10,19 +10,22 @@ interface Props {
uploadEnd: (fileID?: string, fileName?: string) => void; uploadEnd: (fileID?: string, fileName?: string) => void;
keys?: string[]; keys?: string[];
types?: string[]; types?: string[];
hasFilePermission: boolean
} }
export default function FileUploader(props: Props) { export default function FileUploader(props: Props) {
const { uploadStart, uploadEnd, startingSelectorCount, toAttach, keys, types } = props; const { uploadStart, uploadEnd, startingSelectorCount, toAttach, keys, types, hasFilePermission } = props;
const [numSelectors, setNumSelectors] = useState(startingSelectorCount || 1); const [numSelectors, setNumSelectors] = useState(startingSelectorCount || 1);
const [selectors, setSelectors] = useState<JSX.Element[]>([]); const [selectors, setSelectors] = useState<JSX.Element[]>([]);
useEffect(() => { useEffect(() => {
let currentSelectors: JSX.Element[] = []; let currentSelectors: JSX.Element[] = [];
for (let i = 0; i < numSelectors; i++) { for (let i = 0; i < numSelectors; i++) {
currentSelectors.push( currentSelectors.push(
<Box key={i} margin={2}> <Box key={i} margin={2}>
<FileSelector <FileSelector
hasFilePermission={hasFilePermission}
toAttach={toAttach} toAttach={toAttach}
keys={keys} keys={keys}
types={types} types={types}

View file

@ -402,7 +402,7 @@ export default function ContractSettings(props: Props) {
borderRadius: 5, borderRadius: 5,
margin: 20 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.
</Typography> </Typography>
</Box> </Box>
<Grid <Grid

View file

@ -6,26 +6,17 @@ import {
Button, Button,
Grid2, Grid2,
IconButton, IconButton,
Paper,
Stack, Stack,
Tab, Tab,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Tabs, Tabs,
Typography Typography
} from "@mui/material"; } from "@mui/material";
import React, { useCallback, useEffect, useState } from "react"; 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 { useGlobalState, useFieldAPI, useJohnDeereProxyAPI, useCNHiProxyAPI } from "providers";
//import HarvestTable from "harvestPlan/HarvestTable"; //import HarvestTable from "harvestPlan/HarvestTable";
import { Field, fieldScope, teamScope } from "models"; import { Field } from "models";
import GrainDescriber from "grain/GrainDescriber"; import GrainDescriber from "grain/GrainDescriber";
import { pond } from "protobuf-ts/pond";
import HarvestSettings from "harvestPlan/HarvestSettings";
import ResponsiveTable, { Column } from "common/ResponsiveTable"; import ResponsiveTable, { Column } from "common/ResponsiveTable";
import FieldMinimap from "./Fieldminimap"; import FieldMinimap from "./Fieldminimap";
import FieldActions from "./FieldActions"; import FieldActions from "./FieldActions";
@ -36,12 +27,6 @@ import ShareAllFields from "./ShareAllFields";
import EventBlocker from "common/EventBlocker"; import EventBlocker from "common/EventBlocker";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
interface TabPanelProps {
children?: React.ReactNode;
index: any;
value: any;
}
export default function FieldList() { export default function FieldList() {
const [{ as }] = useGlobalState(); const [{ as }] = useGlobalState();
const isMobile = useMobile(); const isMobile = useMobile();
@ -50,7 +35,6 @@ export default function FieldList() {
const cnhAPI = useCNHiProxyAPI(); const cnhAPI = useCNHiProxyAPI();
const { openSnack } = useSnackbar(); const { openSnack } = useSnackbar();
const [tabValue, setTabValue] = React.useState(0); const [tabValue, setTabValue] = React.useState(0);
const [openHarvestSettings, setOpenHarvestSettings] = useState(false);
// const [fieldForPlan, setFieldForPlan] = useState(Field.create()); // const [fieldForPlan, setFieldForPlan] = useState(Field.create());
const [fields, setFields] = useState<Field[]>([]); const [fields, setFields] = useState<Field[]>([]);
const [total, setTotal] = useState(0) const [total, setTotal] = useState(0)
@ -235,7 +219,7 @@ const columns: Column<Field>[] = [
<Typography textAlign="center">{row.grainName()}</Typography> <Typography textAlign="center">{row.grainName()}</Typography>
<Typography textAlign="center">{row.acres()} Acres</Typography> <Typography textAlign="center">{row.acres()} Acres</Typography>
</Stack> </Stack>
<Button variant="contained" color="primary">Go</Button> <Button variant="contained" color="primary" fullWidth onClick={() => goTo(row)}>Go</Button>
</Grid2> </Grid2>
</Grid2> </Grid2>
</AccordionDetails> </AccordionDetails>

View file

@ -0,0 +1,152 @@
import { Box, Card, CardHeader, Grid2, Typography } from "@mui/material"
import { blue, orange } from "@mui/material/colors"
import SingleSetAreaChart, { SSAreaDataPoint } from "charts/SingleSetAreaChart"
import { useComponentAPI } from "hooks"
import { cloneDeep } from "lodash"
import { Component } from "models"
import { UnitMeasurement } from "models/UnitMeasurement"
import moment from "moment"
import { Moment } from "moment"
import { pond } from "protobuf-ts/pond"
import { quack } from "protobuf-ts/quack"
import { useGlobalState } from "providers"
import { useEffect, useState } from "react"
import { getTemperatureUnit } from "utils"
interface Props {
//the key for the temperature chain, this may be overhauled to only be a single node in the future for the outlet and the 'inlet' would be the ambient
deviceID: number
tempComponent: Component
ambient?: Component
start: Moment;
end: Moment;
}
export default function GateDeltaTempGraph(props: Props){
const {
tempComponent,
ambient,
deviceID,
start,
end
} = props
const [data, setData] = useState<SSAreaDataPoint[]>([])
const [{user}] = useGlobalState()
const componentAPI = useComponentAPI();
const [recent, setRecent] = useState<SSAreaDataPoint>()
const warmingColour = orange["500"]
const coolingColour = blue["500"]
/**
* the use effect will use the temp component passed in get the measurements, then use the first node and the last node to determine the difference,
* if there is only one node, or only one node is reading due to errors, then it will use that as both the inlet and outlet so the delta will be 0,
*
* //NOT IMPLEMENTED YET//
* if an ambient is passed in it will use the readings from the ambient as the inlet and the temp component as the outlet
*/
useEffect(()=>{
//get the measurements for the temp component
componentAPI.listUnitMeasurements(
deviceID,
tempComponent.key(),
start.toISOString(),
end.toISOString(),
500,
0,
"timestamp",
).then(resp => {
if(resp.data.measurements){
let tempCompMeasurements = resp.data.measurements.map(um => UnitMeasurement.any(um, user));
//if there is an ambient component, then treat the temp component like a single sensor and not a chain, and use the ambient measurements as the inlet
if(ambient){
//TODO: this is just an idea of how to handle a possible path for the overhaul of gates, if that is the route we take then this needs to be coded
}else{//else no ambient is passed in treat the temp component like a chain
let deltas: SSAreaDataPoint[] = []
tempCompMeasurements.forEach(um => {
if (um.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE){
um.values.forEach((nodes, index) => {
if (nodes.values && nodes.values.length > 0 && um.timestamps[index]){
//use the first node as the inlet
let inletNode = nodes.values[0]
//use the second node as the outlet, if there is an error or there is only one node then it will be used as both and the delta will be 0
let outletNode = nodes.values[nodes.values.length -1]
let newDelta: SSAreaDataPoint = {
value: Math.round((outletNode - inletNode)*100)/100,
timestamp: moment(um.timestamps[index]).valueOf(),
}
deltas.push(newDelta)
}
})
}
})
setData(deltas)
//use the components status to set the recent to show in the header of the card
let recent: SSAreaDataPoint | undefined
if(tempComponent.status.measurement){
tempComponent.status.measurement.forEach(um => {
if(um.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE){
let clone = cloneDeep(um)
let m = UnitMeasurement.create(clone, user)
if(m.values.length > 0){
let lastReading = m.values[m.values.length - 1]
if (lastReading.values.length > 0){
recent = {
value: lastReading.values[lastReading.values.length -1] - lastReading.values[0],
timestamp: moment(m.timestamps[m.values.length - 1]).valueOf()
}
}
}
}
})
}
setRecent(recent)
}
}
})
},[tempComponent, ambient, deviceID, start, end])
return (
<Card raised style={{ padding: 10, marginBottom: 15, marginTop: 15 }}>
<CardHeader
title={<Typography style={{ fontSize: 25, fontWeight: 650 }}>Delta Temp</Typography>}
subheader={
recent ?
(
<Grid2 container>
<Grid2 size={12}>
<span>
{"Delta Temp: "}
<span style={{ color: recent.value > 0 ? warmingColour : coolingColour, fontWeight: 500 }}>
{recent.value.toFixed(2) + (getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? " °F" : " °C")}
</span>
<br />
</span>
</Grid2>
<Grid2 size={12}>
<Typography color="textSecondary" variant={"caption"}>
{moment(recent.timestamp).fromNow()}
</Typography>
</Grid2>
</Grid2>
)
: (
<Typography variant="body1" color="textPrimary">
No Data
</Typography>
)
}
/>
<SingleSetAreaChart
data={data}
tooltipLabel="Delta Temp"
tooltipUnit={getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? " °F" : " °C"}
yAxisLabel={"Delta T" + (getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? " °F" : " °C")}
colourAboveZero={{colour: warmingColour, label: "Heating"}}
colourBelowZero={{colour: coolingColour, label: "Cooling"}}/>
</Card>
)
}

View file

@ -44,21 +44,33 @@ export default function GateDevice(props: Props) {
const [tempKey, setTempKey] = useState(""); const [tempKey, setTempKey] = useState("");
const [pressureKey, setPressureKey] = useState(""); const [pressureKey, setPressureKey] = useState("");
const [ambientKey, setAmbientKey] = useState(""); const [ambientKey, setAmbientKey] = useState("");
const [greenLightKey, setGreenLightKey] = useState("");
const [redLightKey, setRedLightKey] = useState("");
const { openSnack } = useSnackbar(); const { openSnack } = useSnackbar();
const [{ user }] = useGlobalState(); const [{ user }] = useGlobalState();
const [interactionDialog, setInteractionDialog] = useState(false); const [interactionDialog, setInteractionDialog] = useState(false);
const [densityTemp, setDensityTemp] = useState(0); const [densityTemp, setDensityTemp] = useState(0);
const isMobile = useMobile(); const isMobile = useMobile();
const [pcaState, setPCAState] = useState(false); // const [pcaState, setPCAState] = useState(false);
const [pcaFanOn, setPCAFanOn] = useState(false); // const [pcaFanOn, setPCAFanOn] = useState(false);
const [detail, setDetail] = useState<"sensors" | "analytics">("analytics"); const [detail, setDetail] = useState<"sensors" | "analytics">("analytics");
const [lastAmbient, setLastAmbient] = useState(0); const [lastAmbient, setLastAmbient] = useState(0);
const [lastTemps, setLastTemps] = useState({ t1: 0, t2: 0 }); const [lastTemps, setLastTemps] = useState({ t1: 0, t2: 0 });
const [lastPressures, setLastPressures] = useState({ p1: 0, p2: 0 }); const [lastPressures, setLastPressures] = useState({ p1: 0, p2: 0 });
useEffect(() => { useEffect(() => {
//addresses of controller LEDs on a V1 device
let redAddr = "3-1-512";
let greenAddr = "3-1-1024";
if (comprehensiveDevice.device) { if (comprehensiveDevice.device) {
setDevice(Device.any(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) { if (comprehensiveDevice.components) {
let components: Map<string, Component> = new Map<string, Component>(); let components: Map<string, Component> = new Map<string, Component>();
@ -87,19 +99,24 @@ export default function GateDevice(props: Props) {
break; break;
case pond.GateComponentType.GATE_COMPONENT_TYPE_PRESSURE: case pond.GateComponentType.GATE_COMPONENT_TYPE_PRESSURE:
setPressureKey(c.key()); setPressureKey(c.key());
if ( // if (
c.status.measurement[0] && // c.status.measurement[0] &&
c.status.measurement[0].values[0] && // c.status.measurement[0].values[0] &&
c.status.measurement[0].values[0].values[1] // c.status.measurement[0].values[0].values[1]
) { // ) {
setPCAFanOn(c.status.measurement[0].values[0].values[1] > 996); //apx 4 iwg // setPCAFanOn(c.status.measurement[0].values[0].values[1] > 996); //apx 4 iwg
} // }
break; break;
default: default:
// type is unknown, do nothing // type is unknown, do nothing
break; break;
} }
} }
if (c.locationString() === redAddr && c.subType() === 1) {
setRedLightKey(c.key());
} else if (c.locationString() === greenAddr && c.subType() === 1) {
setGreenLightKey(c.key());
}
}); });
setComponentOptions(components); setComponentOptions(components);
} }
@ -328,8 +345,8 @@ export default function GateDevice(props: Props) {
ambientSelector={ambientSelector} ambientSelector={ambientSelector}
tempChainSelector={tempSelector} tempChainSelector={tempSelector}
pressureChainSelector={pressureSelector} pressureChainSelector={pressureSelector}
pcaState={pcaState} pcaState={gate.status.pcaState}
pcaFanState={pcaFanOn} // pcaFanState={pcaFanOn}
checkInTime={device.status.lastActive} checkInTime={device.status.lastActive}
/> />
<Button <Button
@ -357,11 +374,14 @@ export default function GateDevice(props: Props) {
gate={gate} gate={gate}
display={detail} display={detail}
compMap={componentOptions} compMap={componentOptions}
setPCAState={(state: boolean) => { // setPCAState={(state: boolean) => {
setPCAState(state); // setPCAState(state);
}} // }}
ambient={ambientKey} ambient={ambientKey}
pressure={pressureKey} pressure={pressureKey}
tempChain={tempKey}
redKey={redLightKey}
greenKey={greenLightKey}
device={device.id()} device={device.id()}
/> />
</Grid> </Grid>

View file

@ -25,7 +25,7 @@ interface Props {
ambient?: string; ambient?: string;
newXDomain?: number[] | string[]; newXDomain?: number[] | string[];
pressureComponent?: string; pressureComponent?: string;
setPCAState: (state: boolean) => void; // setPCAState: (state: boolean) => void;
multiGraphZoom?: (domain: number[] | string[]) => void; multiGraphZoom?: (domain: number[] | string[]) => void;
} }
@ -55,7 +55,7 @@ export default function GateFlowGraph(props: Props) {
device, device,
ambient, ambient,
pressureComponent, pressureComponent,
setPCAState, // setPCAState,
start, start,
end, end,
newXDomain, newXDomain,
@ -131,22 +131,22 @@ export default function GateFlowGraph(props: Props) {
} }
}); });
setRuntime(moment.duration(runtime)); setRuntime(moment.duration(runtime));
let state = false; // let state = false;
if ( // if (
data.length > 1 && // data.length > 1 &&
data[data.length - 1].value > gate.lowerFlow() && // data[data.length - 1].value > gate.lowerFlow() &&
data[data.length - 1].value < gate.upperFlow() // data[data.length - 1].value < gate.upperFlow()
) { // ) {
state = true; // state = true;
} // }
setPCAState(state); // setPCAState(state);
} }
setFlowData(data); setFlowData(data);
setLoadingChartData(false); setLoadingChartData(false);
setRecent(recent); setRecent(recent);
}); });
} }
}, [gateAPI, gate, ambient, pressureComponent, start, end, device, setPCAState, as]); // eslint-disable-line react-hooks/exhaustive-deps }, [gateAPI, gate, ambient, pressureComponent, start, end, device, as]); // eslint-disable-line react-hooks/exhaustive-deps
const loadFlowEvents = () => { const loadFlowEvents = () => {
if (ambient && pressureComponent) { if (ambient && pressureComponent) {
@ -209,6 +209,8 @@ export default function GateFlowGraph(props: Props) {
{flowData.length !== 0 ? ( {flowData.length !== 0 ? (
<SingleSetAreaChart <SingleSetAreaChart
data={flowData} data={flowData}
tooltipLabel="Flow"
yAxisLabel="Mass Flow (kg/s)"
maxRef={gate.upperFlow()} maxRef={gate.upperFlow()}
minRef={gate.lowerFlow()} minRef={gate.lowerFlow()}
newXDomain={newXDomain} newXDomain={newXDomain}

View file

@ -28,15 +28,19 @@ import { avg } from "utils";
import GateFlowGraph from "./GateFlowGraph"; import GateFlowGraph from "./GateFlowGraph";
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import GateDeltaTempGraph from "./GateDeltaTempGraph";
interface Props { interface Props {
gate: Gate; gate: Gate;
display: "analytics" | "sensors"; display: "analytics" | "sensors";
compMap: Map<string, Component>; compMap: Map<string, Component>;
device: string | number; device: number;
pressure: string; pressure: string;
ambient: string; ambient: string;
setPCAState: (state: boolean) => void; tempChain: string;
redKey: string;
greenKey: string;
// setPCAState: (state: boolean) => void;
} }
const useStyles = makeStyles((theme: Theme) => ({ const useStyles = makeStyles((theme: Theme) => ({
@ -60,7 +64,7 @@ const useStyles = makeStyles((theme: Theme) => ({
); );
export default function GateGraphs(props: Props) { 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 } = props;
const [{ as }] = useGlobalState(); const [{ as }] = useGlobalState();
const [xDomain, setXDomain] = useState<string[] | number[]>(["dataMin", "dataMax"]); const [xDomain, setXDomain] = useState<string[] | number[]>(["dataMin", "dataMax"]);
const [zoomed, setZoomed] = useState(false); const [zoomed, setZoomed] = useState(false);
@ -270,44 +274,77 @@ export default function GateGraphs(props: Props) {
); );
}; };
const sensorGraphs = () => { const graphCard = (component: Component, unitMeasurements: UnitMeasurement[]) => {
return ( return (
<Box> <Card raised key={component.key()} style={{ padding: 10, marginBottom: 15 }}>
{Array.from(compMeasurements.values()).map((compMeasurement, i) => { {graphHeader(component)}
let c = Component.create(); {unitMeasurements.map(um => {
if (compMeasurement[0]) {
c = compMap.get(compMeasurement[0].componentId) ?? Component.create();
}
if (compMap.get(c.key())) {
return (
<Card raised key={i} style={{ padding: 10, marginBottom: 15 }}>
{graphHeader(c)}
{compMeasurement.map(um => {
if (um.values[0] && um.values[0].values.length > 1) { if (um.values[0] && um.values[0].values.length > 1) {
return areaGraph( return areaGraph(
um, um,
describeMeasurement(um.type, c.type(), c.subType()), describeMeasurement(um.type, component.type(), component.subType()),
c.settings.smoothingAverages component.settings.smoothingAverages
); );
} else { } else {
return lineGraph( return lineGraph(
um, um,
describeMeasurement(um.type, c.type(), c.subType()), describeMeasurement(um.type, component.type(), component.subType()),
c.settings.smoothingAverages component.settings.smoothingAverages
); );
} }
})} })}
</Card> </Card>
); );
} else {
return <Box></Box>;
} }
})}
</Box> /**
); * This function creates the charts for the gate page using the MAIN components on the gate, and delta time, it DOES NOT show any other components that could be on the device
}; * @returns list of cards for the charts
*/
const sensorGraphs = () => {
const tempComp = compMap.get(tempChain)
let graphs: JSX.Element[] = []
//pressure
let pressureComp = compMap.get(pressure)
let pressureReadings = compMeasurements.get(pressure)
if(pressureComp && pressureReadings){
graphs.push(graphCard(pressureComp, pressureReadings))
}
if(tempComp){
graphs.push(
<GateDeltaTempGraph deviceID={device} start={startDate} end={endDate} tempComponent={tempComp}/>
)
}
//T/H chain
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 = () => { const analyticGraphs = () => {
let tempComp = compMap.get(tempChain)
return ( return (
<Box> <Box>
<GateFlowGraph <GateFlowGraph
@ -318,14 +355,18 @@ export default function GateGraphs(props: Props) {
gate={gate} gate={gate}
ambient={ambient} ambient={ambient}
pressureComponent={pressure} pressureComponent={pressure}
setPCAState={(state: boolean) => { // setPCAState={(state: boolean) => {
setPCAState(state); // setPCAState(state);
}} // }}
multiGraphZoom={domain => { multiGraphZoom={domain => {
setXDomain(domain); setXDomain(domain);
setZoomed(true); setZoomed(true);
}} }}
/> />
{/* add a new chart here for the delta temp over time */}
{tempComp &&
<GateDeltaTempGraph deviceID={device} start={startDate} end={endDate} tempComponent={tempComp}/>
}
</Box> </Box>
); );
}; };

View file

@ -15,6 +15,11 @@ import { CheckCircleOutline, DoNotDisturb, ErrorOutline, HelpOutlineOutlined, Se
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import moment from "moment"; import moment from "moment";
import { pond } from "protobuf-ts/pond"; 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";
import { react } from "@babel/types";
interface Props { interface Props {
//gates: Gate[]; //gates: Gate[];
@ -47,6 +52,7 @@ export default function GateList(props: Props) {
const navigate = useNavigate(); const navigate = useNavigate();
const isMobile = useMobile(); const isMobile = useMobile();
const classes = useStyles(); const classes = useStyles();
const [{user}] = useGlobalState()
const [gateDialog, setGateDialog] = useState(false); const [gateDialog, setGateDialog] = useState(false);
const [tablePage, setTablePage] = useState(0) const [tablePage, setTablePage] = useState(0)
const [pageSize, setPageSize] = useState(10) const [pageSize, setPageSize] = useState(10)
@ -104,6 +110,95 @@ 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
// }
// })
// if(isMobile){
// return (
// <React.Fragment>
// <Typography color={color}>{outletDisplay}</Typography>
// <Typography>{timeSince}</Typography>
// </React.Fragment>
// )
// }
// return (
// <Box padding={2}>
// <Box display="flex" justifyContent="space-between">
// <Typography>Value:</Typography>
// <Typography color={color}>{outletDisplay}</Typography>
// </Box>
// <Box display="flex" justifyContent="space-between">
// <Typography>Time:</Typography>
// <Typography>{timeSince}</Typography>
// </Box>
// </Box>
// )
// }
// const tempDisplay = (gate: 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 (
// <Typography>
// {display}
// </Typography>
// )
// }
const conditionDisplay = (gate: Gate) => {
console.log(gate)
let display = ""
let deltaTemp = 0
if(gate.status.pcaState === pond.PCAState.PCA_STATE_OFF){
display = "Inactive"
} else if (gate.status.pcaState === pond.PCAState.PCA_STATE_UNKNOWN){
display = "--"
} else { //the pca is currently active calulate the delta temp (T2 - T1) provided there are two temps
//loop to find the temp readings
let clone = cloneDeep(gate.status.lastTempReading)
clone.forEach(unitMeasurement => {
let um = UnitMeasurement.create(unitMeasurement, user)
if(um.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE){
if(um.values.length > 0){ //as long as there is at least on thing in the measurements
let lastReading = um.values[um.values.length-1] //there should only be one measurement in here but just make sure to get the end of the array
if(lastReading.values.length > 1){ //make sure there are at least two values in the array
console.log(lastReading.values)
deltaTemp = lastReading.values[lastReading.values.length -1] - lastReading.values[0] //subtract the first value from the last value to get the delta
}
}
}
})
display = deltaTemp.toFixed(2) + (getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "°F" : "°C")
}
return (
<Typography>
{display}
</Typography>
)
}
// const CtoF = (celsius: number) => {
// return Math.round((celsius * (9 / 5) + 32) * 100) / 100;
// };
const desktopCols = (): Column<Gate>[] => { const desktopCols = (): Column<Gate>[] => {
return [ return [
{ {
@ -126,69 +221,61 @@ export default function GateList(props: Props) {
</Box> </Box>
) )
}, },
{
title: "Duct Type",
render: gate => (
<Box padding={2}>
<Typography>
{gate.settings.ductName}
</Typography>
</Box>
)
},
{
title: "Duct Size(mm)",
render: gate => (
<Box padding={2}>
<Typography>
{gate.ductDiameter()}
</Typography>
</Box>
)
},
{
title: "Duct Length(m)",
render: gate => (
<Box padding={2}>
<Typography>
{gate.settings.ductLength}
</Typography>
</Box>
)
},
{
title: "PCA Unit",
render: gate => (
<Box padding={2}>
<Typography>
{gate.settings.pcaType}
</Typography>
</Box>
)
},
{ {
title: "PCA Status", title: "PCA Status",
render: gate => ( render: gate => {
<Box padding={2}> return (<Box padding={2}>
{displayPCAStatus(gate.status.pcaState)} {displayPCAStatus(gate.status.pcaState)}
</Box> </Box>)
) }
}, },
{ {
title: "Last FLow", title: "Delta Temperature",
render: gate => ( render: gate => {
return (
<Box padding={2}> <Box padding={2}>
<Box display="flex" justifyContent="space-between"> {conditionDisplay(gate)}
<Typography>Last Flow:</Typography>
<Typography>{gate.status.lastMassAirflow.toFixed(2)}</Typography>
</Box> </Box>
<Box display="flex" justifyContent="space-between"> )}
<Typography>Last Reading:</Typography> },
<Typography>{gate.status.lastUpdate !== "" ? moment(gate.status.lastUpdate).fromNow() : "No Reading Yet"}</Typography> //taking out these columns for now because were not sure if the customer actually wants them
</Box> // {
</Box> // title: "Estimated Temp",
) // render: gate => {
} // return (
// <Box padding={2}>
// {tempDisplay(gate)}
// </Box>
// )
// }
// },
// {
// title: "Last FLow",
// render: gate => (
// <Box padding={2}>
// <Box display="flex" justifyContent="space-between">
// <Typography>Flow:</Typography>
// <Typography color={teal[500]}>{gate.status.lastMassAirflow.toFixed(2)} kg/s</Typography>
// </Box>
// <Box display="flex" justifyContent="space-between">
// <Typography>Read:</Typography>
// <Typography>{gate.status.lastUpdate !== "" ? moment(gate.status.lastUpdate).fromNow() : "No Reading Yet"}</Typography>
// </Box>
// </Box>
// )
// },
// {
// title: "Outlet Temperature",
// render: gate => (
// <Box>{lastOutletReading(gate.status.lastTempReading)}</Box>
// )
// },
// {
// title: "Outlet Pressure",
// render: gate => (
// <Box>{lastOutletReading(gate.status.lastPressureReading)}</Box>
// )
// }
] ]
} }
const mobileCols = (): Column<Gate>[] => { const mobileCols = (): Column<Gate>[] => {
@ -224,33 +311,28 @@ export default function GateList(props: Props) {
<Typography>{terminalMap.get(gate.terminal()) ?? "None"}</Typography> <Typography>{terminalMap.get(gate.terminal()) ?? "None"}</Typography>
</Box> </Box>
<Box padding={2} display="flex" flexDirection="row" justifyContent="space-between"> <Box padding={2} display="flex" flexDirection="row" justifyContent="space-between">
<Typography>Duct Type:</Typography> <Typography>Delta Temperature:</Typography>
<Typography>{gate.ductName() !== "" ? gate.ductName() : "None"}</Typography> {conditionDisplay(gate)}
</Box> </Box>
<Box padding={2} display="flex" flexDirection="row" justifyContent="space-between"> {/* taking these out for now because we are not sure if the customer wants them */}
<Typography>Duct Size(mm):</Typography> {/* <Box padding={2} display="flex" flexDirection="row" justifyContent="space-between">
<Typography>{gate.ductDiameter()}</Typography> <Typography>Estimated Temp:</Typography>
</Box> {tempDisplay(gate)}
<Box padding={2} display="flex" flexDirection="row" justifyContent="space-between">
<Typography>DuctLength(m):</Typography>
<Typography>{gate.ductLength()}</Typography>
</Box>
<Box padding={2} display="flex" flexDirection="row" justifyContent="space-between">
<Typography>PCA Unit:</Typography>
<Typography>{gate.settings.pcaType !== "" ? gate.settings.pcaType : "None"}</Typography>
</Box> </Box>
<Box padding={2} display="flex" flexDirection="row" justifyContent="space-between"> <Box padding={2} display="flex" flexDirection="row" justifyContent="space-between">
<Typography>Last Flow:</Typography> <Typography>Last Flow:</Typography>
<Typography>{gate.status.lastMassAirflow}</Typography> <Typography color={teal[500]}>{gate.status.lastMassAirflow.toFixed(2)} kg/s</Typography>
</Box>
<Box padding={2} display="flex" flexDirection="row" justifyContent="space-between">
<Typography>Last Reading:</Typography>
<Typography>{gate.status.lastUpdate !== "" ? moment(gate.status.lastUpdate).fromNow() : "No PCA reading yet"}</Typography> <Typography>{gate.status.lastUpdate !== "" ? moment(gate.status.lastUpdate).fromNow() : "No PCA reading yet"}</Typography>
</Box> </Box>
<Box padding={2} display="flex" flexDirection="row" justifyContent="space-between">
<Typography>Outlet Temp:</Typography>
{lastOutletReading(gate.status.lastTempReading)}
</Box>
<Box padding={2} display="flex" flexDirection="row" justifyContent="space-between">
<Typography>Outlet Pressure:</Typography>
{lastOutletReading(gate.status.lastPressureReading)}
</Box> */}
</Box> </Box>
// <Typography variant="body2" color="textSecondary" sx={{ padding: 1 }}>
// </Typography>
) )
} }

View file

@ -48,8 +48,8 @@ interface Props {
ambientSelector: () => JSX.Element; ambientSelector: () => JSX.Element;
tempChainSelector: () => JSX.Element; tempChainSelector: () => JSX.Element;
pressureChainSelector: () => JSX.Element; pressureChainSelector: () => JSX.Element;
pcaState: boolean; pcaState: pond.PCAState;
pcaFanState: boolean; // pcaFanState: boolean;
checkInTime: string; checkInTime: string;
} }
@ -63,7 +63,7 @@ export default function GateSVG(props: Props) {
tempChainSelector, tempChainSelector,
pressureChainSelector, pressureChainSelector,
pcaState, pcaState,
pcaFanState, // pcaFanState,
checkInTime checkInTime
} = props; } = props;
const classes = useStyles(); const classes = useStyles();
@ -242,16 +242,23 @@ export default function GateSVG(props: Props) {
return temp; return temp;
}; };
const finalTempDisplay = (temp: number) => {
let display = "--"
if(pcaState === pond.PCAState.PCA_STATE_IN_BOUNDS || pcaState === pond.PCAState.PCA_STATE_OUT_BOUNDS){
if(temp < 60 && temp > -40 && !isNaN(temp) && temp !== 0){
display = convertFinalTemp(finalTemp).toFixed(2) + (user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "°F" : "°C")
}
}
return display
}
const renderValues = () => { const renderValues = () => {
let values: JSX.Element[] = []; let values: JSX.Element[] = [];
//add the final temp display to the array //add the final temp display to the array
values.push( values.push(
<g key={"finalTemp"} id={"finalTemp"} data-name={"finalTemp"}> <g key={"finalTemp"} id={"finalTemp"} data-name={"finalTemp"}>
<text fontSize={7} className={classes.fontBase} x={100} y={85}> <text fontSize={7} className={classes.fontBase} x={100} y={85}>
{convertFinalTemp(finalTemp).toFixed(2)} {finalTempDisplay(finalTemp)}
{user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT
? "°F"
: "°C"}
</text> </text>
</g> </g>
); );
@ -302,7 +309,8 @@ export default function GateSVG(props: Props) {
const pcaRed = () => { const pcaRed = () => {
return ( return (
<g key={"pcaRed"} id={"pcaRed"} data-name={"pcaRed"}> <g key={"pcaRed"} id={"pcaRed"} data-name={"pcaRed"}>
<circle cx={175} cy={152} r={8} fill={pcaFanState ? (pcaState ? "grey" : "red") : "grey"} /> {/* <circle cx={175} cy={152} r={8} fill={(pcaFanState && pcaState === pond.PCAState.PCA_STATE_OUT_BOUNDS) ? "red" : "grey"} /> */}
<circle cx={175} cy={152} r={8} fill={pcaState === pond.PCAState.PCA_STATE_OUT_BOUNDS ? "red" : "grey"} />
</g> </g>
); );
}; };
@ -314,7 +322,8 @@ export default function GateSVG(props: Props) {
cx={175} cx={175}
cy={174} cy={174}
r={8} r={8}
fill={pcaFanState ? (pcaState ? "green" : "grey") : "grey"} //fill={(pcaFanState && pcaState === pond.PCAState.PCA_STATE_IN_BOUNDS) ? "green" : "grey"}
fill={pcaState === pond.PCAState.PCA_STATE_IN_BOUNDS ? "green" : "grey"}
/> />
</g> </g>
); );
@ -324,7 +333,8 @@ export default function GateSVG(props: Props) {
return ( return (
<g key={"fanStatus"}> <g key={"fanStatus"}>
<text x={125} y={180} fontSize={5} className={classes.fontBase}> <text x={125} y={180} fontSize={5} className={classes.fontBase}>
PCA Fan: {pcaFanState ? "ON" : "OFF"} {/* PCA Fan: {pcaFanState ? "ON" : "OFF"} */}
PCA Fan: {pcaState === pond.PCAState.PCA_STATE_OFF ? "OFF" : "ON"}
</text> </text>
</g> </g>
); );

View file

@ -14,8 +14,8 @@ import {
import FileUploader from "common/FileUploads/FileUploader"; import FileUploader from "common/FileUploads/FileUploader";
import ResponsiveDialog from "common/ResponsiveDialog"; import ResponsiveDialog from "common/ResponsiveDialog";
import SearchSelect, { Option } from "common/SearchSelect"; import SearchSelect, { Option } from "common/SearchSelect";
import { useMobile } from "hooks"; import { useMobile, useUserAPI } from "hooks";
import { Bin, Field, Contract, GrainBag } from "models"; import { Bin, Field, Contract, GrainBag, teamScope } from "models";
// import { Contract } from "models/Contract"; // import { Contract } from "models/Contract";
// import { GrainBag } from "models/GrainBag"; // import { GrainBag } from "models/GrainBag";
import moment from "moment"; import moment from "moment";
@ -66,7 +66,7 @@ export default function GrainTransaction(props: Props) {
const binAPI = useBinAPI(); const binAPI = useBinAPI();
const grainBagAPI = useGrainBagAPI(); const grainBagAPI = useGrainBagAPI();
const fieldAPI = useFieldAPI(); const fieldAPI = useFieldAPI();
const [{ as }] = useGlobalState(); const [{ as, user }] = useGlobalState();
const { openSnack } = useSnackbar(); const { openSnack } = useSnackbar();
const [grainChangeDialog, setGrainChangeDialog] = useState(false); const [grainChangeDialog, setGrainChangeDialog] = useState(false);
const [grainEntry, setGrainEntry] = useState("0"); const [grainEntry, setGrainEntry] = useState("0");
@ -79,6 +79,8 @@ export default function GrainTransaction(props: Props) {
const [fieldsLoading, setFieldsLoading] = useState(false); const [fieldsLoading, setFieldsLoading] = useState(false);
const [uploadingFile, setUploadingFile] = useState(false); const [uploadingFile, setUploadingFile] = useState(false);
const [imageIDs, setImageIDs] = useState<string[]>([]); const [imageIDs, setImageIDs] = useState<string[]>([]);
const userAPI = useUserAPI();
const [filePermission, setFilePermission] = useState(false);
const closeDialogs = (confirmed: boolean, newTransaction?: pond.Transaction) => { const closeDialogs = (confirmed: boolean, newTransaction?: pond.Transaction) => {
close(); close();
@ -91,6 +93,20 @@ export default function GrainTransaction(props: Props) {
} }
}; };
useEffect(()=>{
//if they are viewing as a team make sure they have file permission to the team for the transactions
if(as){
let scope = teamScope(as)
userAPI.getUser(user.id(), scope).then(resp => {
setFilePermission(resp.permissions.includes(pond.Permission.PERMISSION_FILE_MANAGEMENT))
});
}else{
//if they are not viewing as a team and they were able to open this window, they have edit permission to at least the one object they started the change with
//a transaction is a new object that permissions will not exist for yet so just allow them to do it
setFilePermission(true)
}
},[as])
//get the possible destinations/options (grainbags and bins) //get the possible destinations/options (grainbags and bins)
useEffect(() => { useEffect(() => {
if (open) { if (open) {
@ -527,6 +543,7 @@ export default function GrainTransaction(props: Props) {
<Box marginTop={1}> <Box marginTop={1}>
{allowAttachmentUploads && ( {allowAttachmentUploads && (
<FileUploader <FileUploader
hasFilePermission={filePermission}
uploadEnd={fileID => { uploadEnd={fileID => {
if (fileID) { if (fileID) {
let ids = imageIDs; let ids = imageIDs;

View file

@ -161,6 +161,7 @@ export default function HarvestPlanActions(props: Props) {
update={updatePlan} update={updatePlan}
open={openState.settings} open={openState.settings}
plan={plan.key() !== "" ? plan : undefined} plan={plan.key() !== "" ? plan : undefined}
permissions={permissions}
close={(refresh, updatedPlan) => { close={(refresh, updatedPlan) => {
setOpenState({ ...openState, settings: false }); setOpenState({ ...openState, settings: false });
if (refresh) { if (refresh) {

View file

@ -58,25 +58,25 @@ export default function HarvestPlanDisplay(props: Props) {
<Grid container direction="column"> <Grid container direction="column">
<Grid className={classes.dark}> <Grid className={classes.dark}>
<Box style={{ display: "flex", justifyContent: "space-between" }}> <Box style={{ display: "flex", justifyContent: "space-between" }}>
<Typography variant="subtitle1">Break Even Yield:</Typography> <Typography variant="subtitle1">Break Even Yield (acre):</Typography>
<Typography variant="subtitle1"> <Typography variant="subtitle1">
{( {plan.bushelPrice() !== 0 ? (
(plan.totalEquipmentCost() + plan.totalMaterialCost()) / (plan.totalEquipmentCost() + plan.totalMaterialCost()) /
plan.bushelPrice() plan.bushelPrice()
).toFixed(2)}{" "} ).toFixed(2) : 0}{" "}
BU BU
</Typography> </Typography>
</Box> </Box>
</Grid> </Grid>
<Grid className={classes.light}> <Grid className={classes.light}>
<Box style={{ display: "flex", justifyContent: "space-between" }}> <Box style={{ display: "flex", justifyContent: "space-between" }}>
<Typography variant="subtitle1">Break Even Sales Price</Typography> <Typography variant="subtitle1">Break Even Sales Price (BU):</Typography>
<Typography variant="subtitle1"> <Typography variant="subtitle1">
$ $
{( {plan.yieldTarget() !== 0 ? (
(plan.totalEquipmentCost() + plan.totalMaterialCost()) / (plan.totalEquipmentCost() + plan.totalMaterialCost()) /
plan.yieldTarget() plan.yieldTarget()
).toFixed(2)} ).toFixed(2) : 0}
</Typography> </Typography>
</Box> </Box>
</Grid> </Grid>

View file

@ -11,6 +11,7 @@ import { getThemeType } from "theme"
interface Props { interface Props {
field: Field field: Field
planSelected: (plan: HarvestPlan) => void
} }
const useStyles = makeStyles((theme: Theme) => ({ const useStyles = makeStyles((theme: Theme) => ({
@ -25,7 +26,7 @@ const useStyles = makeStyles((theme: Theme) => ({
})); }));
export default function HarvestPlanTable(props: Props){ export default function HarvestPlanTable(props: Props){
const {field} = props const {field, planSelected} = props
const harvestAPI = useHarvestPlanAPI() const harvestAPI = useHarvestPlanAPI()
const [page, setPage] = useState(0) const [page, setPage] = useState(0)
const [pageSize, setPageSize] = useState(10) const [pageSize, setPageSize] = useState(10)
@ -114,6 +115,9 @@ export default function HarvestPlanTable(props: Props){
title={"Harvest Plans"} title={"Harvest Plans"}
resizeable resizeable
page={page} page={page}
onRowClick={(row) => {
planSelected(row)
}}
pageSize={pageSize} pageSize={pageSize}
rows={loadedPlans} rows={loadedPlans}
total={total} total={total}

View file

@ -35,6 +35,7 @@ interface Props {
field: Field; field: Field;
update?: boolean; update?: boolean;
plan?: HarvestPlan; plan?: HarvestPlan;
permissions?: pond.Permission[]
} }
const cropOptions = [ const cropOptions = [
@ -160,7 +161,7 @@ const useStyles = makeStyles((theme: Theme) => ({
const steps = ["Pre-Seeding", "Seeding", "Post-Seeding", "Harvest", "Fall"]; const steps = ["Pre-Seeding", "Seeding", "Post-Seeding", "Harvest", "Fall"];
export default function HarvestSettings(props: Props) { 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 [{as}] = useGlobalState()
const [planKey, setPlanKey] = useState<string>(); const [planKey, setPlanKey] = useState<string>();
const [newPlan, setNewPlan] = useState(HarvestPlan.create()); const [newPlan, setNewPlan] = useState(HarvestPlan.create());
@ -177,6 +178,7 @@ export default function HarvestSettings(props: Props) {
const [variety, setVariety] = useState(""); const [variety, setVariety] = useState("");
const [harvestYear, setHarvestYear] = useState(new Date().getFullYear()); const [harvestYear, setHarvestYear] = useState(new Date().getFullYear());
const [targetYield, setTargetYield] = useState(0); const [targetYield, setTargetYield] = useState(0);
const [actualYield, setActualYield] = useState(0);
const [price, setPrice] = useState(""); const [price, setPrice] = useState("");
const [newTaskDialog, setNewTaskDialog] = useState(false); const [newTaskDialog, setNewTaskDialog] = useState(false);
const [type, setType] = useState(""); const [type, setType] = useState("");
@ -184,10 +186,16 @@ export default function HarvestSettings(props: Props) {
const { openSnack } = useSnackbar(); const { openSnack } = useSnackbar();
const [deleteOpen, setDeleteOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false);
const loadTasks = useCallback(() => { const loadTasks = useCallback(() => {
if (planKey) { if (planKey) {
taskAPI.getMultiTasks([planKey], as).then(resp => { taskAPI.getMultiTasks([planKey], as).then(resp => {
if(resp.data.tasks){
setPlanTasks(resp.data.tasks.map(t => Task.any(t))); setPlanTasks(resp.data.tasks.map(t => Task.any(t)));
}else{
setPlanTasks([])
}
}); });
} }
}, [taskAPI, planKey, as]); }, [taskAPI, planKey, as]);
@ -197,7 +205,7 @@ export default function HarvestSettings(props: Props) {
}, [loadTasks]); }, [loadTasks]);
useEffect(() => { useEffect(() => {
if (!plan || (plan && plan.permissions.includes(pond.Permission.PERMISSION_WRITE))) { if (!plan || (permissions && permissions.includes(pond.Permission.PERMISSION_WRITE))) {
setActiveStep(0); setActiveStep(0);
} else { } else {
setActiveStep(1); setActiveStep(1);
@ -358,6 +366,7 @@ export default function HarvestSettings(props: Props) {
tempPlan.settings.grainType = variety; tempPlan.settings.grainType = variety;
tempPlan.settings.harvestYear = harvestYear; tempPlan.settings.harvestYear = harvestYear;
tempPlan.settings.yieldTarget = targetYield; tempPlan.settings.yieldTarget = targetYield;
tempPlan.settings.actualYield =
tempPlan.settings.bushelPrice = !isNaN(parseFloat(price)) tempPlan.settings.bushelPrice = !isNaN(parseFloat(price))
? Math.round(parseFloat(price) * 100) / 100 ? Math.round(parseFloat(price) * 100) / 100
: 0; : 0;
@ -411,7 +420,7 @@ export default function HarvestSettings(props: Props) {
key={0} key={0}
classes={{ root: classes.tab }} classes={{ root: classes.tab }}
disabled={ disabled={
!planKey || !(plan && plan.permissions.includes(pond.Permission.PERMISSION_WRITE)) !planKey || !(permissions && permissions.includes(pond.Permission.PERMISSION_WRITE))
} }
label={"General"} label={"General"}
aria-label={"general"} aria-label={"general"}
@ -494,6 +503,16 @@ export default function HarvestSettings(props: Props) {
!isNaN(+e.target.value) && setTargetYield(+e.target.value); !isNaN(+e.target.value) && setTargetYield(+e.target.value);
}} }}
/> />
<TextField
fullWidth
margin="normal"
type="number"
label="Actual Yield per acre"
value={actualYield}
onChange={e => {
!isNaN(+e.target.value) && setActualYield(+e.target.value);
}}
/>
<TextField <TextField
margin="dense" margin="dense"
id="bushPrice" id="bushPrice"
@ -710,7 +729,7 @@ export default function HarvestSettings(props: Props) {
<TaskSettings <TaskSettings
task={taskToEdit} task={taskToEdit}
hasCost hasCost
costTitle="Material Cost(acres)" costTitle="Material Cost(acre)"
secondaryCostTitle="Equipment Cost(acre)" secondaryCostTitle="Equipment Cost(acre)"
objectKey={planKey} objectKey={planKey}
type={type} type={type}

View file

@ -134,7 +134,7 @@ export default function FieldDrawer(props: Props) {
hPlanAPI hPlanAPI
.listHarvestPlans(1, 0, "desc", "createDate", field.key(), as) .listHarvestPlans(1, 0, "desc", "createDate", field.key(), as)
.then(resp => { .then(resp => {
if (resp.data.harvestPlan.length > 0) { if (resp.data.harvestPlan) {
let plan = resp.data.harvestPlan[0]; let plan = resp.data.harvestPlan[0];
setHPlan(HarvestPlan.any(plan)); setHPlan(HarvestPlan.any(plan));
} else { } else {

View file

@ -40,6 +40,7 @@ export default function Contracts() {
.then(resp => { .then(resp => {
let contracts: Contract[] = []; let contracts: Contract[] = [];
let contractPermissions: Map<string, pond.Permission[]> = new Map(); let contractPermissions: Map<string, pond.Permission[]> = new Map();
if (resp.data.contracts){
resp.data.contracts.forEach(contract => { resp.data.contracts.forEach(contract => {
let c = Contract.create(contract); let c = Contract.create(contract);
contracts.push(c); contracts.push(c);
@ -50,6 +51,7 @@ export default function Contracts() {
}); });
setContracts(contracts); setContracts(contracts);
setContractPermissions(contractPermissions); setContractPermissions(contractPermissions);
}
setLoading(false); setLoading(false);
}) })
.catch(err => {}); .catch(err => {});

View file

@ -227,7 +227,7 @@ export default function FieldPage() {
</Grid2> </Grid2>
<Grid2 size={8}> <Grid2 size={8}>
<Card raised> <Card raised>
<HarvestPlanTable field={field}/> <HarvestPlanTable field={field} planSelected={(plan) => {setHPlan(plan)}}/>
</Card> </Card>
</Grid2> </Grid2>
</Grid2> </Grid2>
@ -276,7 +276,7 @@ export default function FieldPage() {
</Card> </Card>
</Grid2> </Grid2>
<Grid2> <Grid2>
<HarvestPlanTable field={field}/> <HarvestPlanTable field={field} planSelected={(plan) => {setHPlan(plan)}}/>
</Grid2> </Grid2>
</Grid2> </Grid2>
</TabPanel> </TabPanel>

View file

@ -134,7 +134,6 @@ export default function Gate(props: Props) {
gateAPI gateAPI
.getGatePageData(id, as) .getGatePageData(id, as)
.then(resp => { .then(resp => {
//console.log(resp.data);
let p = new Map<number, pond.GateDeviceType>(); let p = new Map<number, pond.GateDeviceType>();
Object.keys(resp.data.preferences).forEach(k => { Object.keys(resp.data.preferences).forEach(k => {
let prefKey = parseInt(k); let prefKey = parseInt(k);
@ -192,7 +191,9 @@ export default function Gate(props: Props) {
const deviceDrawer = () => { const deviceDrawer = () => {
return ( return (
<DeviceLinkDrawer <DeviceLinkDrawer
deviceTags={["omniair", "mipca"]} //the mipca tag was not working for some reason so just taking the tags out
//and this way we dont have to worry about tagging them when they are provisioned
//deviceTags={["omniair", "mipca", "MiPCA", "Mipca"]}
devicePrefMap={devPrefs} devicePrefMap={devPrefs}
prefOptions={[ prefOptions={[
<MenuItem <MenuItem
@ -341,15 +342,13 @@ export default function Gate(props: Props) {
spacing={2} spacing={2}
alignItems="center" alignItems="center"
alignContent="center"> alignContent="center">
<Grid> <Grid size={12}>
<Box display="flex" justifyContent="center" alignItems="center"> <Box display="flex" justifyContent="center" alignItems="center">
<Link style={{ height: 50, width: 50 }} /> <Link style={{ height: 50, width: 50 }} />
</Box> <Typography paddingLeft={2} style={{ fontSize: 25, fontWeight: 650 }}>
</Grid>
<Grid>
<Typography style={{ fontSize: 25, fontWeight: 650 }}>
Connect Device Connect Device
</Typography> </Typography>
</Box>
</Grid> </Grid>
<Grid size={12}> <Grid size={12}>
Click here to add a device to the gate. To add an additional device to a gate Click here to add a device to the gate. To add an additional device to a gate

View file

@ -9,9 +9,21 @@ const useStyles = makeStyles((theme: Theme) => ({
width: "100%", width: "100%",
overflowX: "hidden", overflowX: "hidden",
overflowY: "auto", overflowY: "auto",
// height: `calc(100vh - 112px)`,
// [theme.breakpoints.up("sm")]: {
// height: `calc(100vh - 64px)`
// }
minHeight: 0,
height: `calc(100vh - 112px)`, height: `calc(100vh - 112px)`,
"@supports (height: 100dvh)": {
height: `calc(100dvh - 112px)`
},
[theme.breakpoints.up("sm")]: { [theme.breakpoints.up("sm")]: {
height: `calc(100vh - 64px)` height: `calc(100vh - 64px)`,
"@supports (height: 100dvh)": {
height: `calc(100dvh - 64px)`
}
} }
}, },
fullViewportContainer: { fullViewportContainer: {
@ -19,6 +31,9 @@ const useStyles = makeStyles((theme: Theme) => ({
top: 0, top: 0,
left: 0, left: 0,
height: "100vh", height: "100vh",
"@supports (height: 100dvh)": {
height: "100dvh"
},
width: "100vw", width: "100vw",
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
zIndex: 2000, zIndex: 2000,

View file

@ -33,13 +33,13 @@ export function Airflow(subtype: number = 0): ComponentTypeExtension {
let measurementTypes = [ let measurementTypes = [
{ {
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PPM, measurementType: quack.MeasurementType.MEASUREMENT_TYPE_CFM,
label: cfm.label(), label: cfm.label(),
colour: cfm.colour(), colour: cfm.colour(),
graphType: cfm.graph() graphType: cfm.graph()
} as ComponentMeasurement, } as ComponentMeasurement,
{ {
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_VOLTAGE, measurementType: quack.MeasurementType.MEASUREMENT_TYPE_SPEED,
label: velocity.label(), label: velocity.label(),
colour: velocity.colour(), colour: velocity.colour(),
graphType: velocity.graph() graphType: velocity.graph()

View file

@ -453,7 +453,7 @@ export class MeasurementDescriber {
case quack.MeasurementType.MEASUREMENT_TYPE_SPEED: case quack.MeasurementType.MEASUREMENT_TYPE_SPEED:
let speedUnit = getDistanceUnit() let speedUnit = getDistanceUnit()
this.details.label = "Speed"; 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.colour = green["500"];
this.details.path = "edgeTriggered.rises"; this.details.path = "edgeTriggered.rises";
this.details.max = 500; this.details.max = 500;

View file

@ -323,7 +323,8 @@ const whitelabels = new Map<string, WhiteLabel>([
["mivent", MIVENT_WHITE_LABEL], ["mivent", MIVENT_WHITE_LABEL],
["adaptiveconstruction", ADAPTIVE_CONSTRUCTION_WHITE_LABEL], ["adaptiveconstruction", ADAPTIVE_CONSTRUCTION_WHITE_LABEL],
["omniair", OMNIAIR_WHITE_LABEL], ["omniair", OMNIAIR_WHITE_LABEL],
["mipca", MIPCA_WHITE_LABEL] ["mipca", MIPCA_WHITE_LABEL],
["mionetech", MIPCA_WHITE_LABEL]
]); ]);
export function getWhitelabel(): WhiteLabel { export function getWhitelabel(): WhiteLabel {

View file

@ -20,7 +20,7 @@ import Edit from "@mui/icons-material/Edit";
import DeleteIcon from "@mui/icons-material/Delete"; import DeleteIcon from "@mui/icons-material/Delete";
import { red } from "@mui/material/colors"; import { red } from "@mui/material/colors";
import { pond } from "protobuf-ts/pond"; import { pond } from "protobuf-ts/pond";
import { taskScope } from "models/Scope"; import { taskScope, teamScope } from "models/Scope";
import EventBlocker from "common/EventBlocker"; import EventBlocker from "common/EventBlocker";
interface Props { interface Props {
@ -58,19 +58,22 @@ export default function TaskCard(props: Props) {
const [day, setDay] = useState(0); const [day, setDay] = useState(0);
const classes = useStyles(); const classes = useStyles();
const [permissions, setPermissions] = useState<pond.Permission[]>([]); const [permissions, setPermissions] = useState<pond.Permission[]>([]);
const [{ user }] = useGlobalState(); const [{ user, as }] = useGlobalState();
const userAPI = useUserAPI(); const userAPI = useUserAPI();
const [menuAnchorEl, setMenuAnchorEl] = useState<Element | null>(null); const [menuAnchorEl, setMenuAnchorEl] = useState<Element | null>(null);
const loadPermissions = useCallback(() => { const loadPermissions = useCallback(() => {
let scope = taskScope(props.task.key); 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 userAPI
.getUser(user.id(), scope) .getUser(user.id(), scope)
.then(resp => { .then(resp => {
setPermissions(resp.permissions); setPermissions(resp.permissions);
}) })
.catch(err => {}); .catch(err => {});
}, [props.task, userAPI, user]); }, [props.task, userAPI, user, as]);
useEffect(() => { useEffect(() => {
loadPermissions(); loadPermissions();
@ -176,3 +179,4 @@ export default function TaskCard(props: Props) {
</Card> </Card>
); );
} }