frontend/src/pages/BinV2.tsx

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