frontend/src/pages/Bins.tsx

1480 lines
48 KiB
TypeScript

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 ButtonGroup from "common/ButtonGroup";
// 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<PaginatedBins>({
bins: [],
binsOffset: 0,
binsTotal: 0
});
const [binMetrics, setBinMetrics] = useState<pond.BinMetrics>();
const [binMenuAnchorEl, setBinMenuAnchorEl] = useState<Element | null>(null);
const [menuAnchorEl, setMenuAnchorEl] = useState<Element | null>(null);
const [contentFilter, setContentFilter] = useState<string>("");
const [yardFilter, setYardFilter] = useState<string>("");
const [order, setOrder] = useState<"asc" | "desc">("asc");
const [orderBy, setOrderBy] = useState<string>("name");
const binsLimit = 40;
const theme = useTheme();
const width = useWidth();
const [yards, setYards] = useState<pond.BinYardSettings[]>(props.yards ?? []);
const [yardPermissions, setYardPermissions] = useState<Dictionary<pond.Permission[]>>({});
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<Bin[]>([]);
const [fertilizerBins, setFertilizerBins] = useState<Bin[]>([]);
const [emptyBins, setEmptyBins] = useState<Bin[]>([]);
const [grainBags, setGrainBags] = useState<GrainBag[]>([]);
const [displayedInventory, setDisplayedInventory] = useState<GrainAmount[]>([]);
const [displayedFertilizer, setDisplayedFertilizer] = useState<GrainAmount[]>([]);
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<QrCodeKey[]>([]);
const [totalFanControllers, setTotalFanControllers] = useState(0);
const [totalFanControllersOn, setTotalFanControllersOn] = useState(0);
const [yardsLoading, setYardsLoading] = useState(false);
const [openAddAlerts, setOpenAddAlerts] = useState(false);
const [alertBins, setAlertBins] = useState<Bin[]>([]);
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<pond.Permission[]> = {};
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<pond.Permission[]> = {};
// //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 determineNodeToggle = () => {
switch(cardValDisplay) {
case "low":
return [0]
case "high":
return [2]
default:
return [1]
}
}
const duplicateBin = (bin: Bin) => {
binAPI.addBin(bin.settings, as).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, as)
.then(resp => {
openSnack("Successfully added " + resp.data.numAlertsSet + " alerts");
})
.catch(err => {
console.log("errors found");
});
};
const addALertsDialog = () => {
return (
<ResponsiveDialog open={openAddAlerts} onClose={() => setOpenAddAlerts(false)}>
<DialogTitle>Add Alert to Bin Cables</DialogTitle>
<DialogContent>
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
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenAddAlerts(false)}>Close</Button>
<Button
onClick={() => {
addAlertsToCables(alertBins);
setOpenAddAlerts(false);
}}
variant="contained"
color="primary">
Confirm
</Button>
</DialogActions>
</ResponsiveDialog>
);
};
const binsContent = () => {
const { binsTotal } = paginatedBins;
if (!allLoading && binsTotal <= 0) {
return (
<Box textAlign="center" marginTop={1}>
<Typography variant="subtitle1">No bins found</Typography>
<Typography variant="body1" color="textSecondary">
Create a bin and it will appear here
</Typography>
</Box>
);
}
if (binView === "list") {
return (
<ObjectTable
startingObject={pond.ObjectType.OBJECT_TYPE_BIN}
preLoadedData={paginatedBins.bins}
rowClickFunction={data => {
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 (
<React.Fragment>
<Grid container>
{displayGrain && (
<React.Fragment>
<Grid size={{ xs: 12 }}>
<Divider />
</Grid>
<Grid size={{ xs: 12 }}>
<BinsList
valDisplay={cardValDisplay}
gridView={binView === "grid"}
bins={grainBins}
duplicateBin={duplicateBin}
title={"Grain Bins"}
//startingTranslate={scrollTranslations.bins}
loadMore={() => {
if (paginatedBins.bins.length < paginatedBins.binsTotal) {
loadMoreBins(contentFilter, paginatedBins.binsOffset);
}
}}
/>
</Grid>
</React.Fragment>
)}
{displayFert && (
<React.Fragment>
<Grid size={{ xs: 12 }}>
<Divider />
</Grid>
<Grid size={{ xs: 12 }}>
<BinsList
valDisplay={cardValDisplay}
gridView={binView === "grid"}
bins={fertilizerBins}
duplicateBin={duplicateBin}
title={"Fertilizer Bins"}
//startingTranslate={scrollTranslations.fertilizer}
loadMore={() => {
if (paginatedBins.bins.length < paginatedBins.binsTotal) {
loadMoreBins(contentFilter, paginatedBins.binsOffset);
//setScrollTranslations({ ...scrollTranslations, fertilizer: newTranslation });
}
}}
/>
</Grid>
</React.Fragment>
)}
{displayEmpty && (
<React.Fragment>
<Grid size={{ xs: 12 }}>
<Divider />
</Grid>
<Grid size={{ xs: 12 }}>
<BinsList
valDisplay={cardValDisplay}
gridView={binView === "grid"}
bins={emptyBins}
duplicateBin={duplicateBin}
title={"Empty Bins"}
//startingTranslate={scrollTranslations.empty}
loadMore={() => {
if (paginatedBins.bins.length < paginatedBins.binsTotal) {
loadMoreBins(contentFilter, paginatedBins.binsOffset);
//setScrollTranslations({ ...scrollTranslations, empty: newTranslation });
}
}}
/>
</Grid>
</React.Fragment>
)}
</Grid>
</React.Fragment>
);
};
const grainBagsContent = () => {
if (!allLoading && grainBags.length <= 0) {
return (
<Box textAlign="center" marginTop={1}>
<Typography variant="subtitle1">No bags found</Typography>
<Typography variant="body1" color="textSecondary">
Create a grain bag and it will appear here
</Typography>
</Box>
);
}
// if (bagView === "list") {
// return (
// <ObjectTable
// startingObject={pond.ObjectType.OBJECT_TYPE_GRAIN_BAG}
// preLoadedData={grainBags}
// rowClickFunction={data => {
// let bag = data as GrainBag;
// let path = "/grainbags/" + bag.key();
// history.push(path);
// }}
// />
// );
// }
return (
<React.Fragment>
<Grid container>
<React.Fragment>
<Grid size={{ xs: 12 }}>
<Divider />
</Grid>
<Grid size={{ xs: 12 }}>
<GrainBagList
gridView={bagView === "grid"}
grainBags={grainBags}
title={"Grain Bags"}
/>
</Grid>
</React.Fragment>
</Grid>
</React.Fragment>
);
};
const binMenu = () => {
return (
<Menu
id="binMenu"
anchorEl={menuAnchorEl ? menuAnchorEl : null}
open={menuAnchorEl !== null}
onClose={() => {
setMenuAnchorEl(null);
setAddBinOpen(false);
}}
disableAutoFocusItem>
<MenuItem onClick={() => setAddBinOpen(true)} aria-label="Create Bin" dense>
<ListItemIcon>
<LibraryAdd className={classes.green} />
</ListItemIcon>
<ListItemText secondary="Create Bin" />
</MenuItem>
</Menu>
);
};
const mobileViewCarousel = () => {
let length = 2;
let charts: JSX.Element[] = [
<BinInventoryChart
customLabel="Grain"
inventory={displayedInventory}
onClick={(grain: pond.Grain, grainName: string) => {
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}
/>,
<BinInventoryChart
customLabel="Fertilizer"
inventory={displayedFertilizer}
onClick={(grain: pond.Grain, grainName: string) => {
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 (
<React.Fragment>
<Grid container direction="row" alignContent="center" alignItems="center">
<Grid size={{ xs: 2 }}>
<Button
style={{ height: 160 }}
onClick={() => {
const newIndex = carouselIndex - 1;
setCarouselIndex(newIndex < 0 ? length - 1 : newIndex);
}}>
<ArrowBackIos />
</Button>
</Grid>
<Grid size={{ xs: 8 }}>
<Box height={"180px"} flexDirection="row" display="flex">
{charts[carouselIndex]}
</Box>
</Grid>
<Grid size={{ xs: 2 }}>
<Button
style={{ height: 160 }}
onClick={() => {
const newIndex = carouselIndex + 1;
setCarouselIndex(newIndex >= length ? 0 : newIndex);
}}>
<ArrowForwardIos />
</Button>
</Grid>
</Grid>
</React.Fragment>
);
};
const desktopInventory = () => {
return (
<React.Fragment>
<Grid container direction="row" alignContent="center" alignItems="center">
<Grid style={{ height: "200px" }} size={{ xs: 6 }}>
<BinInventoryChart
customLabel="Grain"
inventory={displayedInventory}
onClick={(grain: pond.Grain, grainName: string) => {
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}
/>
</Grid>
<Grid style={{ height: "200px" }} size={{ xs: 6 }}>
<BinInventoryChart
customLabel="Fertilizer"
inventory={displayedFertilizer}
onClick={(grain: pond.Grain, grainName: string) => {
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}
/>
</Grid>
</Grid>
</React.Fragment>
);
};
const totalInventory = () => {
return (
<Accordion
className={classes.accordion}
expanded={expandTotal}
onChange={(_, expanded) => {
setExpandTotal(expanded);
sessionStorage.setItem("expandBinTotal", expanded.toString());
}}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography variant="h6" style={{ fontWeight: 650 }}>
Total Inventory
</Typography>
</AccordionSummary>
<AccordionDetails>{isMobile ? mobileViewCarousel() : desktopInventory()}</AccordionDetails>
</Accordion>
);
};
const binUtilizationList = () => {
const hasInventory = binMetrics && binMetrics.grainInventory.length > 0;
const useWeight = getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT;
return (
<Accordion
className={classes.accordion}
style={{ marginTop: 5 }}
expanded={expandUtilization}
onChange={(_, expanded) => {
setExpandUtilization(expanded);
sessionStorage.setItem("expandBinUtilization", expanded.toString());
}}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography variant="h6" style={{ fontWeight: 650 }}>
Bin Utilization
</Typography>
</AccordionSummary>
<AccordionDetails>
{hasInventory ? (
<Box width={"100%"}>
<Typography variant="caption" color="textSecondary">
Click a chart to filter bins by grain
</Typography>
<ImageList
className={classes.gridList}
cols={
props.insert && !isMobile
? 3
: width === "xs"
? 3
: width === "sm"
? 5
: width === "md"
? 6
: width === "lg"
? 7
: 8
}
>
{binMetrics &&
binMetrics.grainInventory.map((inv, key) => (
<ImageListItem key={key}>
<BinUtilizationChart
grain={inv.grainType}
customUnit={useWeight ? " mT" : " bu"}
bushelAmount={
useWeight
? inv.bushelAmount / GrainDescriber(inv.grainType).bushelsPerTonne
: inv.bushelAmount
}
bushelCapacity={
useWeight
? inv.bushelCapacity / GrainDescriber(inv.grainType).bushelsPerTonne
: inv.bushelCapacity
}
onClick={() => {
setContentFilter(pond.Grain[inv.grainType]);
loadMoreBins(pond.Grain[inv.grainType]);
}}
grainActive={contentFilter === pond.Grain[inv.grainType]}
/>
</ImageListItem>
))}
{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 (
<ImageListItem key={key}>
<BinUtilizationChart
grain={pond.Grain.GRAIN_CUSTOM}
bushelAmount={amount}
bushelCapacity={cap}
customUnit={unit}
onClick={() => {
setContentFilter(inv.name);
loadMoreBins(inv.name);
}}
grainActive={contentFilter === inv.name}
customLabel={inv.name}
customColour={stringToMaterialColour(inv.name)}
/>
</ImageListItem>
);
})}
</ImageList>
</Box>
) : (
<Typography color="textSecondary" style={{ margin: "2rem", marginTop: "0rem" }}>
No active bins
</Typography>
)}
</AccordionDetails>
</Accordion>
);
};
const binFanTable = () => {
return (
<Accordion
className={classes.accordion}
expanded={expandFanTable}
style={{ marginTop: 5 }}
onChange={(_, expanded) => {
setExpandFanTable(expanded);
sessionStorage.setItem("expandFanTable", expanded.toString());
}}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography variant="h6" style={{ fontWeight: 650 }}>
Fan Status{" "}
{totalFanControllers > 0 && !isMobile
? " - " + totalFanControllersOn + " of " + totalFanControllers + " controllers on"
: ""}
</Typography>
</AccordionSummary>
<AccordionDetails>
<BinsFansStatusTable
bins={paginatedBins.bins}
isLoading={binsLoading}
getControllerInfo={(total, on) => {
setTotalFanControllers(total);
setTotalFanControllersOn(on);
}}
/>
</AccordionDetails>
</Accordion>
);
};
const binsHeader = () => {
return (
<Menu
id="yardMenu"
anchorEl={binMenuAnchorEl ? binMenuAnchorEl : null}
open={binMenuAnchorEl !== null}
onClose={() => {
setBinMenuAnchorEl(null);
}}
disableAutoFocusItem>
<MenuItem>
<FormControl style={{ marginLeft: theme.spacing(2) }}>
<InputLabel id="demo-simple-select-label">Order</InputLabel>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={order}
onChange={event => setOrder(event.target.value as SetStateAction<"asc" | "desc">)}>
<MenuItem value={"asc"}>Ascending</MenuItem>
<MenuItem value={"desc"}>Descending</MenuItem>
</Select>
</FormControl>
</MenuItem>
<MenuItem>
<FormControl style={{ marginLeft: theme.spacing(2) }}>
<InputLabel id="demo-simple-select-label">Order By</InputLabel>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={orderBy}
onChange={event => setOrderBy(event.target.value as SetStateAction<string>)}>
<MenuItem value={"name"}>Name</MenuItem>
<MenuItem value={"timestamp"}>Timestamp</MenuItem>
</Select>
</FormControl>
</MenuItem>
<Divider />
<MenuItem>
<FormControlLabel
control={
<Checkbox
checked={displayGrain}
onChange={(e, checked) => {
setDisplayGrain(checked);
}}
/>
}
label={<Typography>Grain</Typography>}
/>
</MenuItem>
<MenuItem>
<FormControlLabel
control={
<Checkbox
checked={displayFert}
onChange={(e, checked) => {
setDisplayFert(checked);
}}
/>
}
label={<Typography>Fertilizer</Typography>}
/>
</MenuItem>
<MenuItem>
<FormControlLabel
control={
<Checkbox
checked={displayEmpty}
onChange={(e, checked) => {
setDisplayEmpty(checked);
}}
/>
}
label={<Typography>Empty Bins</Typography>}
/>
</MenuItem>
<MenuItem
onClick={() => {
setOpenQrGenerator(true);
}}>
<Typography>Generate QR Codes</Typography>
</MenuItem>
</Menu>
);
};
const binsByGrainType = () => {
return (
<Box width={1} marginBottom={1}>
<Grid
container
direction="row"
alignContent="center"
alignItems="center"
justifyContent="space-between">
<Grid >
<Box display="flex" paddingY={1} alignContent="center" alignItems="center">
<Typography variant="h6" style={{ fontWeight: 650 }}>
Bins - ({paginatedBins.bins.length} of {paginatedBins.binsTotal})
</Typography>
{contentFilter && (
<Box marginLeft={1}>
<Chip
size="small"
onDelete={() => {
setContentFilter("");
loadMoreBins("");
}}
label={contentFilter}
/>
</Box>
)}
{(binView === "grid" || binView === "list") &&
paginatedBins.bins.length < paginatedBins.binsTotal && (
<Button
style={{ marginLeft: 10 }}
variant="contained"
color="primary"
onClick={() => {
loadMoreBins(
contentFilter,
paginatedBins.binsOffset,
paginatedBins.binsTotal - paginatedBins.bins.length
);
}}>
Load All
</Button>
)}
<ButtonGroup
toggle
toggledButtons={determineNodeToggle()}
buttons={[
{
title: "Low",
function: () => {setCardValDisplay("low")}
},
{
title: "Average",
function: () => {setCardValDisplay("average")}
},
{
title: "High",
function: () => {setCardValDisplay("high")}
}
]}
/>
{/* <StyledToggleButtonGroup
id="cardValueDisplay"
value={cardValDisplay}
exclusive
size="small"
aria-label="card detail">
<StyledToggle
value={"low"}
aria-label="low"
onClick={() => {
setCardValDisplay("low");
}}>
Low
</StyledToggle>
<StyledToggle
value={"average"}
aria-label="average"
onClick={() => {
setCardValDisplay("average");
}}>
Average
</StyledToggle>
<StyledToggle
value={"high"}
aria-label="high"
onClick={() => {
setCardValDisplay("high");
}}>
High
</StyledToggle>
</StyledToggleButtonGroup> */}
</Box>
</Grid>
<Grid >
<Box display="flex" paddingY={1} alignContent="center" alignItems="center">
<ButtonGroup
toggle
buttons={[
{
title: "Grid",
icon: <ViewComfy />,
function: () => {
setBinView("grid");
sessionStorage.setItem("binsView", "grid");
}
},
{
title: "List",
icon: <ViewList />,
function: () => {
setBinView("list");
sessionStorage.setItem("binsView", "list");
}
}
]}
/>
{/* <StyledToggleButtonGroup
id="tour-graph-tabs"
value={binView}
exclusive
size="small"
aria-label="detail">
<StyledToggle
value={"grid"}
aria-label="grid view"
onClick={() => {
setBinView("grid");
sessionStorage.setItem("binsView", "grid");
}}>
<ViewComfy />
</StyledToggle> */}
{/* hidden at dustins request so that grid view and list are the only two */}
{/* <StyledToggle
value={"scroll"}
aria-label="scroll view"
onClick={() => {
setBinView("scroll");
sessionStorage.setItem("binsView", "scroll");
}}>
<ViewColumn />
</StyledToggle> */}
{/* <StyledToggle
value={"list"}
aria-label="list view"
onClick={() => {
setBinView("list");
sessionStorage.setItem("binsView", "list");
}}>
<ViewList />
</StyledToggle>
</StyledToggleButtonGroup> */}
<MoreVert
className={classes.icon}
onClick={event => {
let target = event.currentTarget;
setBinMenuAnchorEl(target);
}}
/>
</Box>
</Grid>
</Grid>
{binsLoading ? <Skeleton variant="rectangular" height="200px" /> : binsContent()}
{binsHeader()}
</Box>
);
};
const grainBagDisplay = () => {
return (
<Box width={1} marginBottom={1}>
<Grid
container
direction="row"
alignContent="center"
alignItems="center"
justifyContent="space-between">
<Grid size={{ xs: 12 }}>
<Divider />
</Grid>
<Grid >
<Box display="flex" paddingY={1} alignContent="center" alignItems="center">
<Typography variant="h6" style={{ fontWeight: 650 }}>
Bags
</Typography>
</Box>
</Grid>
<Grid >
<Box display="flex" paddingY={1} alignContent="center" alignItems="center">
<ButtonGroup
toggle
buttons={[
{
title: "Grid",
icon: <ViewComfy />,
function: () => {
setBagView("grid");
sessionStorage.setItem("bagsView", "grid");
}
},
{
title: "List",
icon: <ViewList />,
function: () => {
setBagView("list");
sessionStorage.setItem("bagsView", "list");
}
}
]}
/>
{/* <StyledToggleButtonGroup
id="tour-graph-tabs"
value={bagView}
exclusive
size="small"
aria-label="detail">
<StyledToggle
value={"grid"}
aria-label="grid view"
onClick={() => {
setBagView("grid");
sessionStorage.setItem("bagsView", "grid");
}}>
<ViewComfy />
</StyledToggle> */}
{/* hidden at dustins request so that grid view and list are the only two */}
{/* <StyledToggle
value={"scroll"}
aria-label="scroll view"
onClick={() => {
setBagView("scroll");
sessionStorage.setItem("bagsView", "scroll");
}}>
<ViewColumn />
</StyledToggle> */}
{/* <StyledToggle
value={"list"}
aria-label="list view"
onClick={() => {
setBagView("list");
sessionStorage.setItem("bagsView", "list");
}}>
<ViewList />
</StyledToggle>
</StyledToggleButtonGroup> */}
<IconButton
className={classes.icon}
onClick={() => {
setAddBagOpen(true);
}}>
+
</IconButton>
</Box>
</Grid>
</Grid>
{allLoading ? <Skeleton variant="rectangular" height="200px" /> : grainBagsContent()}
</Box>
);
};
const { binsTotal } = paginatedBins;
const page = () => {
return (
<Box>
{addALertsDialog()}
<BinYards
yards={yards}
yardsLoading={yardsLoading}
yardPerms={yardPermissions}
showTabs={!props.insert}
setYardFilter={setYardFilter}
bins={paginatedBins.bins}
loadBins={loadBins}
loadYards={(search, limit, offset) => {
searchYards(search, limit, offset);
}}
grainBags={grainBags}
setShowing={(val: "all" | "bins" | "bags") => {
setShowing(val);
}}
/>
<Box height={1}>
<Box
paddingY={isMobile || props.insert ? 0.5 : 1}
paddingX={isMobile || props.insert ? 1 : 2}>
<Grid container>
{(showing === "all" || showing === "bins") && (
<Grid size={{ xs: 12 }}>
{binsByGrainType()}
</Grid>
)}
<Grid size={{ xs: 12 }}>
{allLoading ? <Skeleton variant="rectangular" height="200px" /> : totalInventory()}
</Grid>
<Grid size={{ xs: 12 }}>
<Divider />
</Grid>
<Grid size={{ xs: 12 }}>
{allLoading ? <Skeleton variant="rectangular" height="200px" /> : binUtilizationList()}
</Grid>
<Grid size={{ xs: 12 }}>
<Divider />
</Grid>
<Grid size={{ xs: 12 }}>
{allLoading ? <Skeleton variant="rectangular" height="200px" /> : binFanTable()}
</Grid>
<Grid size={{ xs: 12 }}>
<Divider />
</Grid>
{(showing === "all" || showing === "bags") && (
<Grid size={{ xs: 12 }}>
{grainBagDisplay()}
</Grid>
)}
</Grid>
</Box>
</Box>
<BinSettings
mode="add"
canEdit={true}
open={addBinOpen}
userID={user.id()}
openedBinYard={yardFilter}
binYards={yards}
onClose={refresh => {
setAddBinOpen(false);
if (refresh) {
loadBins();
}
}}
/>
<GrainBagSettings
open={addBagOpen}
close={() => {
setAddBagOpen(false);
}}
/>
<QrCodeGenerator
open={openQrGenerator}
close={() => {
setOpenQrGenerator(false);
}}
keyList={qrKeyList}
requiredUrlAffix={"bins"}
/>
{binMenu()}
<AddBinFab onClick={() => setAddBinOpen(true)} pulse={binsTotal < 1 && !allLoading} />
</Box>
);
};
return !props.insert ? <PageContainer padding={-1}>{page()}</PageContainer> : page();
}