frontend/src/pages/Bin.tsx

1163 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 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";
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;
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 = binKey ?? useParams<{ binID: string }>()?.binID ?? "";
const binAPI = useBinAPI();
const [{ user, as }] = 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, 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.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, 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: 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, undefined, undefined, as).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 = () => {
console.log(showTasks())
if (showTasks()) {
return (
<React.Fragment>
<TaskViewer drawerView objectKey={binID} loadKeys={[binID]} />
</React.Fragment>
);
}
return null;
};
const goToDevice = (dev: Device) => {
if (fromMap) {
navigate("/bins/" + binID + "/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, as);
};
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>
<IconButton style={{ width: 50 }} onClick={() => setTaskDrawer(false)}>
<Close />
</IconButton>
<TaskViewer drawerView objectKey={binID} loadKeys={[binID]} />
</Box>
</Drawer>
)}
</Grid>
</Box>
{objectTeams()}
{deviceMenu()}
<BinTour />
</PageContainer>
);
}