import { Accordion, AccordionDetails, AccordionSummary, Box, Button, Checkbox, Chip, darken, DialogActions, DialogContent, DialogTitle, Divider, FormControl, FormControlLabel, Grid2 as Grid, IconButton, ImageList, ImageListItem, InputLabel, lighten, ListItemIcon, ListItemText, Menu, MenuItem, Select, Skeleton, Theme, ToggleButton, ToggleButtonGroup, Typography, useTheme, } from "@mui/material"; import { ArrowBackIos, ArrowForwardIos, ExpandMore, LibraryAdd, MoreVert, ViewComfy, ViewList } from "@mui/icons-material"; import BinsFansStatusTable from "bin/BinFansStatusTable"; // import BinSettings from "bin/BinSettings"; import BinsList from "bin/BinsList"; // import BinYard from "bin/BinYard"; import BinInventoryChart, { GrainAmount } from "charts/BinInventoryChart"; import BinUtilizationChart from "charts/BinUtilizationChart"; // import QrCodeGenerator, { QrCodeKey } from "common/QrCodeGenerator"; import GrainDescriber, { grainName } from "grain/GrainDescriber"; // import GrainBagList from "grainBag/grainBagList"; // import GrainBagSettings from "grainBag/grainBagSettings"; import { useMobile, useWidth } from "hooks"; import { Dictionary } from "lodash"; // import { Bin, Interaction, BinYard as BinYardModel } from "models"; import { Bin, Interaction, BinYard as BinYardModel } from "models"; // import { GrainBag } from "models/GrainBag"; import { pond, quack } from "protobuf-ts/pond"; import { useBinAPI, // useBinYardAPI, useGlobalState, useInteractionsAPI, useSnackbar } from "providers"; import React, { SetStateAction, useCallback, useEffect, useState } from "react"; // import { getThemeType } from "theme"; import { getGrainUnit, stringToMaterialColour } from "utils"; //import InfiniteScroll from "react-infinite-scroller"; import PageContainer from "./PageContainer"; import ObjectTable from "objects/ObjectTable"; import ResponsiveDialog from "common/ResponsiveDialog"; import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; import { green, yellow } from "@mui/material/colors"; import { makeStyles, styled } from "@mui/styles"; import { useNavigate } from "react-router-dom"; import BinSettings from "bin/BinSettings"; import AddBinFab from "bin/AddBinFab"; import { GrainBag } from "models/GrainBag"; import { getThemeType } from "theme/themeType"; import GrainBagSettings from "grainBag/grainBagSettings"; import GrainBagList from "grainBag/grainBagList"; import BinYards from "bin/BinYards"; // import { useHistory } from "react-router"; const useStyles = makeStyles((theme: Theme) => { const themeType = getThemeType(); const accordionBg = themeType === "light" ? theme.palette.background.paper : darken(theme.palette.background.paper, 0.2) return ({ "@keyframes pulsate": { to: { boxShadow: "0 0 0 16px" + yellow["500"] + "00" } }, green: { color: green["500"], "&:hover": { color: green["600"] } }, gridList: { width: "100%", flexWrap: "nowrap", transform: "translateZ(0)" }, pulse: { boxShadow: "0 0 0 0 " + yellow["500"] + "75", animation: "$pulsate 1.75s infinite cubic-bezier(0.66, 0.33, 0, 1)" }, icon: { padding: 6, width: 36, height: 36, borderRadius: "18px", background: "rgba(0,0,0,0)", "&:hover": { background: "radial-gradient(closest-side, rgba(150, 150, 150, 0.5) 50%, rgba(150, 150, 150, 0.5))" } }, accordion: { background: accordionBg } }); }); interface PaginatedBins { bins: Bin[]; binsOffset: number; binsTotal: number; } interface Props { yardFilter?: string; yards?: pond.BinYardSettings[]; insert?: boolean; } export default function Bins(props: Props) { const classes = useStyles(); const isMobile = useMobile(); const binAPI = useBinAPI(); // const binyardAPI = useBinYardAPI(); const interactionsAPI = useInteractionsAPI(); const [{ user, as }] = useGlobalState(); const [allLoading, setAllLoading] = useState(false); const [binsLoading, setBinsLoading] = useState(false); const [addBinOpen, setAddBinOpen] = useState(false); const [paginatedBins, setPaginatedBins] = useState({ bins: [], binsOffset: 0, binsTotal: 0 }); const [binMetrics, setBinMetrics] = useState(); const [binMenuAnchorEl, setBinMenuAnchorEl] = useState(null); const [menuAnchorEl, setMenuAnchorEl] = useState(null); const [contentFilter, setContentFilter] = useState(""); const [yardFilter, setYardFilter] = useState(""); const [order, setOrder] = useState<"asc" | "desc">("asc"); const [orderBy, setOrderBy] = useState("name"); const binsLimit = 40; const theme = useTheme(); const width = useWidth(); const [yards, setYards] = useState(props.yards ?? []); const [yardPermissions, setYardPermissions] = useState>({}); const [loadingAs, setLoadingAs] = useState(as); const [loadingFilter, setLoadingFilter] = useState("init"); const [displayGrain, setDisplayGrain] = useState(true); const [displayFert, setDisplayFert] = useState(false); const [displayEmpty, setDisplayEmpty] = useState(false); const [grainBins, setGrainBins] = useState([]); const [fertilizerBins, setFertilizerBins] = useState([]); const [emptyBins, setEmptyBins] = useState([]); const [grainBags, setGrainBags] = useState([]); const [displayedInventory, setDisplayedInventory] = useState([]); const [displayedFertilizer, setDisplayedFertilizer] = useState([]); const [carouselIndex, setCarouselIndex] = useState(0); const [addBagOpen, setAddBagOpen] = useState(false); const [showing, setShowing] = useState<"all" | "bins" | "bags">("all"); //const [binGridView, setBinGridView] = useState(false); const [binView, setBinView] = useState<"grid" | "scroll" | "list">("grid"); const [bagView, setBagView] = useState<"grid" | "scroll" | "list">("grid"); const [expandTotal, setExpandTotal] = useState(false); const [expandUtilization, setExpandUtilization] = useState(false); const [expandFanTable, setExpandFanTable] = useState(false); const [openQrGenerator, setOpenQrGenerator] = useState(false); // const [qrKeyList, setQrKeyList] = useState([]); const [totalFanControllers, setTotalFanControllers] = useState(0); const [totalFanControllersOn, setTotalFanControllersOn] = useState(0); const [yardsLoading, setYardsLoading] = useState(false); const [openAddAlerts, setOpenAddAlerts] = useState(false); const [alertBins, setAlertBins] = useState([]); const { openSnack } = useSnackbar(); // const history = useHistory(); const navigate = useNavigate() const [cardValDisplay, setCardValDisplay] = useState<"high" | "low" | "average">("average"); // const [scrollTranslations, setScrollTranslations] = useState({ // bins: 0, // empty: 0, // fertilizer: 0 // }); const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }: { theme: Theme }) => ({ '& .MuiToggleButtonGroup-grouped': { // border: 'none' replaces the old 'grouped' border: "none" border: 'none', padding: theme.spacing(1), // Middle buttons '&:not(:first-child):not(:last-child)': { borderRadius: 24, marginRight: theme.spacing(0.5), marginLeft: theme.spacing(0.5), }, // First button '&:first-child': { borderRadius: 24, marginLeft: theme.spacing(0.25), }, // Last button '&:last-child': { borderRadius: 24, marginRight: theme.spacing(0.25), }, }, // Root styles backgroundColor: darken( theme.palette.background.paper, getThemeType() === 'light' ? 0.05 : 0.25 ), borderRadius: 24, boxSizing: 'content-box', // Fixed 'content: "border-box"' typo from original })); const StyledToggle = styled(ToggleButton)(({ theme }: { theme: Theme }) => ({ 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: {} })); useEffect(() => { let ebt = sessionStorage.getItem("expandBinTotal"); if (ebt === "true") { setExpandTotal(true); } else { setExpandTotal(false); } let ebu = sessionStorage.getItem("expandBinUtilization"); if (ebu === "true") { setExpandUtilization(true); } else { setExpandUtilization(false); } let binsView = sessionStorage.getItem("binsView"); if (binsView === "scroll") { setBinView("scroll"); } else { setBinView("grid"); } let bagsView = sessionStorage.getItem("bagsView"); if (bagsView === "scroll") { setBagView("scroll"); } else { setBagView("grid"); } let fanTable = sessionStorage.getItem("expandFanTable"); if (fanTable === "true") { setExpandFanTable(true); } else { setExpandFanTable(false); } }, []); useEffect(() => { if (props.yardFilter) { setYardFilter(props.yardFilter); } }, [props.yardFilter]); const loadBins = useCallback(() => { //let filter = grainFilter; let filter = yardFilter; if (as === loadingAs && loadingFilter === filter) { return; } setLoadingAs(as); setLoadingFilter(filter); setAllLoading(true); setBinsLoading(true); setDisplayEmpty(false); setDisplayFert(false); binAPI .listBinsAndData(binsLimit, 0, order, orderBy, filter, as, false) .then(resp => { let grain: Bin[] = []; let empty: Bin[] = []; let fert: Bin[] = []; // let qr: QrCodeKey[] = []; let b = resp.data.bins.map(b => Bin.any(b)); b.forEach(bin => { if (bin.empty()) { empty.push(bin); } else if (bin.storage() === pond.BinStorage.BIN_STORAGE_FERTILIZER) { fert.push(bin); } else { grain.push(bin); } //build qr keys here // qr.push({ // key: bin.key(), // name: bin.name() // }); }); // setQrKeyList(qr); setGrainBins(grain); if (fert.length > 0) { setDisplayFert(true); } if (empty.length > 0) { setDisplayEmpty(true); } setFertilizerBins(fert); setEmptyBins(empty); setPaginatedBins({ bins: b, binsOffset: resp.data.nextOffset, binsTotal: resp.data.total }); let metrics = pond.BinMetrics.fromObject(resp.data.metrics ?? {}); let inventory: GrainAmount[] = []; let fertInventory: GrainAmount[] = []; if (metrics) { metrics.grainInventory.forEach(grain => { inventory.push({ grain: grain.grainType, bushelAmount: grain.bushelAmount, grainName: grainName(grain.grainType) }); }); metrics.customInventory.forEach(invObject => { if (invObject.storageType === pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN) { inventory.push({ grain: pond.Grain.GRAIN_CUSTOM, bushelAmount: invObject.amount, grainName: invObject.name }); } else if (invObject.storageType === pond.BinStorage.BIN_STORAGE_FERTILIZER) { fertInventory.push({ grain: pond.Grain.GRAIN_CUSTOM, bushelAmount: invObject.amount * 35.239, grainName: invObject.name }); } }); } setBinMetrics(metrics); setDisplayedInventory(inventory); setDisplayedFertilizer(fertInventory); if (yards.length === 0) { let y: pond.BinYardSettings[] = []; let p: Dictionary = {}; let sortedYards = resp.data.binYards.sort((a, b) => { let yard1 = BinYardModel.create(a); let yard2 = BinYardModel.create(b); return ( yard1.settings.userSort[as ?? user.id()] - yard2.settings.userSort[as ?? user.id()] ); }); sortedYards.forEach(yard => { let newYard = pond.BinYard.fromObject(yard ?? {}); if (newYard.settings) { y.push(newYard.settings); p[newYard.settings.key] = newYard.yardPermissions; } }); setYardPermissions(p); setYards(y); } //move load of grain bags into this api call const bags = resp.data.bags.map(b => GrainBag.create(b)); setGrainBags(bags); }) .catch(err => { setPaginatedBins({ bins: [], binsOffset: 0, binsTotal: 0 }); setBinMetrics(pond.BinMetrics.create()); setYards([]); }) .finally(() => { setAllLoading(false); setBinsLoading(false); }); }, [binAPI, yardFilter, order, orderBy, as, loadingAs, loadingFilter]); // eslint-disable-line react-hooks/exhaustive-deps //made this a seperate function because we only need to load the bins themselves, we do not need to load all the metric data again const loadMoreBins = (filter: string, offset?: number, limit?: number) => { setBinsLoading(true); let binFilter = filter; binFilter = binFilter + " " + yardFilter; binAPI .listBins(limit ?? binsLimit, offset ?? 0, order, orderBy, binFilter, as) .then(resp => { const newBins = resp.data.bins.map(b => Bin.any(b)); setPaginatedBins({ //the offset being passed in means to load more rather than a new set so combine the loaded bins with the existing ones rather than replace them bins: offset ? paginatedBins.bins.concat(newBins) : newBins, binsOffset: resp.data.nextOffset, binsTotal: resp.data.total }); let empty: Bin[] = offset ? emptyBins : []; let fert: Bin[] = offset ? fertilizerBins : []; let grain: Bin[] = offset ? grainBins : []; // let newKeyList: QrCodeKey[] = qrKeyList; newBins.forEach(bin => { if (bin.empty()) { empty.push(bin); } else if (bin.storage() === pond.BinStorage.BIN_STORAGE_FERTILIZER) { fert.push(bin); } else { grain.push(bin); } // newKeyList.push({ // key: bin.key(), // name: bin.name() // }); }); if (fert.length > 0) { setDisplayFert(true); } if (empty.length > 0) { setDisplayEmpty(true); } setGrainBins([...grain]); setEmptyBins([...empty]); setFertilizerBins([...fert]); // setQrKeyList([...newKeyList]); }) .catch(err => { setPaginatedBins({ bins: [], binsOffset: 0, binsTotal: 0 }); }) .finally(() => { setBinsLoading(false); }); }; const searchYards = ( search?: string, limit?: number, offset?: number, order?: "asc" | "desc" ) => { setYardsLoading(true); // binyardAPI.listBinYards(limit ?? binsLimit, offset ?? 0, order, "name", search).then(resp => { // let y: pond.BinYardSettings[] = []; // let p: Dictionary = {}; // //sort the yards after loading // let sortedYards = resp.data.yard.sort((a, b) => { // let yard1 = BinYardModel.create(a); // let yard2 = BinYardModel.create(b); // return yard1.settings.userSort[as ?? user.id()] - yard2.settings.userSort[as ?? user.id()]; // }); // //loop through the sorted yards // sortedYards.forEach(yard => { // let newYard = pond.BinYard.fromObject(yard ?? {}); // if (newYard.settings) { // y.push(newYard.settings); // p[newYard.settings.key] = newYard.yardPermissions; // } // }); // if (y.length > 0) { // //dont set the yards if it came back with nothing // setYardPermissions(p); // setYards(y); // } // setYardsLoading(false); // }); }; useEffect(() => { loadBins(); //if it is a drawer only show the bins, bags are only shown on the all tab so should not show up on the yard drawers if (props.insert) { setShowing("bins"); } }, [loadBins, props.insert, contentFilter]); const duplicateBin = (bin: Bin) => { binAPI.addBin(bin.settings).then(resp => { loadBins(); }); }; const addAlertsToCables = (bins: Bin[]) => { let alertList: pond.AlertData[] = []; bins.forEach(bin => { //for each of the cable in the bin create alert data bin.status.grainCables.forEach(cable => { //split the location into its three parts let locationParts = cable.address.split("-"); if (locationParts[0] && locationParts[1] && locationParts[2]) { //build the alert based on the bin alertList.push( pond.AlertData.create({ device: cable.device, nodeOne: cable.topNode, interactionSubtype: Interaction.upToSubtype(cable.topNode), condition: pond.InteractionCondition.create({ measurementType: quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE, comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN, value: describeMeasurement( quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE ).toStored(bin.settings.highTemp) }), componentLocation: quack.ComponentID.create({ type: parseInt(locationParts[0]), addressType: parseInt(locationParts[1]), address: parseInt(locationParts[2]) }) }) ); } }); }); interactionsAPI .setAlertInteractions(alertList) .then(resp => { openSnack("Successfully added " + resp.data.numAlertsSet + " alerts"); }) .catch(err => { console.log("errors found"); }); }; const addALertsDialog = () => { return ( setOpenAddAlerts(false)}> Add Alert to Bin Cables This action will add an interaction to notify if the temp of any cable node in grain rises above the high temp warning for all selected bins ); }; const binsContent = () => { const { binsTotal } = paginatedBins; if (!allLoading && binsTotal <= 0) { return ( No bins found Create a bin and it will appear here ); } if (binView === "list") { return ( { let bin = data as Bin; let path = "/bins/" + bin.key(); navigate(path, { state: { bin: bin }}); }} customButtons={[ { label: "Set High Temp Notifications", function: objects => { setAlertBins(objects as Bin[]); setOpenAddAlerts(true); } } ]} /> ); } return ( {displayGrain && ( { if (paginatedBins.bins.length < paginatedBins.binsTotal) { loadMoreBins(contentFilter, paginatedBins.binsOffset); } }} /> )} {displayFert && ( { if (paginatedBins.bins.length < paginatedBins.binsTotal) { loadMoreBins(contentFilter, paginatedBins.binsOffset); //setScrollTranslations({ ...scrollTranslations, fertilizer: newTranslation }); } }} /> )} {displayEmpty && ( { if (paginatedBins.bins.length < paginatedBins.binsTotal) { loadMoreBins(contentFilter, paginatedBins.binsOffset); //setScrollTranslations({ ...scrollTranslations, empty: newTranslation }); } }} /> )} ); }; const grainBagsContent = () => { if (!allLoading && grainBags.length <= 0) { return ( No bags found Create a grain bag and it will appear here ); } // if (bagView === "list") { // return ( // { // let bag = data as GrainBag; // let path = "/grainbags/" + bag.key(); // history.push(path); // }} // /> // ); // } return ( ); }; const binMenu = () => { return ( { setMenuAnchorEl(null); setAddBinOpen(false); }} disableAutoFocusItem> setAddBinOpen(true)} aria-label="Create Bin" dense> ); }; const mobileViewCarousel = () => { let length = 2; let charts: JSX.Element[] = [ { let filter = ""; if (grain !== pond.Grain.GRAIN_NONE && grain !== pond.Grain.GRAIN_CUSTOM) { filter = pond.Grain[grain]; } else { filter = grainName; } setContentFilter(filter); loadMoreBins(filter); }} //activeGrain={grainFilter} />, { let filter = ""; if (grain !== pond.Grain.GRAIN_NONE && grain !== pond.Grain.GRAIN_CUSTOM) { filter = pond.Grain[grain]; } else { filter = grainName; } setContentFilter(filter); loadMoreBins(filter); }} customUnit={"L"} //activeGrain={grainFilter} /> ]; return ( {charts[carouselIndex]} ); }; const desktopInventory = () => { return ( { let filter = ""; if (grain !== pond.Grain.GRAIN_NONE && grain !== pond.Grain.GRAIN_CUSTOM) { filter = pond.Grain[grain]; } else { filter = grainName; } setContentFilter(filter); loadMoreBins(filter); }} //activeGrain={grainFilter} /> { let filter = ""; if (grain !== pond.Grain.GRAIN_NONE && grain !== pond.Grain.GRAIN_CUSTOM) { filter = pond.Grain[grain]; } else { filter = grainName; } setContentFilter(filter); loadMoreBins(filter); }} customUnit={"L"} // activeGrain={grainFilter} /> ); }; const totalInventory = () => { return ( { setExpandTotal(expanded); sessionStorage.setItem("expandBinTotal", expanded.toString()); }}> }> Total Inventory {isMobile ? mobileViewCarousel() : desktopInventory()} ); }; const binUtilizationList = () => { const hasInventory = binMetrics && binMetrics.grainInventory.length > 0; const useWeight = getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT; return ( { setExpandUtilization(expanded); sessionStorage.setItem("expandBinUtilization", expanded.toString()); }}> }> Bin Utilization {hasInventory ? ( Click a chart to filter bins by grain {binMetrics && binMetrics.grainInventory.map((inv, key) => ( { setContentFilter(pond.Grain[inv.grainType]); loadMoreBins(pond.Grain[inv.grainType]); }} grainActive={contentFilter === pond.Grain[inv.grainType]} /> ))} {binMetrics && binMetrics.customInventory.map((inv, key) => { //default to bushel values let amount = inv.amount; let cap = inv.capacity; let unit = " bu"; if (inv.storageType === pond.BinStorage.BIN_STORAGE_FERTILIZER) { amount = amount * 35.239; cap = cap * 35.239; unit = " L"; } if (useWeight && inv.bushelsPerTonne > 1) { amount = amount / inv.bushelsPerTonne; cap = cap / inv.bushelsPerTonne; unit = " mT"; } return ( { setContentFilter(inv.name); loadMoreBins(inv.name); }} grainActive={contentFilter === inv.name} customLabel={inv.name} customColour={stringToMaterialColour(inv.name)} /> ); })} ) : ( No active bins )} ); }; const binFanTable = () => { return ( { setExpandFanTable(expanded); sessionStorage.setItem("expandFanTable", expanded.toString()); }}> }> Fan Status{" "} {totalFanControllers > 0 && !isMobile ? " - " + totalFanControllersOn + " of " + totalFanControllers + " controllers on" : ""} { setTotalFanControllers(total); setTotalFanControllersOn(on); }} /> ); }; const binsHeader = () => { return ( { setBinMenuAnchorEl(null); }} disableAutoFocusItem> Order Order By { setDisplayGrain(checked); }} /> } label={Grain} /> { setDisplayFert(checked); }} /> } label={Fertilizer} /> { setDisplayEmpty(checked); }} /> } label={Empty Bins} /> { setOpenQrGenerator(true); }}> Generate QR Codes ); }; const binsByGrainType = () => { return ( Bins - ({paginatedBins.bins.length} of {paginatedBins.binsTotal}) {contentFilter && ( { setContentFilter(""); loadMoreBins(""); }} label={contentFilter} /> )} {(binView === "grid" || binView === "list") && paginatedBins.bins.length < paginatedBins.binsTotal && ( )} { setCardValDisplay("low"); }}> Low { setCardValDisplay("average"); }}> Average { setCardValDisplay("high"); }}> High { setBinView("grid"); sessionStorage.setItem("binsView", "grid"); }}> {/* hidden at dustins request so that grid view and list are the only two */} {/* { setBinView("scroll"); sessionStorage.setItem("binsView", "scroll"); }}> */} { setBinView("list"); sessionStorage.setItem("binsView", "list"); }}> { let target = event.currentTarget; setBinMenuAnchorEl(target); }} /> {binsLoading ? : binsContent()} {binsHeader()} ); }; const grainBagDisplay = () => { return ( Bags { setBagView("grid"); sessionStorage.setItem("bagsView", "grid"); }}> {/* hidden at dustins request so that grid view and list are the only two */} {/* { setBagView("scroll"); sessionStorage.setItem("bagsView", "scroll"); }}> */} { setBagView("list"); sessionStorage.setItem("bagsView", "list"); }}> { setAddBagOpen(true); }}> + {allLoading ? : grainBagsContent()} ); }; const { binsTotal } = paginatedBins; const page = () => { return ( {addALertsDialog()} { searchYards(search, limit, offset); }} grainBags={grainBags} setShowing={(val: "all" | "bins" | "bags") => { setShowing(val); }} /> {(showing === "all" || showing === "bins") && ( {binsByGrainType()} )} {allLoading ? : totalInventory()} {allLoading ? : binUtilizationList()} {allLoading ? : binFanTable()} {(showing === "all" || showing === "bags") && ( {grainBagDisplay()} )} { setAddBinOpen(false); if (refresh) { loadBins(); } }} /> { setAddBagOpen(false); }} /> {/* { setOpenQrGenerator(false); }} keyList={qrKeyList} requiredUrlAffix={"bins"} /> */} {binMenu()} setAddBinOpen(true)} pulse={binsTotal < 1 && !allLoading} /> ); }; return !props.insert ? {page()} : page(); }