From 235464bb937a3f85e2d209b3a380460d1a1391e4 Mon Sep 17 00:00:00 2001 From: Carter Date: Tue, 11 Mar 2025 13:35:15 -0600 Subject: [PATCH] added binyard and grainbag stuff --- src/grainBag/grainBagActions.tsx | 207 +++++++++++ src/grainBag/grainBagCard.tsx | 85 +++++ src/grainBag/grainBagInventoryGraph.tsx | 106 ++++++ src/grainBag/grainBagList.tsx | 92 +++++ src/grainBag/grainBagSVG.tsx | 78 ++++ src/grainBag/grainBagSettings.tsx | 456 ++++++++++++++++++++++++ src/grainBag/grainBagVisualizer.tsx | 322 +++++++++++++++++ src/models/BinYard.ts | 38 ++ src/models/GrainBag.ts | 135 +++++++ src/models/index.ts | 2 +- src/pages/Bins.tsx | 65 ++-- 11 files changed, 1553 insertions(+), 33 deletions(-) create mode 100644 src/grainBag/grainBagActions.tsx create mode 100644 src/grainBag/grainBagCard.tsx create mode 100644 src/grainBag/grainBagInventoryGraph.tsx create mode 100644 src/grainBag/grainBagList.tsx create mode 100644 src/grainBag/grainBagSVG.tsx create mode 100644 src/grainBag/grainBagSettings.tsx create mode 100644 src/grainBag/grainBagVisualizer.tsx create mode 100644 src/models/BinYard.ts create mode 100644 src/models/GrainBag.ts diff --git a/src/grainBag/grainBagActions.tsx b/src/grainBag/grainBagActions.tsx new file mode 100644 index 0000000..26a2708 --- /dev/null +++ b/src/grainBag/grainBagActions.tsx @@ -0,0 +1,207 @@ +import { + createStyles, + IconButton, + ListItemIcon, + ListItemText, + makeStyles, + Menu, + MenuItem, + Tooltip +} from "@material-ui/core"; +import SettingsIcon from "@material-ui/icons/Settings"; +import MoreIcon from "@material-ui/icons/MoreVert"; +import React, { useState } from "react"; +import ObjectUsersIcon from "@material-ui/icons/AccountCircle"; +import ObjectTeamsIcon from "@material-ui/icons/SupervisedUserCircle"; +import ObjectUsers from "user/ObjectUsers"; +import { pond } from "protobuf-ts/pond"; +import { Scope } from "models"; +import ObjectTeams from "teams/ObjectTeams"; +import RemoveSelfFromObject from "user/RemoveSelfFromObject"; +import ShareObject from "user/ShareObject"; +import { blue } from "@material-ui/core/colors"; +import RemoveSelfIcon from "@material-ui/icons/ExitToApp"; +import { Share } from "@material-ui/icons"; +import { GrainBag } from "models/GrainBag"; +import GrainBagSettings from "./grainBagSettings"; + +const useStyles = makeStyles(() => { + return createStyles({ + shareIcon: { + color: blue["500"], + "&:hover": { + color: blue["600"] + } + }, + removeIcon: { + color: "var(--status-alert)" + }, + red: { + color: "var(--status-alert)" + }, + blueIcon: { + color: blue["500"], + "&:hover": { + color: blue["600"] + } + } + }); +}); +interface OpenState { + users: boolean; + teams: boolean; + settings: boolean; + removeSelf: boolean; + share: boolean; +} + +interface Props { + grainBag: GrainBag; + refreshCallback: (updatedBag: GrainBag) => void; + permissions: pond.Permission[]; +} + +export default function GrainBagActions(props: Props) { + const classes = useStyles(); + const { grainBag, refreshCallback, permissions } = props; + const [anchorEl, setAnchorEl] = React.useState(null); + const [openState, setOpenState] = useState({ + users: false, + teams: false, + settings: false, + removeSelf: false, + share: false + }); + + const groupMenu = () => { + return ( + setAnchorEl(null)} + keepMounted + disableAutoFocusItem> + { + setOpenState({ ...openState, share: true }); + setAnchorEl(null); + }} + button + dense> + + + + + + { + setOpenState({ ...openState, users: true }); + setAnchorEl(null); + }} + button> + + + + + + { + setOpenState({ ...openState, teams: true }); + setAnchorEl(null); + }} + button> + + + + + + { + setOpenState({ ...openState, removeSelf: true }); + setAnchorEl(null); + }} + button> + + + + + + + ); + }; + + const dialogs = () => { + const key = grainBag.key; + const label = grainBag.name(); + return ( + + { + if (updatedBag) { + refreshCallback(updatedBag); + } + setOpenState({ ...openState, settings: false }); + }} + /> + setOpenState({ ...openState, share: false })} + /> + setOpenState({ ...openState, users: false })} + refreshCallback={refreshCallback} + /> + setOpenState({ ...openState, teams: false })} + refreshCallback={refreshCallback} + /> + { + setOpenState({ ...openState, removeSelf: false }); + }} + /> + + ); + }; + + return ( + + {permissions.includes(pond.Permission.PERMISSION_WRITE) && ( + + setOpenState({ ...openState, settings: true })}> + + + + )} + ) => setAnchorEl(event.currentTarget)}> + + + {dialogs()} + {groupMenu()} + + ); +} diff --git a/src/grainBag/grainBagCard.tsx b/src/grainBag/grainBagCard.tsx new file mode 100644 index 0000000..51f055e --- /dev/null +++ b/src/grainBag/grainBagCard.tsx @@ -0,0 +1,85 @@ +import { Box, Card, Typography } from "@material-ui/core"; +import GrainDescriber from "grain/GrainDescriber"; +import { GrainBag } from "models/GrainBag"; +import moment from "moment"; +import React, { useEffect, useState } from "react"; +import GrainBagSVG from "./grainBagSVG"; + +interface Props { + grainBag: GrainBag; +} + +export default function GrainBagCard(props: Props) { + const { grainBag } = props; + const [fillLevel, setFillLevel] = useState(0); + + useEffect(() => { + setFillLevel((grainBag.bushels() / grainBag.capacity()) * 100); + }, [grainBag]); + + const timeDisplay = () => { + let now = moment(); + let duration = moment.duration(moment(grainBag.settings.fillDate).diff(now)); + let days = duration.asDays(); + days = Math.abs(days); + duration.subtract(moment.duration(days, "days")); + // let hours = duration.hours(); + // hours = Math.abs(hours); + + return Math.floor(days) + " days"; + }; + + return ( + + + + + {grainBag.name()} + + + + + + {grainBag.isCustom() + ? grainBag.settings.customGrain + : GrainDescriber(grainBag.settings.supportedGrain).name} + + + {grainBag.settings.grainSubtype} + + + + Days in Field + + + {timeDisplay()} + + + Initial Moisture + + + {grainBag.settings.initialMoisture}% + + + + + + + + + {grainBag.bushels() <= 0 + ? "empty" + : grainBag.bushels().toLocaleString() + + "/" + + grainBag.capacity().toLocaleString() + + " bu"} + + + + ); +} diff --git a/src/grainBag/grainBagInventoryGraph.tsx b/src/grainBag/grainBagInventoryGraph.tsx new file mode 100644 index 0000000..7298453 --- /dev/null +++ b/src/grainBag/grainBagInventoryGraph.tsx @@ -0,0 +1,106 @@ +import { Box, Card, CircularProgress, Typography } from "@material-ui/core"; +import BarGraph, { BarData } from "charts/BarGraph"; +import GrainDescriber from "grain/GrainDescriber"; +import { GrainBag } from "models/GrainBag"; +import moment, { Moment } from "moment"; +import { pond } from "protobuf-ts/pond"; +import { useGrainBagAPI, useSnackbar } from "providers"; +import React, { useEffect, useState } from "react"; +import { getGrainUnit } from "utils"; + +interface Props { + grainBag: GrainBag; + startDate: Moment; + endDate: Moment; + customHeight?: number | string; +} + +export default function GrainBagInventoryGraph(props: Props) { + const { grainBag, startDate, endDate, customHeight } = props; + const grainBagAPI = useGrainBagAPI(); + const [data, setData] = useState([]); + const [loadingData, setLoadingData] = useState(false); + const { openSnack } = useSnackbar(); + + useEffect(() => { + if (grainBag.key() && !loadingData) { + setLoadingData(true); + grainBagAPI + .listHistory(grainBag.key(), 500, 0, startDate.toISOString(), endDate.toISOString()) + .then(resp => { + let barData: BarData[] = []; + let lastBushels = 0; + resp.data.history.forEach(hist => { + if (hist.object?.grainBagSettings) { + //let time = hist.timestamp; + let val = hist.object.grainBagSettings.currentBushels ?? 0; + if ( + getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && + grainBag.bushelsPerTonne() > 1 + ) { + val = Math.round((val / grainBag.bushelsPerTonne()) * 100) / 100; + } + if (val !== lastBushels) { + lastBushels = val; + barData.push({ + timestamp: moment(hist.timestamp).valueOf(), + value: val + }); + } + } + }); + if (barData.length === 0) { + barData.push({ + timestamp: moment.now().valueOf(), + value: + getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && + grainBag.bushelsPerTonne() > 1 + ? Math.round((grainBag.bushels() / grainBag.bushelsPerTonne()) * 100) / 100 + : grainBag.bushels() + }); + } + setData(barData); + }) + .catch(err => { + openSnack("There was a problem retrieving inventory information"); + }) + .finally(() => { + setLoadingData(false); + }); + } + }, [grainBagAPI, grainBag, startDate, endDate, openSnack]); //eslint-disable-line react-hooks/exhaustive-deps + + return ( + + + + Inventory over Time + + + {!loadingData && data.length > 0 ? ( + + ) : ( + + + + )} + + ); +} diff --git a/src/grainBag/grainBagList.tsx b/src/grainBag/grainBagList.tsx new file mode 100644 index 0000000..9ef6717 --- /dev/null +++ b/src/grainBag/grainBagList.tsx @@ -0,0 +1,92 @@ +import { Box, Button, createStyles, Grid, makeStyles, Theme, Typography } from "@material-ui/core"; +import React from "react"; +import ScrollMenu from "react-horizontal-scroll-menu"; +import { useHistory } from "react-router"; +import { ArrowForward, ArrowBack } from "@material-ui/icons"; +import { useMobile } from "hooks"; +import { GrainBag } from "models/GrainBag"; +import GrainBagCard from "./grainBagCard"; + +interface Props { + grainBags: GrainBag[]; + title?: string; + gridView?: boolean; +} + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + gridListTile: { + minHeight: "184px", + height: "auto !important", + width: "184px", + padding: 2 + }, + hidden: { + visibility: "hidden" + } + }) +); + +export default function GrainBagList(props: Props) { + const { grainBags, title, gridView } = props; + const classes = useStyles(); + const history = useHistory(); + const isMobile = useMobile(); + + const goToBag = (i: number) => { + let path = "/grainbags/" + grainBags[i].key(); + history.replace(path); + }; + + const scroll = () => { + return ( + + { + goToBag(e as number); + }} + arrowLeft={ + + } + arrowRight={ + + } + data={grainBags.map((b, i) => ( + + + + ))} + /> + + ); + }; + + const grid = () => { + return ( + + {grainBags.map((b, i) => ( + goToBag(i)}> + + + ))} + + ); + }; + + return ( + + {title && {title}} + {gridView ? grid() : scroll()} + + ); +} diff --git a/src/grainBag/grainBagSVG.tsx b/src/grainBag/grainBagSVG.tsx new file mode 100644 index 0000000..384eba4 --- /dev/null +++ b/src/grainBag/grainBagSVG.tsx @@ -0,0 +1,78 @@ +import { Box, makeStyles, Theme, createStyles } from "@material-ui/core"; +import React from "react"; + +const GRAIN_COLOUR = "#b5a962"; + +const useStyles = makeStyles((theme: Theme) => { + return createStyles({ + bag: { + fill: theme.palette.type === "light" ? "#373737" : "#292929", + fillOpacity: 1 + }, + inventory: { + fill: GRAIN_COLOUR, + fillOpacity: 0.5 + } + }); +}); + +interface Props { + fillPercent: number; + height: number; +} + +export default function GrainBagSVG(props: Props) { + const { fillPercent, height } = props; + const classes = useStyles(); + + const bagFillSVG = () => { + const topY = 173; + const bottomY = 235; + const fillLevel = ((bottomY - topY) * (100 - fillPercent)) / 100 + topY; + return ( + + {/* starts drawing top right corner */} + + + ); + }; + + return ( + + + + + {bagFillSVG()} + + {/* this path draws the bag */} + + + + + + + ); +} diff --git a/src/grainBag/grainBagSettings.tsx b/src/grainBag/grainBagSettings.tsx new file mode 100644 index 0000000..e49aa07 --- /dev/null +++ b/src/grainBag/grainBagSettings.tsx @@ -0,0 +1,456 @@ +import { + Button, + createStyles, + DialogActions, + DialogContent, + DialogTitle, + Grid, + InputAdornment, + makeStyles, + Switch, + TextField, + Theme, + useTheme +} from "@material-ui/core"; +import ResponsiveDialog from "common/ResponsiveDialog"; +import SearchSelect, { Option } from "common/SearchSelect"; +import GrainDescriber, { GrainOptions, ToGrainOption } from "grain/GrainDescriber"; +import GrainTransaction from "grain/GrainTransaction"; +import { clone } from "lodash"; +import { GrainBag } from "models/GrainBag"; +import moment from "moment"; +import { pond } from "protobuf-ts/pond"; +import { useGlobalState, useGrainBagAPI, useSnackbar } from "providers"; +import React, { useEffect, useState } from "react"; +import { useHistory } from "react-router"; +import { getGrainUnit } from "utils"; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + bottomSpacing: { + marginBottom: theme.spacing(1) + } + }) +); + +interface Props { + open: boolean; + close: (updatedBag?: GrainBag) => void; + coordinates?: { start: pond.Coordinates; end: pond.Coordinates }; + grainBag?: GrainBag; +} + +export default function GrainBagSettings(props: Props) { + const { open, close, grainBag, coordinates } = props; + const classes = useStyles(); + const grainBagAPI = useGrainBagAPI(); + const [bagName, setBagName] = useState(""); + const [bagDiameterM, setBagDiameterM] = useState(0); + const [bagLengthM, setBagLengthM] = useState(0); + const [bagDiameterDisplay, setBagDiameterDisplay] = useState(0); + const [bagLengthDispla, setBagLengthDisplay] = useState(0); + const [{ user }] = useGlobalState(); + const [isCustom, setIsCustom] = useState(false); + const [customType, setCustomType] = useState(""); + const [supportedGrainType, setSupportedGrainType] = useState(pond.Grain.GRAIN_INVALID); + const [selectedGrainOp, setSelectedGrainOp] = useState