563 lines
No EOL
21 KiB
TypeScript
563 lines
No EOL
21 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import PageContainer from "./PageContainer";
|
|
import { DevicePreset } from "models/DevicePreset";
|
|
import { Controller } from "models/Controller";
|
|
import { useNavigate, useParams } from "react-router-dom";
|
|
import { useMobile, useSnackbar } from "hooks";
|
|
import { Component, Device, Bin as IBin, Interaction } from "models";
|
|
import { useBinAPI, useGlobalState } from "providers";
|
|
import { pond } from "protobuf-ts/pond";
|
|
import { Plenum } from "models/Plenum";
|
|
import { Ambient } from "models/Ambient";
|
|
import { GrainCable } from "models/GrainCable";
|
|
import { Pressure } from "models/Pressure";
|
|
import { CO2 } from "models/CO2";
|
|
import moment from "moment";
|
|
import { quack } from "protobuf-ts/quack";
|
|
import { Box, Drawer, Grid2 as Grid, IconButton, ListItemIcon, ListItemText, Menu, MenuItem, Tab, Tabs, Theme, Tooltip } from "@mui/material";
|
|
import { makeStyles } from "@mui/styles";
|
|
import NotesIcon from "@mui/icons-material/Notes";
|
|
import TasksIcon from "products/Construction/TasksIcon";
|
|
import BindaptIcon from "products/Bindapt/BindaptIcon";
|
|
import FieldsIcon from "products/AgIcons/FieldsIcon";
|
|
import BinActions from "bin/BinActions";
|
|
import BinSummary from "bin/binSummary/BinSummary";
|
|
import { Close } from "@mui/icons-material";
|
|
import React from "react";
|
|
import BinHistory from "bin/BinHistory";
|
|
import Chat from "chat/Chat";
|
|
import TaskViewer from "tasks/TaskViewer";
|
|
|
|
interface TabPanelProps {
|
|
children?: React.ReactNode;
|
|
index: any;
|
|
value: any;
|
|
}
|
|
|
|
function TabPanelMine(props: TabPanelProps) {
|
|
const { children, value, index, ...other } = props;
|
|
|
|
return (
|
|
<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 themeType = theme.palette.mode;
|
|
return ({
|
|
spacer: {
|
|
width: "32px",
|
|
height: "32px",
|
|
padding: "auto",
|
|
display: "flex",
|
|
justifyContent: "center",
|
|
alignItems: "center"
|
|
},
|
|
avatar: {
|
|
color: themeType === "light" ? theme.palette.common.black : theme.palette.common.white,
|
|
backgroundColor: "transparent",
|
|
width: theme.spacing(5),
|
|
height: theme.spacing(5),
|
|
border: "1px solid",
|
|
borderColor: theme.palette.divider
|
|
},
|
|
menuPaper: {
|
|
borderRadius: "20px"
|
|
},
|
|
drawerPaperDesktop: {
|
|
height: "100%",
|
|
width: "30%"
|
|
},
|
|
drawerPaperMobile: {
|
|
height: "50%",
|
|
width: "100%"
|
|
},
|
|
})
|
|
})
|
|
|
|
|
|
export default function BinV2(){
|
|
const warningThreshold = 6;
|
|
const isMobile = useMobile();
|
|
const binID = useParams<{ binID: string }>()?.binID ?? "";
|
|
const binAPI = useBinAPI();
|
|
const classes = useStyles()
|
|
const navigate = useNavigate()
|
|
const [{ user, as, showErrors }] = useGlobalState();
|
|
const [binLoading, setBinLoading] = useState(false);
|
|
const loadRef = useRef<boolean>(false);
|
|
const [bin, setBin] = useState<IBin>(IBin.create());
|
|
const [devices, setDevices] = useState<Device[]>([]);
|
|
const [components, setComponents] = useState<Map<string, Component>>(new Map());
|
|
const [interactions, setInteractions] = useState<Interaction[]>([]);
|
|
const [permissions, setPermissions] = useState<pond.Permission[]>([]);
|
|
const [preferences, setPreferences] = useState<Map<string, pond.BinComponentPreferences>>();
|
|
const [plenums, setPlenums] = useState<Plenum[]>([]);
|
|
const [ambients, setAmbients] = useState<Ambient[]>([]);
|
|
const [grainCables, setGrainCables] = useState<GrainCable[]>([]);
|
|
const [pressures, setPressures] = useState<Pressure[]>([]);
|
|
const [headspaceCO2, setHeadspaceCO2] = useState<CO2[]>([]);
|
|
const [heaters, setHeaters] = useState<Controller[]>([]);
|
|
const {openSnack} = useSnackbar()
|
|
const [fans, setFans] = useState<Controller[]>([]);
|
|
const [compositionNameMap, setCompositionNameMap] = useState<Map<string, string>>(new Map());
|
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
|
const [noteTab, setNoteTab] = useState(0)
|
|
|
|
|
|
const [componentDevices, setComponentDevices] = useState<Map<string, number>>(
|
|
new Map<string, number>()
|
|
);
|
|
const [interactionDevices, setInteractionDevices] = useState<Map<string, number>>(
|
|
new Map<string, number>()
|
|
);
|
|
const [binPresets, setBinPresets] = useState<DevicePreset[]>([]);
|
|
const [missedReadings, setMissedReadings] = useState(0);
|
|
|
|
//Drawer states
|
|
const [noteDrawerOpen, setNoteDrawerOpen] = useState(false)
|
|
const [taskDrawerOpen, setTaskDrawerOpen] = useState(false)
|
|
|
|
//DATA LOAD/UPDATE FUNCTIONS
|
|
|
|
const load = useCallback(() => {
|
|
if (loadRef.current || user.id() === "") return;
|
|
setBinLoading(true);
|
|
loadRef.current = true;
|
|
//add the presets to the bin page data load
|
|
binAPI
|
|
.getBinPageData(binID, user.id(), showErrors, as)
|
|
.then(resp => {
|
|
if (resp.data.grainCompositionNames) {
|
|
let tempMap: Map<string, string> = new Map();
|
|
Object.keys(resp.data.grainCompositionNames).forEach(key => {
|
|
tempMap.set(key, resp.data.grainCompositionNames[key]);
|
|
});
|
|
tempMap.set("correction", "Unknown Source");
|
|
setCompositionNameMap(tempMap);
|
|
}
|
|
|
|
let devs: Device[] = [];
|
|
let p = new Map<string, pond.BinComponentPreferences>();
|
|
Object.keys(resp.data.preferences).forEach(k => {
|
|
let prefKey = k.split(":")[1] ? k.split(":")[1] : k;
|
|
p.set(prefKey, pond.BinComponentPreferences.fromObject(resp.data.preferences[k]));
|
|
});
|
|
setPreferences(p);
|
|
let r: pond.GetBinPageDataResponse = pond.GetBinPageDataResponse.fromObject(resp.data);
|
|
setBin(IBin.any(resp.data.bin));
|
|
let newComponentDevices = new Map<string, number>();
|
|
let attachedDevIds: number[] = [];
|
|
if (resp.data.componentDevices)
|
|
Object.keys(resp.data.componentDevices).forEach(k => {
|
|
newComponentDevices.set(k, resp.data.componentDevices[k]);
|
|
//make a list of all of the ids for devices that components are currently attached
|
|
if (!attachedDevIds.includes(resp.data.componentDevices[k])) {
|
|
attachedDevIds.push(resp.data.componentDevices[k]);
|
|
}
|
|
});
|
|
|
|
setComponentDevices(newComponentDevices);
|
|
|
|
let newInteractionDevices = new Map<string, number>();
|
|
if (r.interactionDevices)
|
|
Object.keys(r.interactionDevices).forEach(k => {
|
|
newInteractionDevices.set(k, r.interactionDevices[k]);
|
|
});
|
|
setInteractionDevices(newInteractionDevices);
|
|
|
|
if (resp.data.interactions) {
|
|
setInteractions(resp.data.interactions.map((i: pond.Interaction) => Interaction.any(i)));
|
|
} else {
|
|
setInteractions([]);
|
|
}
|
|
//go through the devices and if the device has an attached component include it in the device list
|
|
if (resp.data.devices && resp.data.devices[0]) {
|
|
resp.data.devices.forEach((dev: any) => {
|
|
let device = Device.create(dev);
|
|
if (attachedDevIds.includes(device.id())) {
|
|
devs.push(device);
|
|
}
|
|
});
|
|
setDevices(devs);
|
|
}
|
|
if (resp.data.components) {
|
|
let compMap: Map<string, Component> = new Map();
|
|
resp.data.components.forEach((comp: pond.Component) => {
|
|
if (comp && comp.settings) {
|
|
let c = Component.any(comp);
|
|
compMap.set(comp.settings.key, c);
|
|
}
|
|
});
|
|
setComponents(compMap);
|
|
}
|
|
if (resp.data.presets) {
|
|
setBinPresets(
|
|
resp.data.presets.map((preset: pond.DevicePreset) => DevicePreset.create(preset))
|
|
);
|
|
}
|
|
setPermissions(r.permissions ? r.permissions : []);
|
|
setBinLoading(false);
|
|
loadRef.current = false;
|
|
})
|
|
.finally(() => {
|
|
setBinLoading(false);
|
|
loadRef.current = false;
|
|
});
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [binID, user.id(), binAPI, showErrors, as]);
|
|
|
|
useEffect(() => {
|
|
load();
|
|
}, [load]);
|
|
|
|
const setBinComponents = useCallback(() => {
|
|
if (!preferences) return;
|
|
var unassigned: Component[] = [];
|
|
var plenums: Plenum[] = [];
|
|
var ambients: Ambient[] = [];
|
|
var grainCables: GrainCable[] = [];
|
|
var fans: Controller[] = [];
|
|
var heaters: Controller[] = [];
|
|
var pressures: Pressure[] = [];
|
|
var headspaceCo2: CO2[] = [];
|
|
var mostMissed: number = 0;
|
|
let now = moment();
|
|
|
|
components.forEach(comp => {
|
|
let pref = preferences.get(comp.key());
|
|
if (pref) {
|
|
if (pref.type) {
|
|
if (pref.type === pond.BinComponent.BIN_COMPONENT_PLENUM)
|
|
plenums.push(Plenum.create(comp));
|
|
if (pref.type === pond.BinComponent.BIN_COMPONENT_AMBIENT)
|
|
ambients.push(Ambient.create(comp));
|
|
if (pref.type === pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE) {
|
|
//check cable for missed measurements
|
|
let cable = GrainCable.create(comp);
|
|
let lastRead = moment(cable.lastReading);
|
|
let elapsedMS = now.diff(lastRead);
|
|
if (elapsedMS > cable.settings.measurementPeriodMs) {
|
|
let missedReadings = Math.floor(elapsedMS / cable.settings.measurementPeriodMs);
|
|
if (missedReadings > mostMissed) {
|
|
mostMissed = missedReadings;
|
|
}
|
|
}
|
|
grainCables.push(cable);
|
|
}
|
|
if (pref.type === pond.BinComponent.BIN_COMPONENT_HEATER)
|
|
heaters.push(Controller.create(comp));
|
|
if (pref.type === pond.BinComponent.BIN_COMPONENT_FAN) fans.push(Controller.create(comp));
|
|
if (pref.type === pond.BinComponent.BIN_COMPONENT_PRESSURE)
|
|
pressures.push(Pressure.create(comp));
|
|
if (pref.type === pond.BinComponent.BIN_COMPONENT_HEADSPACE) {
|
|
if (comp.type() === quack.ComponentType.COMPONENT_TYPE_CO2) {
|
|
//check if there are missed readings from the co2 and compare them to what we already had
|
|
let coComp = CO2.create(comp)
|
|
let lastRead = moment(coComp.lastReading);
|
|
let elapsedMS = now.diff(lastRead);
|
|
if (elapsedMS > coComp.settings.measurementPeriodMs) {
|
|
let missedReadings = Math.floor(elapsedMS / coComp.settings.measurementPeriodMs);
|
|
if (missedReadings > mostMissed) {
|
|
mostMissed = missedReadings;
|
|
}
|
|
}
|
|
headspaceCo2.push(coComp);
|
|
}
|
|
}
|
|
} else {
|
|
unassigned.push(comp);
|
|
}
|
|
}
|
|
});
|
|
setMissedReadings(mostMissed);
|
|
setPlenums(plenums);
|
|
setGrainCables(grainCables);
|
|
setPressures(pressures);
|
|
setAmbients(ambients);
|
|
setHeaters(heaters);
|
|
setFans(fans);
|
|
setHeadspaceCO2(headspaceCo2);
|
|
}, [components, preferences]);
|
|
|
|
useEffect(() => {
|
|
setBinComponents();
|
|
}, [setBinComponents]);
|
|
|
|
const updateStatus = (componentKeys: string[], removed?: boolean) => {
|
|
componentKeys.forEach(compKey => {
|
|
let comp = components.get(compKey);
|
|
if (comp) {
|
|
//determine what part of the status to update based on the component
|
|
//for lidar
|
|
if (comp.type() === quack.ComponentType.COMPONENT_TYPE_LIDAR) {
|
|
//determine how to update the status based on if added or removed
|
|
if (removed) {
|
|
bin.status.distance = 0;
|
|
} else {
|
|
if (
|
|
comp.lastMeasurement[0] &&
|
|
comp.lastMeasurement[0].values[0] &&
|
|
comp.lastMeasurement[0].values[0].values[0]
|
|
) {
|
|
bin.status.distance = comp.lastMeasurement[0].values[0].values[0];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
//update the bins status with the new values based on the components changed
|
|
binAPI.updateBinStatus(bin.key(), bin.status, as);
|
|
};
|
|
|
|
//NAVIGATION FUNCTIONS
|
|
const goToDevice = (dev: Device) => {
|
|
navigate(window.location.pathname + "/devices/" + dev.id(), { replace: true });
|
|
};
|
|
|
|
const goToYard = (bin: IBin) => {
|
|
navigate("/visualFarm", { state: {
|
|
long: bin.getLocation()?.longitude,
|
|
lat: bin.getLocation()?.latitude
|
|
}
|
|
});
|
|
};
|
|
|
|
|
|
/**
|
|
* UI STUFF STARTS HERE
|
|
*/
|
|
|
|
const deviceMenu = () => {
|
|
return (
|
|
<Menu
|
|
id="groupMenu"
|
|
anchorEl={anchorEl}
|
|
open={Boolean(anchorEl)}
|
|
onClose={() => setAnchorEl(null)}
|
|
keepMounted
|
|
classes={{ paper: classes.menuPaper }}
|
|
disableAutoFocusItem>
|
|
{devices.map(dev => (
|
|
<MenuItem key={dev.id()} onClick={() => goToDevice(dev)}>
|
|
<ListItemIcon>
|
|
<BindaptIcon />
|
|
</ListItemIcon>
|
|
<ListItemText primary={dev.name()} />
|
|
</MenuItem>
|
|
))}
|
|
</Menu>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Header - consists of the actions and things that are always visible like
|
|
* opening the note/history and tasks drawers, navigating to the device and map pages, opening binsensors and settings etc.
|
|
* also has the bin name/model and activity and the dropdown for changing sections
|
|
*/
|
|
const pageHeader = () => {
|
|
return (
|
|
<Box margin={2} marginBottom={0}>
|
|
<Grid
|
|
container
|
|
direction="row"
|
|
justifyContent="space-between"
|
|
alignContent="center"
|
|
alignItems="center">
|
|
<Grid >
|
|
<IconButton
|
|
onClick={() =>
|
|
setNoteDrawerOpen(true)
|
|
}
|
|
size="small"
|
|
style={{
|
|
marginRight: "0.5rem"
|
|
}}
|
|
className={classes.avatar}
|
|
component="span">
|
|
<div className={classes.spacer}>
|
|
<NotesIcon />
|
|
</div>
|
|
</IconButton>
|
|
<IconButton
|
|
onClick={() =>
|
|
setTaskDrawerOpen(true)
|
|
}
|
|
size="small"
|
|
style={{
|
|
marginRight: "0.5rem"
|
|
}}
|
|
className={classes.avatar}
|
|
component="span">
|
|
<div className={classes.spacer}>
|
|
<TasksIcon />
|
|
</div>
|
|
</IconButton>
|
|
{devices.length > 1 ? (
|
|
<Tooltip key="devices" title="devices">
|
|
<IconButton
|
|
key="devButton"
|
|
onClick={(event: React.MouseEvent<HTMLButtonElement>) =>
|
|
setAnchorEl(event.currentTarget)
|
|
}
|
|
size="small"
|
|
style={{
|
|
marginRight: "0.5rem"
|
|
}}
|
|
className={classes.avatar}
|
|
component="span">
|
|
<BindaptIcon />
|
|
</IconButton>
|
|
</Tooltip>
|
|
) : (
|
|
devices.map(dev => (
|
|
<Tooltip key={dev.id()} title={dev.name()}>
|
|
<IconButton
|
|
key={dev.id()}
|
|
onClick={() => goToDevice(dev)}
|
|
size="small"
|
|
style={{
|
|
marginRight: "0.5rem"
|
|
}}
|
|
className={classes.avatar}
|
|
component="span">
|
|
<div className={classes.spacer}>
|
|
<BindaptIcon />
|
|
</div>
|
|
</IconButton>
|
|
</Tooltip>
|
|
))
|
|
)}
|
|
{bin.binMapped() && (
|
|
<Tooltip key={"goToMap"} title={"Go to Yard"}>
|
|
<IconButton
|
|
onClick={() => {
|
|
goToYard(bin);
|
|
}}
|
|
size="small"
|
|
style={{
|
|
marginRight: "0.5rem"
|
|
}}
|
|
className={classes.avatar}
|
|
component="span">
|
|
<FieldsIcon />
|
|
</IconButton>
|
|
</Tooltip>
|
|
)}
|
|
</Grid>
|
|
<Grid>
|
|
<BinActions
|
|
bin={bin}
|
|
permissions={permissions}
|
|
refreshCallback={() => {
|
|
load();
|
|
}}
|
|
userID={user.id()}
|
|
components={components}
|
|
setComponents={setComponents}
|
|
componentDevices={componentDevices}
|
|
setComponentDevices={setComponentDevices}
|
|
updateBinStatus={updateStatus}
|
|
/>
|
|
</Grid>
|
|
</Grid>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
const handleChange = (_event: React.ChangeEvent<{}>, newValue: number) => {
|
|
setNoteTab(newValue);
|
|
};
|
|
|
|
|
|
//DRAWERS
|
|
const noteDrawer = () => {
|
|
return (
|
|
<Drawer
|
|
open={noteDrawerOpen}
|
|
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 = () => {
|
|
return (
|
|
<Drawer
|
|
style={{ zIndex: 1099 }}
|
|
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>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<PageContainer>
|
|
{deviceMenu()}
|
|
{pageHeader()}
|
|
<BinSummary
|
|
bin={bin}
|
|
devices={devices}
|
|
fans={fans}
|
|
heaters={heaters}
|
|
cables={grainCables}
|
|
plenums={plenums}
|
|
permissions={permissions}
|
|
componentDevices={componentDevices}
|
|
componentMap={components}
|
|
binPrefs={preferences}
|
|
setPreferences={setPreferences}
|
|
updateBinCallback={(bin) => {
|
|
setBin(bin)
|
|
binAPI.updateBin(bin.key(), bin.settings).then(resp => {
|
|
openSnack("Bin has Been Updated")
|
|
})
|
|
}}
|
|
setBin={setBin}
|
|
/>
|
|
{/* render drawers */}
|
|
{noteDrawer()}
|
|
{taskDrawer()}
|
|
</PageContainer>
|
|
)
|
|
} |