import { Box, IconButton, Theme, Grid2 as Grid, DialogTitle, DialogContent, DialogContentText, DialogActions, Button, Tooltip, Drawer, Menu, MenuItem, ListItemIcon, ListItemText, Tab, Tabs, Card, Accordion, AccordionSummary, AccordionDetails, Typography, } 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 BinGraphs from "bin/graphs/BinGraphs"; import BinTour from "bin/BinTour"; import { getBinModel } from "common/DataImports/BinCables/BinCableImporter"; 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 } from "@mui/styles"; import { useNavigate, useParams } from "react-router-dom"; import { Controller } from "models/Controller"; import TaskViewer from "tasks/TaskViewer"; import ButtonGroup from "common/ButtonGroup"; import BinTransactions from "bin/BinTransactions"; import { CO2 } from "models/CO2"; import ObjectInteractions from "objects/objectInteractions/ObjectInteractions"; interface TabPanelProps { children?: React.ReactNode; index: any; value: any; } function TabPanelMine(props: TabPanelProps) { const { children, value, index, ...other } = props; return ( ); } 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(); // 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(false); const [bin, setBin] = useState(IBin.create()); const [tab, setTab] = useState("Bin"); const [value, setValue] = React.useState(0); const [devices, setDevices] = useState([]); const [components, setComponents] = useState>(new Map()); const [interactions, setInteractions] = useState([]); const [plenumError, setPlenumError] = useState(false); const [teamDialog, setTeamDialog] = useState(false); // const history = useHistory(); const navigate = useNavigate() const [noteDrawer, setNoteDrawer] = useState(false); const [taskDrawer, setTaskDrawer] = useState(false); const [anchorEl, setAnchorEl] = React.useState(null); const [permissions, setPermissions] = useState([]); const [preferences, setPreferences] = useState>(); const [plenums, setPlenums] = useState([]); const [ambients, setAmbients] = useState([]); const [grainCables, setGrainCables] = useState([]); const [pressures, setPressures] = useState([]); const [headspaceCO2, setHeadspaceCO2] = useState([]); const [heaters, setHeaters] = useState([]); const [fans, setFans] = useState([]); const [activeDetails, setActiveDetails] = useState([0]) const [detail, setDetail] = useState< "inventory" | "sensors" | "analytics" | "presets" | "alerts" | "transactions" >("inventory"); const [mobileTab, setMobileTab] = useState(0); const componentAPI = useComponentAPI(); const [{ showErrors }] = useGlobalState(); const [compositionNameMap, setCompositionNameMap] = useState>(new Map()); const [componentDevices, setComponentDevices] = useState>( new Map() ); const [interactionDevices, setInteractionDevices] = useState>( new Map() ); const [binPresets, setBinPresets] = useState([]); const [missedReadings, setMissedReadings] = useState(0); const handleChange = (_event: React.ChangeEvent<{}>, newValue: number) => { setValue(newValue); }; 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 = 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(); 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(); 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(); 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 = 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]); 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 ( setPlenumError(false)}> No Plenum Sensor Detected 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. ); }; // 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 ( ); } return null; }; const tasks = () => { if (showTasks()) { return ( ); } 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 ( setAnchorEl(null)} keepMounted classes={{ paper: classes.menuPaper }} disableAutoFocusItem> {devices.map(dev => ( goToDevice(dev)}> ))} ); }; const tabs = () => { return ( switchToTab("Bin")} size="small" style={{ marginRight: "0.5rem" }} component="span" className={tab === "Bin" ? classes.active : classes.avatar}>
icon
isMobile || displayMobile ? switchToTab("Notes") : setNoteDrawer(true) } size="small" style={{ marginRight: "0.5rem" }} className={tab === "Notes" ? classes.active : classes.avatar} component="span">
isMobile || displayMobile ? switchToTab("Tasks") : setTaskDrawer(true) } size="small" style={{ marginRight: "0.5rem" }} className={tab === "Tasks" ? classes.active : classes.avatar} component="span">
{devices.length > 1 ? ( ) => setAnchorEl(event.currentTarget) } size="small" style={{ marginRight: "0.5rem" }} className={classes.avatar} component="span"> ) : ( devices.map(dev => ( goToDevice(dev)} size="small" style={{ marginRight: "0.5rem" }} className={classes.avatar} component="span">
)) )} {bin.binMapped() && !fromMap && ( { goToYard(bin); }} size="small" style={{ marginRight: "0.5rem" }} className={classes.avatar} component="span"> )}
{tab === "Bin" && ( { load(); }} userID={user.id()} components={components} setComponents={setComponents} updateBinStatus={updateStatus} /> )}
); }; const overview = () => { return ( { reLoadSingleComponent(componentKey); }} binPresets={binPresets} /> ); }; const blank = () => { return true; }; const objectTeams = () => { let scope = binScope(binID); return ( 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) => { return ( }> Bin Components ); }; const desktopView = () => { return ( {bin.name()} {" - " + (bin.settings.specs?.modelId ? getBinModel(bin.settings.specs.modelId)?.Manufacturer + " " + getBinModel(bin.settings.specs.modelId)?.Model : "Custom")} {componentState()} {overview()} {preferences && binComponents(preferences)} {(bin.settings.mode === pond.BinMode.BIN_MODE_DRYING || bin.settings.mode === pond.BinMode.BIN_MODE_HYDRATING || bin.settings.mode === pond.BinMode.BIN_MODE_COOLDOWN) && ( )} { setDetail("inventory") setActiveDetails([0]) } }, { title: "Sensors", function: () => { setDetail("sensors") setActiveDetails([1]) } }, { title: "Analysis", function: () => { setDetail("analytics") setActiveDetails([2]) } }, { title: "Alerts", function: () => { setDetail("alerts") setActiveDetails([3]) } }, { title: "Presets", function: () => { setDetail("presets") setActiveDetails([4]) } }, { title: "Transactions", function: () => { setDetail("transactions") setActiveDetails([5]) } } ]} /> {detail === "alerts" && ( )} {detail === "presets" && ( { setBinPresets(presets); }} /> )} {detail === "transactions" && } {(detail === "inventory" || detail === "sensors" || detail === "analytics") && ( )} ); }; const mobileView = () => { return ( { setMobileTab(newVal); }} aria-label="bin tabs" variant="scrollable" // scrollButtons="on" > {overview()} {(bin.settings.mode === pond.BinMode.BIN_MODE_DRYING || bin.settings.mode === pond.BinMode.BIN_MODE_HYDRATING) && ( )} {preferences && binComponents(preferences)} {/*TODO-CS need to find a better way to do this rather than have 3 seperate panels with the same thing */} { setBinPresets(presets); }} /> ); }; const componentState = () => { const hasCables = grainCables.length > 0 const hasCO2 = headspaceCO2.length > 0 return ( {hasCables || hasCO2 ? {missedReadings < 3 ? "Active" : missedReadings <= warningThreshold ? "Missing" : "Inactive"} {missedReadings < 3 ? ( ) : missedReadings <= warningThreshold ? ( ) : ( )} : Unmonitored } ); }; return ( {showPlenumError()} {tabs()} {isMobile && ( {bin.name()} {" - " + (bin.settings.specs?.modelId ? getBinModel(bin.settings.specs.modelId)?.Manufacturer + " " + getBinModel(bin.settings.specs.modelId)?.Model : "Custom")} {componentState()} )} {showBin() && (isMobile || displayMobile ? mobileView() : desktopView())} {isMobile || displayMobile ? ( notesAndHistory(displayMobile) ) : ( setNoteDrawer(false)}> setNoteDrawer(false)}> {notesAndHistory(true)} )} {isMobile || displayMobile ? ( tasks() ) : ( setTaskDrawer(false)}> setTaskDrawer(false)}> )} {objectTeams()} {deviceMenu()} { setDetail(detail) setActiveDetails([buttonIndex]) }}/> ); }