1162 lines
38 KiB
TypeScript
1162 lines
38 KiB
TypeScript
import {
|
|
Box,
|
|
IconButton,
|
|
Theme,
|
|
Grid2 as Grid,
|
|
DialogTitle,
|
|
DialogContent,
|
|
DialogContentText,
|
|
DialogActions,
|
|
Button,
|
|
Tooltip,
|
|
Drawer,
|
|
Menu,
|
|
MenuItem,
|
|
ListItemIcon,
|
|
ListItemText,
|
|
Tab,
|
|
Tabs,
|
|
Card,
|
|
Accordion,
|
|
AccordionSummary,
|
|
AccordionDetails,
|
|
darken,
|
|
Typography,
|
|
ToggleButton,
|
|
ToggleButtonGroup
|
|
} from "@mui/material";
|
|
import BinActions from "bin/BinActions";
|
|
import BinHistory from "bin/BinHistory";
|
|
import { useComponentAPI, useMobile } from "hooks";
|
|
import { Bin as IBin, binScope } from "models";
|
|
import { pond, quack } from "protobuf-ts/pond";
|
|
import { useBinAPI, useGlobalState } from "providers";
|
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
import PageContainer from "./PageContainer";
|
|
import { Component, Device, Interaction } from "models";
|
|
import BinLightIcon from "assets/products/bindapt/binLight.png";
|
|
import BinDarkIcon from "assets/products/bindapt/binDark.png";
|
|
import NotesIcon from "@mui/icons-material/Notes";
|
|
import ResponsiveDialog from "common/ResponsiveDialog";
|
|
import ObjectTeams from "teams/ObjectTeams";
|
|
import BindaptIcon from "products/Bindapt/BindaptIcon";
|
|
import Close from "@mui/icons-material/Close";
|
|
import Chat from "chat/Chat";
|
|
import TasksIcon from "products/Construction/TasksIcon";
|
|
// import TaskViewer from "tasks/TaskViewer";
|
|
import FieldsIcon from "products/AgIcons/FieldsIcon";
|
|
import BinComponentTypes from "bin/BinComponentTypes";
|
|
import { Plenum } from "models/Plenum";
|
|
import { GrainCable } from "models/GrainCable";
|
|
// import { Controller } from "models/Controller";
|
|
import { Pressure } from "models/Pressure";
|
|
import { CheckCircle, ExpandMore, Warning } from "@mui/icons-material";
|
|
import { getThemeType } from "theme";
|
|
import BinGraphs from "bin/graphs/BinGraphs";
|
|
import BinTour from "bin/BinTour";
|
|
import { getBinModel } from "common/DataImports/BinCables/BinCableImporter";
|
|
import ObjectAlerts from "objects/ObjectAlerts";
|
|
import DevicePresetController from "device/presets/devicePresetController";
|
|
import { DevicePreset } from "models/DevicePreset";
|
|
import BinVisualizerV2 from "bin/BinVisualizerV2";
|
|
import { Ambient } from "models/Ambient";
|
|
import moment from "moment";
|
|
import BinStorageConditions from "bin/BinStorageConditions";
|
|
import BinConditioningCard from "bin/BinConditioningCard";
|
|
import { makeStyles, styled } from "@mui/styles";
|
|
import { useNavigate, useParams } from "react-router-dom";
|
|
import { Controller } from "models/Controller";
|
|
|
|
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;
|
|
const activeBG = theme.palette.secondary.main;
|
|
return ({
|
|
cardContent: {
|
|
padding: theme.spacing(1),
|
|
height: "100%",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
flexGrow: 1
|
|
},
|
|
boxContent: {
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
flexGrow: 2
|
|
},
|
|
img: {
|
|
height: "28px",
|
|
width: "auto !important",
|
|
margin: "auto"
|
|
},
|
|
spacer: {
|
|
width: "32px",
|
|
height: "32px",
|
|
padding: "auto",
|
|
display: "flex",
|
|
justifyContent: "center",
|
|
alignItems: "center"
|
|
},
|
|
icon: {
|
|
border: "1px solid grey",
|
|
marginBottom: "0.75rem",
|
|
marginRight: "0.5rem"
|
|
},
|
|
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
|
|
},
|
|
active: {
|
|
color: theme.palette.getContrastText(activeBG),
|
|
backgroundColor: "gold",
|
|
width: theme.spacing(5),
|
|
height: theme.spacing(5),
|
|
border: 0,
|
|
"&:hover": {
|
|
backgroundColor: "gold"
|
|
}
|
|
},
|
|
customIcon: {
|
|
padding: theme.spacing(0.5)
|
|
},
|
|
drawerPaper: {
|
|
height: "100%",
|
|
width: "30%"
|
|
},
|
|
menuPaper: {
|
|
borderRadius: "20px"
|
|
},
|
|
tab: {}
|
|
});
|
|
});
|
|
|
|
interface Props {
|
|
binKey?: string;
|
|
displayMobile?: boolean;
|
|
fromMap?: boolean;
|
|
}
|
|
|
|
export default function Bin(props: Props) {
|
|
const { binKey, displayMobile, fromMap } = props;
|
|
const warningThreshold = 6;
|
|
const isMobile = useMobile();
|
|
const classes = useStyles();
|
|
// const match = useRouteMatch<MatchParams>();
|
|
// const binID = binKey ?? match.params.binID;
|
|
const binID = useParams<{ binID: string }>()?.binID ?? "";
|
|
const binAPI = useBinAPI();
|
|
const [{ user }] = useGlobalState();
|
|
const [binLoading, setBinLoading] = useState(false);
|
|
const loadRef = useRef<boolean>(false);
|
|
const [bin, setBin] = useState<IBin>(IBin.create());
|
|
const [tab, setTab] = useState<String>("Bin");
|
|
const [value, setValue] = React.useState(0);
|
|
const [devices, setDevices] = useState<Device[]>([]);
|
|
const [components, setComponents] = useState<Map<string, Component>>(new Map());
|
|
const [interactions, setInteractions] = useState<Interaction[]>([]);
|
|
const [plenumError, setPlenumError] = useState<boolean>(false);
|
|
const [teamDialog, setTeamDialog] = useState<boolean>(false);
|
|
// const history = useHistory();
|
|
const navigate = useNavigate()
|
|
const [noteDrawer, setNoteDrawer] = useState(false);
|
|
const [taskDrawer, setTaskDrawer] = useState(false);
|
|
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
|
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<Component[]>([]);
|
|
const [heaters, setHeaters] = useState<Controller[]>([]);
|
|
const [fans, setFans] = useState<Controller[]>([]);
|
|
const [detail, setDetail] = useState<
|
|
"inventory" | "sensors" | "analytics" | "presets" | "alerts"
|
|
>("inventory");
|
|
const [mobileTab, setMobileTab] = useState(0);
|
|
const componentAPI = useComponentAPI();
|
|
const [{ showErrors }] = useGlobalState();
|
|
const [compositionNameMap, setCompositionNameMap] = useState<Map<string, string>>(new Map());
|
|
|
|
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 [missedCableReadings, setMissedCableReadings] = useState(0);
|
|
|
|
const handleChange = (_event: React.ChangeEvent<{}>, newValue: number) => {
|
|
setValue(newValue);
|
|
};
|
|
|
|
const StyledToggle = styled(ToggleButton)(() => ({
|
|
root: {
|
|
backgroundColor: "transparent",
|
|
overflow: "visible",
|
|
content: "content-box",
|
|
"&$selected": {
|
|
backgroundColor: "gold",
|
|
color: "black",
|
|
borderRadius: 24,
|
|
fontWeight: "bold"
|
|
},
|
|
"&$selected:hover": {
|
|
backgroundColor: "rgb(255, 255, 0)",
|
|
color: "black",
|
|
borderRadius: 24
|
|
}
|
|
},
|
|
selected: {}
|
|
}))
|
|
|
|
const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }: { theme: Theme }) => ({
|
|
grouped: {
|
|
margin: theme.spacing(-0.5),
|
|
border: "none",
|
|
padding: theme.spacing(1),
|
|
"&:not(:first-child):not(:last-child)": {
|
|
borderRadius: 24,
|
|
marginRight: theme.spacing(0.5),
|
|
marginLeft: theme.spacing(0.5)
|
|
},
|
|
"&:first-child": {
|
|
borderRadius: 24,
|
|
marginLeft: theme.spacing(0.25)
|
|
},
|
|
"&:last-child": {
|
|
borderRadius: 24,
|
|
marginRight: theme.spacing(0.25)
|
|
}
|
|
},
|
|
root: {
|
|
backgroundColor: darken(
|
|
theme.palette.background.paper,
|
|
getThemeType() === "light" ? 0.05 : 0.25
|
|
),
|
|
borderRadius: 24,
|
|
content: "border-box"
|
|
}
|
|
}))
|
|
|
|
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)
|
|
.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.create(dev));
|
|
}
|
|
});
|
|
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]);
|
|
|
|
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: Component[] = [];
|
|
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) {
|
|
headspaceCo2.push(comp);
|
|
}
|
|
}
|
|
} else {
|
|
unassigned.push(comp);
|
|
}
|
|
}
|
|
});
|
|
setMissedCableReadings(mostMissed);
|
|
setPlenums(plenums);
|
|
setGrainCables(grainCables);
|
|
setPressures(pressures);
|
|
setAmbients(ambients);
|
|
setHeaters(heaters);
|
|
setFans(fans);
|
|
setHeadspaceCO2(headspaceCo2);
|
|
}, [components, preferences]);
|
|
|
|
const reLoadSingleComponent = (componentKey: string) => {
|
|
let dev = componentDevices.get(componentKey);
|
|
if (dev) {
|
|
componentAPI.get(dev, componentKey).then(resp => {
|
|
let compMap = components;
|
|
let comp = Component.any(resp.data);
|
|
compMap.set(comp.key(), comp);
|
|
setComponents(compMap);
|
|
setBinComponents();
|
|
});
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
setBinComponents();
|
|
}, [setBinComponents]);
|
|
|
|
const showPlenumError = () => {
|
|
return (
|
|
<ResponsiveDialog open={plenumError} onClose={() => setPlenumError(false)}>
|
|
<DialogContent>
|
|
<DialogTitle>No Plenum Sensor Detected</DialogTitle>
|
|
<DialogContentText>
|
|
Cooldown and drying mode both require a plenum sensor measuring the temperature and
|
|
humidity of the air entering the bin. If you would like to change device interactions
|
|
manually, you will have to do so in the device page.
|
|
</DialogContentText>
|
|
<DialogActions>
|
|
<Button onClick={() => setPlenumError(false)} color="primary">
|
|
Close
|
|
</Button>
|
|
</DialogActions>
|
|
</DialogContent>
|
|
</ResponsiveDialog>
|
|
);
|
|
};
|
|
|
|
// const changeBinMode = (newBinMode: pond.BinMode) => {
|
|
// const fallback = bin.settings.mode;
|
|
// let updatedBin = bin;
|
|
// let hasPlenum = false;
|
|
|
|
// // If we aren't doing storage mode, make sure it has a plenum
|
|
// hasPlenum = plenums.length > 0;
|
|
|
|
// if (hasPlenum || newBinMode === pond.BinMode.BIN_MODE_STORAGE) {
|
|
// updatedBin.settings.mode = newBinMode;
|
|
// binAPI
|
|
// .updateBin(bin.key(), updatedBin.settings)
|
|
// .then(() => {
|
|
// openSnack("Bin Mode was successfully updated");
|
|
// })
|
|
// .catch(() => {
|
|
// updatedBin.settings.mode = fallback;
|
|
// openSnack("Error occurred while changing Bin Mode");
|
|
// })
|
|
// .finally(() => {
|
|
// refresh();
|
|
// });
|
|
// } else {
|
|
// updatedBin.settings.mode = pond.BinMode.BIN_MODE_STORAGE;
|
|
// setPlenumError(true);
|
|
// return false;
|
|
// }
|
|
// return true;
|
|
// };
|
|
|
|
const refresh = (_showSnack?: boolean) => {
|
|
// binAPI.getBin(bin.key()).then(resp => {
|
|
// let bin = IBin.any(resp.data);
|
|
// setBin(bin);
|
|
// if (showSnack) {
|
|
// openSnack("Bin data refreshed");
|
|
// }
|
|
// });
|
|
load();
|
|
};
|
|
|
|
const switchToTab = (to: String) => {
|
|
setTab(to);
|
|
};
|
|
|
|
const showBin = () => {
|
|
if ((isMobile || displayMobile) && tab !== "Bin") {
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
const showNotes = () => {
|
|
if ((isMobile || displayMobile) && tab !== "Notes") {
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
const showTasks = () => {
|
|
if ((isMobile || displayMobile) && tab !== "Tasks") {
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
const notesAndHistory = (drawer?: boolean) => {
|
|
if (showNotes()) {
|
|
return (
|
|
<React.Fragment>
|
|
<Tabs
|
|
value={value}
|
|
indicatorColor="primary"
|
|
textColor="primary"
|
|
onChange={handleChange}
|
|
aria-label="disabled tabs example"
|
|
centered={!(isMobile || displayMobile)}
|
|
style={{
|
|
marginLeft: isMobile || displayMobile || drawer ? "auto" : "none"
|
|
//marginTop: isMobile || displayMobile || drawer ? "-3rem" : "0.25rem"
|
|
}}>
|
|
<Tab label="Notes" />
|
|
<Tab label="History" />
|
|
</Tabs>
|
|
<TabPanelMine value={value} index={0}>
|
|
<Box height={isMobile || displayMobile ? "80vh" : "90vh"} padding={2}>
|
|
<Chat objectKey={binID} type={pond.NoteType.NOTE_TYPE_BIN} />
|
|
</Box>
|
|
</TabPanelMine>
|
|
<TabPanelMine value={value} index={1}>
|
|
<BinHistory drawer={drawer} binID={binID} />
|
|
</TabPanelMine>
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const tasks = () => {
|
|
if (showTasks()) {
|
|
return (
|
|
<React.Fragment>
|
|
{/* <TaskViewer drawerView objectKey={binID} loadKeys={[binID]} /> */}
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const goToDevice = (dev: Device) => {
|
|
if (fromMap) {
|
|
navigate("/bins/" + binKey + "/devices/" + dev.id(), { replace: true });
|
|
} else {
|
|
navigate(window.location.pathname + "/devices/" + dev.id(), { replace: true });
|
|
}
|
|
};
|
|
|
|
const goToYard = (bin: IBin) => {
|
|
navigate("/visualFarm", { state: {
|
|
long: bin.getLocation()?.longitude,
|
|
lat: bin.getLocation()?.latitude
|
|
}
|
|
});
|
|
};
|
|
|
|
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>
|
|
);
|
|
};
|
|
|
|
const tabs = () => {
|
|
return (
|
|
<Box marginTop={1} marginBottom={1}>
|
|
<Grid
|
|
container
|
|
direction="row"
|
|
justifyContent="space-between"
|
|
alignContent="center"
|
|
alignItems="center">
|
|
<Grid >
|
|
<IconButton
|
|
onClick={() => switchToTab("Bin")}
|
|
size="small"
|
|
style={{
|
|
marginRight: "0.5rem"
|
|
}}
|
|
component="span"
|
|
className={tab === "Bin" ? classes.active : classes.avatar}>
|
|
<div className={classes.spacer}>
|
|
<img
|
|
className={classes.img}
|
|
src={tab !== "Bin" ? BinLightIcon : BinDarkIcon}
|
|
alt="icon"
|
|
/>
|
|
</div>
|
|
</IconButton>
|
|
<IconButton
|
|
onClick={() =>
|
|
isMobile || displayMobile ? switchToTab("Notes") : setNoteDrawer(true)
|
|
}
|
|
size="small"
|
|
style={{
|
|
marginRight: "0.5rem"
|
|
}}
|
|
className={tab === "Notes" ? classes.active : classes.avatar}
|
|
component="span">
|
|
<div className={classes.spacer}>
|
|
<NotesIcon />
|
|
</div>
|
|
</IconButton>
|
|
<IconButton
|
|
onClick={() =>
|
|
isMobile || displayMobile ? switchToTab("Tasks") : setTaskDrawer(true)
|
|
}
|
|
size="small"
|
|
style={{
|
|
marginRight: "0.5rem"
|
|
}}
|
|
className={tab === "Tasks" ? classes.active : classes.avatar}
|
|
component="span">
|
|
<div className={classes.spacer}>
|
|
<TasksIcon type={tab === "Tasks" ? "dark" : "light"} />
|
|
</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() && !fromMap && (
|
|
<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>
|
|
{tab === "Bin" && (
|
|
<Grid>
|
|
<BinActions
|
|
bin={bin}
|
|
permissions={permissions}
|
|
refreshCallback={() => {
|
|
load();
|
|
}}
|
|
userID={user.id()}
|
|
components={components}
|
|
setComponents={setComponents}
|
|
updateBinStatus={updateStatus}
|
|
/>
|
|
</Grid>
|
|
)}
|
|
</Grid>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const overview = () => {
|
|
return (
|
|
<BinVisualizerV2
|
|
plenum={plenums[0]}
|
|
pressure={pressures[0]}
|
|
ambient={ambients[0]}
|
|
cables={grainCables}
|
|
bin={bin}
|
|
loading={binLoading}
|
|
components={components}
|
|
componentDevices={componentDevices}
|
|
devices={devices}
|
|
interactions={interactions}
|
|
//changeBinMode={changeBinMode}
|
|
afterUpdate={refresh}
|
|
refresh={refresh}
|
|
preferences={preferences}
|
|
permissions={permissions}
|
|
updateComponentCallback={componentKey => {
|
|
reLoadSingleComponent(componentKey);
|
|
}}
|
|
binPresets={binPresets}
|
|
/>
|
|
);
|
|
};
|
|
|
|
const blank = () => {
|
|
return true;
|
|
};
|
|
|
|
const objectTeams = () => {
|
|
let scope = binScope(binID);
|
|
return (
|
|
<ObjectTeams
|
|
scope={scope}
|
|
label="Teams"
|
|
permissions={permissions}
|
|
isDialogOpen={teamDialog}
|
|
refreshCallback={blank}
|
|
closeDialogCallback={() => setTeamDialog(false)}
|
|
/>
|
|
);
|
|
};
|
|
|
|
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);
|
|
};
|
|
|
|
const binComponents = (preferences: Map<string, pond.BinComponentPreferences>) => {
|
|
return (
|
|
<Card raised style={{ marginTop: 15 }}>
|
|
<Accordion elevation={0}>
|
|
<AccordionSummary expandIcon={<ExpandMore />}>
|
|
<Typography style={{ fontWeight: 650 }}>Bin Components</Typography>
|
|
</AccordionSummary>
|
|
<AccordionDetails>
|
|
<BinComponentTypes
|
|
bin={bin.key()}
|
|
updateBinStatus={updateStatus}
|
|
components={components}
|
|
setComponents={setComponents}
|
|
preferences={preferences}
|
|
setPreferences={setPreferences}
|
|
/>
|
|
</AccordionDetails>
|
|
</Accordion>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
const desktopView = () => {
|
|
return (
|
|
<Card style={{ padding: 15 }}>
|
|
<Grid container spacing={2} direction={"row"} justifyContent="center" alignItems="stretch">
|
|
<Grid size={{ xs: 12, sm: 12, md: 6, lg: 3 }}>
|
|
<Box
|
|
paddingX={1}
|
|
marginBottom={1}
|
|
display="flex"
|
|
alignItems="center"
|
|
justifyContent="space-between"
|
|
style={{ background: "gold", borderRadius: 25 }}>
|
|
<Box display="flex" alignItems="center">
|
|
<Typography style={{ fontWeight: 650, fontSize: 20, color: "black" }}>
|
|
{bin.name()}
|
|
</Typography>
|
|
<Typography variant="subtitle2" style={{ paddingLeft: 5, color: "black" }}>
|
|
{" - " +
|
|
(bin.settings.specs?.modelId
|
|
? getBinModel(bin.settings.specs.modelId)?.Manufacturer +
|
|
" " +
|
|
getBinModel(bin.settings.specs.modelId)?.Model
|
|
: "Custom")}
|
|
</Typography>
|
|
</Box>
|
|
<Box>{componentState()}</Box>
|
|
</Box>
|
|
{overview()}
|
|
{preferences && binComponents(preferences)}
|
|
</Grid>
|
|
<Grid size={{ xs: 12, sm: 12, md: 6, lg: 3 }}>
|
|
{(bin.settings.mode === pond.BinMode.BIN_MODE_DRYING ||
|
|
bin.settings.mode === pond.BinMode.BIN_MODE_HYDRATING) && (
|
|
<BinConditioningCard
|
|
mode={bin.settings.mode}
|
|
interactions={interactions}
|
|
interactionDevices={interactionDevices}
|
|
plenums={plenums}
|
|
ambients={ambients}
|
|
fans={fans}
|
|
heaters={heaters}
|
|
grain={bin.grain()}
|
|
/>
|
|
)}
|
|
<Box marginTop={2}>
|
|
<BinStorageConditions bin={bin} headspaceCO2={headspaceCO2} cables={grainCables} />
|
|
</Box>
|
|
</Grid>
|
|
<Grid id="tour-graphs" size={{ xs: 12, sm: 12, md: 12, lg: 6 }}>
|
|
<Box paddingBottom={2}>
|
|
<StyledToggleButtonGroup
|
|
id="tour-graph-tabs"
|
|
value={detail}
|
|
exclusive
|
|
size="small"
|
|
aria-label="detail">
|
|
<StyledToggle
|
|
value={"inventory"}
|
|
aria-label="Inventory Details"
|
|
onClick={() => setDetail("inventory")}>
|
|
Inventory
|
|
</StyledToggle>
|
|
<StyledToggle
|
|
onClick={() => setDetail("sensors")}
|
|
value={"sensors"}
|
|
aria-label="Bin Sensor Graphs">
|
|
Sensors
|
|
</StyledToggle>
|
|
<StyledToggle
|
|
onClick={() => setDetail("analytics")}
|
|
value={"analytics"}
|
|
aria-label="Bin Analysis Graphs">
|
|
Analysis
|
|
</StyledToggle>
|
|
<StyledToggle
|
|
onClick={() => setDetail("alerts")}
|
|
value={"alerts"}
|
|
aria-label="Device Alerts">
|
|
Alerts
|
|
</StyledToggle>
|
|
<StyledToggle
|
|
onClick={() => setDetail("presets")}
|
|
value={"presets"}
|
|
aria-label="Bin Mode Presets">
|
|
Presets
|
|
</StyledToggle>
|
|
</StyledToggleButtonGroup>
|
|
</Box>
|
|
{detail === "alerts" && (
|
|
<Box>
|
|
<ObjectAlerts
|
|
linkedComponents={components}
|
|
componentDevices={componentDevices}
|
|
objectType={pond.ObjectType.OBJECT_TYPE_BIN}
|
|
objectKey={bin.key()}
|
|
/>
|
|
</Box>
|
|
)}
|
|
{detail === "presets" && (
|
|
<Box>
|
|
<DevicePresetController
|
|
objectKey={bin.key()}
|
|
objectType={pond.ObjectType.OBJECT_TYPE_BIN}
|
|
presets={binPresets}
|
|
refreshCallback={presets => {
|
|
setBinPresets(presets);
|
|
}}
|
|
/>
|
|
</Box>
|
|
)}
|
|
{(detail === "inventory" || detail === "sensors" || detail === "analytics") && (
|
|
<BinGraphs
|
|
bin={bin}
|
|
binLoading={binLoading}
|
|
plenums={plenums}
|
|
cables={grainCables}
|
|
pressures={pressures}
|
|
fans={fans}
|
|
componentDevices={componentDevices}
|
|
display={detail}
|
|
compMap={components}
|
|
compositionNameMap={compositionNameMap}
|
|
/>
|
|
)}
|
|
</Grid>
|
|
</Grid>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
const mobileView = () => {
|
|
return (
|
|
<Card style={{ padding: 2, width: "100%" }}>
|
|
<Tabs
|
|
style={{ marginBottom: 5 }}
|
|
value={mobileTab}
|
|
indicatorColor="secondary"
|
|
//textColor="secondary"
|
|
onChange={(_, newVal) => {
|
|
setMobileTab(newVal);
|
|
}}
|
|
aria-label="bin tabs"
|
|
variant="scrollable"
|
|
// scrollButtons="on"
|
|
>
|
|
<Tab
|
|
classes={{ root: classes.tab }}
|
|
label="Bin"
|
|
style={{ fontWeight: mobileTab === 0 ? 650 : 500 }}
|
|
/>
|
|
<Tab label="Inventory" style={{ fontWeight: mobileTab === 1 ? 650 : 500 }} />
|
|
<Tab label="Sensors" style={{ fontWeight: mobileTab === 2 ? 650 : 500 }} />
|
|
<Tab label="Analysis" style={{ fontWeight: mobileTab === 3 ? 650 : 500 }} />
|
|
<Tab label="Alerts" style={{ fontWeight: mobileTab === 3 ? 650 : 500 }} />
|
|
<Tab label="Presets" style={{ fontWeight: mobileTab === 3 ? 650 : 500 }} />
|
|
</Tabs>
|
|
<TabPanelMine value={mobileTab} index={0}>
|
|
{overview()}
|
|
{(bin.settings.mode === pond.BinMode.BIN_MODE_DRYING ||
|
|
bin.settings.mode === pond.BinMode.BIN_MODE_HYDRATING) && (
|
|
<Box style={{ marginTop: 15 }}>
|
|
<BinConditioningCard
|
|
mode={bin.settings.mode}
|
|
interactions={interactions}
|
|
interactionDevices={interactionDevices}
|
|
plenums={plenums}
|
|
ambients={ambients}
|
|
fans={fans}
|
|
heaters={heaters}
|
|
grain={bin.grain()}
|
|
/>
|
|
</Box>
|
|
)}
|
|
<Box style={{ marginTop: 15 }}>
|
|
<BinStorageConditions bin={bin} headspaceCO2={headspaceCO2} cables={grainCables} />
|
|
</Box>
|
|
{preferences && binComponents(preferences)}
|
|
</TabPanelMine>
|
|
{/*TODO-CS need to find a better way to do this rather than have 3 seperate panels with the same thing */}
|
|
<TabPanelMine value={mobileTab} index={1}>
|
|
<BinGraphs
|
|
bin={bin}
|
|
binLoading={binLoading}
|
|
plenums={plenums}
|
|
cables={grainCables}
|
|
pressures={pressures}
|
|
fans={fans}
|
|
componentDevices={componentDevices}
|
|
display={"inventory"}
|
|
compMap={components}
|
|
compositionNameMap={compositionNameMap}
|
|
/>
|
|
</TabPanelMine>
|
|
<TabPanelMine value={mobileTab} index={2}>
|
|
<BinGraphs
|
|
bin={bin}
|
|
binLoading={binLoading}
|
|
plenums={plenums}
|
|
cables={grainCables}
|
|
pressures={pressures}
|
|
fans={fans}
|
|
componentDevices={componentDevices}
|
|
display={"sensors"}
|
|
compositionNameMap={compositionNameMap}
|
|
compMap={components}
|
|
/>
|
|
</TabPanelMine>
|
|
<TabPanelMine value={mobileTab} index={3}>
|
|
<BinGraphs
|
|
bin={bin}
|
|
binLoading={binLoading}
|
|
plenums={plenums}
|
|
cables={grainCables}
|
|
pressures={pressures}
|
|
fans={fans}
|
|
componentDevices={componentDevices}
|
|
display={"analytics"}
|
|
compositionNameMap={compositionNameMap}
|
|
compMap={components}
|
|
/>
|
|
</TabPanelMine>
|
|
<TabPanelMine value={mobileTab} index={4}>
|
|
<Box>
|
|
<ObjectAlerts
|
|
linkedComponents={components}
|
|
componentDevices={componentDevices}
|
|
objectType={pond.ObjectType.OBJECT_TYPE_BIN}
|
|
objectKey={bin.key()}
|
|
/>
|
|
</Box>
|
|
</TabPanelMine>
|
|
<TabPanelMine value={mobileTab} index={5}>
|
|
<DevicePresetController
|
|
objectKey={bin.key()}
|
|
objectType={pond.ObjectType.OBJECT_TYPE_BIN}
|
|
presets={binPresets}
|
|
refreshCallback={presets => {
|
|
setBinPresets(presets);
|
|
}}
|
|
/>
|
|
</TabPanelMine>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
const componentState = () => {
|
|
return (
|
|
<Box display="flex" alignContent="center" alignItems="center">
|
|
<Typography
|
|
align="center"
|
|
style={{
|
|
marginRight: 10,
|
|
fontWeight: 650,
|
|
fontSize: 15,
|
|
textAlign: "center",
|
|
color: "black"
|
|
}}>
|
|
{missedCableReadings < 3
|
|
? "Active"
|
|
: missedCableReadings <= warningThreshold
|
|
? "Missing"
|
|
: "Inactive"}
|
|
</Typography>
|
|
{missedCableReadings < 3 ? (
|
|
<CheckCircle style={{ color: "green" }} />
|
|
) : missedCableReadings <= warningThreshold ? (
|
|
<Warning style={{ color: "yellow" }} />
|
|
) : (
|
|
<Warning style={{ color: "red" }} />
|
|
)}
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<PageContainer>
|
|
{showPlenumError()}
|
|
<Box
|
|
paddingBottom={isMobile || displayMobile ? 1 : 2}
|
|
paddingX={isMobile || displayMobile ? 1 : 2}>
|
|
{tabs()}
|
|
{isMobile && (
|
|
<Box
|
|
paddingX={1}
|
|
marginBottom={1}
|
|
display="flex"
|
|
alignItems="center"
|
|
justifyContent="space-between"
|
|
style={{ background: "gold", borderRadius: 25 }}>
|
|
<Box display="flex" alignItems="center">
|
|
<Typography style={{ fontWeight: 650, fontSize: 18, color: "black" }}>
|
|
{bin.name()}
|
|
</Typography>
|
|
<Typography variant="subtitle2" style={{ paddingLeft: 5, color: "black" }}>
|
|
{" - " +
|
|
(bin.settings.specs?.modelId
|
|
? getBinModel(bin.settings.specs.modelId)?.Manufacturer +
|
|
" " +
|
|
getBinModel(bin.settings.specs.modelId)?.Model
|
|
: "Custom")}
|
|
</Typography>
|
|
</Box>
|
|
<Box>{componentState()}</Box>
|
|
</Box>
|
|
)}
|
|
<Grid container spacing={1} direction="column" justifyContent="center" alignItems="stretch">
|
|
{showBin() && (isMobile || displayMobile ? mobileView() : desktopView())}
|
|
{isMobile || displayMobile ? (
|
|
notesAndHistory(displayMobile)
|
|
) : (
|
|
<Drawer
|
|
open={noteDrawer}
|
|
anchor="right"
|
|
classes={{ paper: classes.drawerPaper }}
|
|
onClose={() => setNoteDrawer(false)}>
|
|
<IconButton style={{ width: 50 }} onClick={() => setNoteDrawer(false)}>
|
|
<Close />
|
|
</IconButton>
|
|
{notesAndHistory(true)}
|
|
</Drawer>
|
|
)}
|
|
{isMobile || displayMobile ? (
|
|
tasks()
|
|
) : (
|
|
<Drawer
|
|
BackdropProps={{ invisible: true }}
|
|
style={{ zIndex: 1099 }}
|
|
open={taskDrawer}
|
|
anchor="right"
|
|
classes={{ paper: classes.drawerPaper }}
|
|
onClose={() => setTaskDrawer(false)}>
|
|
<Box paddingTop={10}>
|
|
<IconButton style={{ width: 50 }} onClick={() => setTaskDrawer(false)}>
|
|
<Close />
|
|
</IconButton>
|
|
{/* <TaskViewer drawerView objectKey={binID} loadKeys={[binID]} /> */}
|
|
</Box>
|
|
</Drawer>
|
|
)}
|
|
</Grid>
|
|
</Box>
|
|
{objectTeams()}
|
|
{deviceMenu()}
|
|
<BinTour />
|
|
</PageContainer>
|
|
);
|
|
}
|