ag visual farm added to the dev branch field related stuff not imported yet, and the geocoder has some bugs with the new version

This commit is contained in:
csawatzky 2025-03-26 14:38:31 -06:00
parent b4da0d6859
commit 022925d7d7
41 changed files with 8895 additions and 79 deletions

833
src/bin/BinyardDisplay.tsx Normal file
View file

@ -0,0 +1,833 @@
import {
Accordion,
AccordionDetails,
AccordionSummary,
Box,
Button,
Checkbox,
Chip,
darken,
Divider,
FormControl,
FormControlLabel,
Grid2 as Grid,
ImageList as GridList,
ImageListItem as GridListTile,
InputLabel,
Menu,
MenuItem,
Select,
Skeleton,
Theme,
Typography,
useTheme
} from "@mui/material";
import { green, yellow } from "@mui/material/colors";
import { makeStyles } from "@mui/styles";
import { ArrowBackIos, ArrowForwardIos, ExpandMore, MoreVert } from "@mui/icons-material";
//import { Skeleton } from "@material-ui/lab";
import BinInventoryChart, { GrainAmount } from "charts/BinInventoryChart";
import BinUtilizationChart from "charts/BinUtilizationChart";
//import QrCodeGenerator, { QrCodeKey } from "common/QrCodeGenerator";
import GrainDescriber, { grainName } from "grain/GrainDescriber";
import { useMobile, useWidth } from "hooks";
import { Bin } from "models";
import { pond } from "protobuf-ts/pond";
import { useBinAPI, useGlobalState } from "providers";
import React, { useEffect, useState, useCallback, SetStateAction } from "react";
import { getGrainUnit, stringToMaterialColour } from "utils";
//import BinsFansStatusTable from "./BinFansStatusTable";
import BinsList from "./BinsList";
interface Props {
yardKey: string;
insert?: boolean;
}
const useStyles = makeStyles((theme: Theme) => ({
"@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: darken(theme.palette.background.paper, 0.4)
}
}));
export default function BinyardDisplay(props: Props) {
const { yardKey, insert } = props;
const classes = useStyles();
const binAPI = useBinAPI();
const theme = useTheme();
const width = useWidth();
const isMobile = useMobile();
const [carouselIndex, setCarouselIndex] = useState(0);
const [binMenuAnchorEl, setBinMenuAnchorEl] = useState<Element | null>(null);
const [{ as }] = useGlobalState();
const maxBins = 40;
const [binsLoading, setBinsLoading] = useState(false);
const [expandTotal, setExpandTotal] = useState(false);
const [displayedInventory, setDisplayedInventory] = useState<GrainAmount[]>([]);
const [displayedFertilizer, setDisplayedFertilizer] = useState<GrainAmount[]>([]);
const [expandUtilization, setExpandUtilization] = useState(false);
const [expandFanTable, setExpandFanTable] = useState(false);
const [displayGrain, setDisplayGrain] = useState(true);
const [displayFert, setDisplayFert] = useState(false);
const [displayEmpty, setDisplayEmpty] = useState(false);
const [yardBins, setYardBins] = useState<Bin[]>([]);
const [grainBins, setGrainBins] = useState<Bin[]>([]);
const [fertBins, setFertBins] = useState<Bin[]>([]);
const [emptyBins, setEmptyBins] = useState<Bin[]>([]);
const [binTotal, setBinTotal] = useState(0);
const [binMetrics, setBinMetrics] = useState<pond.BinMetrics>();
const [contentFilter, setContentFilter] = useState<string>("");
const [totalFanControllers, setTotalFanControllers] = useState(0);
const [totalFanControllersOn, setTotalFanControllersOn] = useState(0);
const [openQrGenerator, setOpenQrGenerator] = useState(false);
const [order, setOrder] = useState<"asc" | "desc">("asc");
const [orderBy, setOrderBy] = useState<string>("name");
//const [qrKeyList, setQrKeyList] = useState<QrCodeKey[]>([]);
const [scrollTranslations, setScrollTranslations] = useState({
bins: 0,
empty: 0,
fertilizer: 0
});
const loadBins = useCallback(() => {
setBinsLoading(true);
//load the bins based on the yard key
binAPI
.listBinsAndData(maxBins, 0, "asc", "name", undefined, as, undefined, [yardKey], ["binyard"])
.then(resp => {
let yardBins: Bin[] = [];
let grainBins: Bin[] = [];
let fertBins: Bin[] = [];
let emptyBins: Bin[] = [];
//let qr: QrCodeKey[] = [];
//set the list of bins into the yard bins
resp.data.bins.forEach(b => {
let bin = Bin.any(b);
yardBins.push(bin);
if (bin.empty()) {
emptyBins.push(bin);
} else if (bin.storage() === pond.BinStorage.BIN_STORAGE_FERTILIZER) {
fertBins.push(bin);
} else {
grainBins.push(bin);
}
//build qr keys here
// qr.push({
// key: bin.key(),
// name: bin.name()
// });
});
setYardBins(yardBins);
setBinTotal(resp.data.total);
//setQrKeyList(qr);
setGrainBins(grainBins);
if (fertBins.length > 0) {
setDisplayFert(true);
}
if (emptyBins.length > 0) {
setDisplayEmpty(true);
}
setFertBins(fertBins);
setEmptyBins(emptyBins);
setBinsLoading(false);
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);
});
}, [as, binAPI, yardKey]);
const loadMoreBins = (filter: string, total?: number, offset?: number) => {
setBinsLoading(true);
binAPI
.listBins(
total ?? maxBins,
offset ?? 0,
order,
orderBy,
filter,
as,
undefined,
undefined,
[yardKey],
["binyard"]
)
.then(resp => {
let bins = yardBins;
//if the offset was 0 or undefined we started from the beginning so replace the yard bins with the new ones
if (!offset || offset === 0) {
bins = resp.data.bins.map(b => Bin.any(b));
//else append the new ones on to the end of the existing yard bins
} else {
bins = bins.concat(resp.data.bins.map(b => Bin.any(b)));
}
//loop through the new list of bins setting the bins into the correct sections
let grainBins: Bin[] = [];
let fertBins: Bin[] = [];
let emptyBins: Bin[] = [];
//let qr: QrCodeKey[] = [];
//set the list of bins into the yard bins
resp.data.bins.forEach(b => {
let bin = Bin.any(b);
yardBins.push(bin);
if (bin.empty()) {
emptyBins.push(bin);
} else if (bin.storage() === pond.BinStorage.BIN_STORAGE_FERTILIZER) {
fertBins.push(bin);
} else {
grainBins.push(bin);
}
//build qr keys here
// qr.push({
// key: bin.key(),
// name: bin.name()
// });
});
setYardBins(bins);
setBinTotal(resp.data.total);
//setQrKeyList(qr);
setGrainBins(grainBins);
if (fertBins.length > 0) {
setDisplayFert(true);
}
if (emptyBins.length > 0) {
setDisplayEmpty(true);
}
setFertBins(fertBins);
setEmptyBins(emptyBins);
setBinsLoading(false);
});
};
useEffect(() => {
loadBins();
}, [yardKey, loadBins]);
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 fanTable = sessionStorage.getItem("expandFanTable");
if (fanTable === "true") {
setExpandFanTable(true);
} else {
setExpandFanTable(false);
}
}, []);
const duplicateBin = (bin: Bin) => {
binAPI.addBin(bin.settings).then(resp => {
loadBins();
});
};
const binsContent = () => {
if (!binsLoading && binTotal === 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>
);
}
return (
<React.Fragment>
<Grid container>
{displayGrain && (
<React.Fragment>
<Grid size={12}>
<Divider />
</Grid>
<Grid size={12}>
<BinsList
gridView
bins={grainBins}
duplicateBin={duplicateBin}
title={"Grain Bins"}
// because this only triggers in scroll view it is no longer necessary as we dont do the scroll here
// loadMore={newTranslation => {
// //only triggered in the scroll view so this will never trigger in grid view
// if (yardBins.length < binTotal) {
// loadMoreBins(contentFilter);
// setScrollTranslations({ ...scrollTranslations, bins: newTranslation });
// }
// }}
/>
</Grid>
</React.Fragment>
)}
{displayFert && (
<React.Fragment>
<Grid size={12}>
<Divider />
</Grid>
<Grid size={12}>
<BinsList
gridView
bins={fertBins}
duplicateBin={duplicateBin}
title={"Fertilizer Bins"}
// loadMore={newTranslation => {
// //only triggered in the scroll view so this will never trigger in grid view
// if (yardBins.length < binTotal) {
// loadMoreBins(contentFilter);
// setScrollTranslations({ ...scrollTranslations, fertilizer: newTranslation });
// }
// }}
/>
</Grid>
</React.Fragment>
)}
{displayEmpty && (
<React.Fragment>
<Grid size={12}>
<Divider />
</Grid>
<Grid size={12}>
<BinsList
gridView
bins={emptyBins}
duplicateBin={duplicateBin}
title={"Empty Bins"}
// loadMore={newTranslation => {
// //only triggered in the scroll view so this will never trigger in grid view
// if (yardBins.length < binTotal) {
// loadMoreBins(contentFilter);
// setScrollTranslations({ ...scrollTranslations, empty: newTranslation });
// }
// }}
/>
</Grid>
</React.Fragment>
)}
</Grid>
</React.Fragment>
);
};
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">
<Grid>
<Box display="flex" paddingY={1} alignContent="center" alignItems="center">
<Typography variant="h6" style={{ fontWeight: 650 }}>
Bins - ({yardBins.length} of {binTotal})
</Typography>
{contentFilter && (
<Box marginLeft={1}>
<Chip
size="small"
onDelete={() => {
setContentFilter("");
loadMoreBins("");
}}
label={contentFilter}
/>
</Box>
)}
{yardBins.length < binTotal && (
<Button
style={{ marginLeft: 10 }}
variant="contained"
color="primary"
onClick={() => {
loadMoreBins(contentFilter, 0, yardBins.length);
}}>
Load All
</Button>
)}
</Box>
</Grid>
<Grid>
<MoreVert
className={classes.icon}
onClick={event => {
let target = event.currentTarget;
setBinMenuAnchorEl(target);
}}
/>
</Grid>
</Grid>
{binsLoading ? <Skeleton variant="rectangular" height="200px" /> : binsContent()}
{binsHeader()}
</Box>
);
};
const desktopInventory = () => {
return (
<React.Fragment>
<Grid container direction="row" alignContent="center" alignItems="center">
<Grid style={{ height: "200px" }} size={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={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 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={2}>
<Button
style={{ height: 160 }}
onClick={() => {
const newIndex = carouselIndex - 1;
setCarouselIndex(newIndex < 0 ? length - 1 : newIndex);
}}>
<ArrowBackIos />
</Button>
</Grid>
<Grid size={8}>
<Box height={"180px"} flexDirection="row" display="flex">
{charts[carouselIndex]}
</Box>
</Grid>
<Grid size={2}>
<Button
style={{ height: 160 }}
onClick={() => {
const newIndex = carouselIndex + 1;
setCarouselIndex(newIndex >= length ? 0 : newIndex);
}}>
<ArrowForwardIos />
</Button>
</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 || insert ? 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>
<GridList
className={classes.gridList}
cols={
props.insert && !isMobile
? 3.25
: width === "xs"
? 3.25
: width === "sm"
? 5.5
: width === "md"
? 6.5
: width === "lg"
? 7.5
: 8.5
}>
{binMetrics &&
binMetrics.grainInventory.map((inv, key) => (
<GridListTile 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]}
/>
</GridListTile>
))}
{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 (
<GridListTile 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)}
/>
</GridListTile>
);
})}
</GridList>
</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={yardBins}
isLoading={binsLoading}
getControllerInfo={(total, on) => {
setTotalFanControllers(total);
setTotalFanControllersOn(on);
}}
/> */}
</AccordionDetails>
</Accordion>
);
};
return (
<Box height={1}>
<Box paddingY={0.5} paddingX={1}>
<Grid container>
<Grid size={12}>
{binsByGrainType()}
</Grid>
<Grid size={12}>
{binsLoading ? <Skeleton variant="rectangular" height="200px" /> : totalInventory()}
</Grid>
<Grid size={12}>
<Divider />
</Grid>
<Grid size={12}>
{binsLoading ? <Skeleton variant="rectangular" height="200px" /> : binUtilizationList()}
</Grid>
<Grid size={12}>
<Divider />
</Grid>
<Grid size={12}>
{binsLoading ? <Skeleton variant="rectangular" height="200px" /> : binFanTable()}
</Grid>
<Grid size={12}>
<Divider />
</Grid>
</Grid>
</Box>
{/* <QrCodeGenerator
open={openQrGenerator}
close={() => {
setOpenQrGenerator(false);
}}
keyList={qrKeyList}
requiredUrlAffix={"bins"}
/> */}
</Box>
);
}