added binyard and grainbag stuff
This commit is contained in:
parent
e52a9a4fbf
commit
235464bb93
11 changed files with 1553 additions and 33 deletions
207
src/grainBag/grainBagActions.tsx
Normal file
207
src/grainBag/grainBagActions.tsx
Normal file
|
|
@ -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 | HTMLElement>(null);
|
||||
const [openState, setOpenState] = useState<OpenState>({
|
||||
users: false,
|
||||
teams: false,
|
||||
settings: false,
|
||||
removeSelf: false,
|
||||
share: false
|
||||
});
|
||||
|
||||
const groupMenu = () => {
|
||||
return (
|
||||
<Menu
|
||||
id="menu"
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={() => setAnchorEl(null)}
|
||||
keepMounted
|
||||
disableAutoFocusItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setOpenState({ ...openState, share: true });
|
||||
setAnchorEl(null);
|
||||
}}
|
||||
button
|
||||
dense>
|
||||
<ListItemIcon>
|
||||
<Share className={classes.blueIcon} />
|
||||
</ListItemIcon>
|
||||
<ListItemText secondary="Share" />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
dense
|
||||
onClick={() => {
|
||||
setOpenState({ ...openState, users: true });
|
||||
setAnchorEl(null);
|
||||
}}
|
||||
button>
|
||||
<ListItemIcon>
|
||||
<ObjectUsersIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Users" />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
dense
|
||||
onClick={() => {
|
||||
setOpenState({ ...openState, teams: true });
|
||||
setAnchorEl(null);
|
||||
}}
|
||||
button>
|
||||
<ListItemIcon>
|
||||
<ObjectTeamsIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Teams" />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
dense
|
||||
onClick={() => {
|
||||
setOpenState({ ...openState, removeSelf: true });
|
||||
setAnchorEl(null);
|
||||
}}
|
||||
button>
|
||||
<ListItemIcon>
|
||||
<RemoveSelfIcon className={classes.red} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Leave" />
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
const dialogs = () => {
|
||||
const key = grainBag.key;
|
||||
const label = grainBag.name();
|
||||
return (
|
||||
<React.Fragment>
|
||||
<GrainBagSettings
|
||||
open={openState.settings}
|
||||
grainBag={grainBag}
|
||||
close={updatedBag => {
|
||||
if (updatedBag) {
|
||||
refreshCallback(updatedBag);
|
||||
}
|
||||
setOpenState({ ...openState, settings: false });
|
||||
}}
|
||||
/>
|
||||
<ShareObject
|
||||
scope={{ kind: "grainbag", key: key() } as Scope}
|
||||
label={label}
|
||||
permissions={permissions}
|
||||
isDialogOpen={openState.share}
|
||||
closeDialogCallback={() => setOpenState({ ...openState, share: false })}
|
||||
/>
|
||||
<ObjectUsers
|
||||
scope={{ kind: "grainbag", key: key() } as Scope}
|
||||
label={label}
|
||||
permissions={permissions}
|
||||
isDialogOpen={openState.users}
|
||||
closeDialogCallback={() => setOpenState({ ...openState, users: false })}
|
||||
refreshCallback={refreshCallback}
|
||||
/>
|
||||
<ObjectTeams
|
||||
scope={{ kind: "grainbag", key: key() } as Scope}
|
||||
label={label}
|
||||
permissions={permissions}
|
||||
isDialogOpen={openState.teams}
|
||||
closeDialogCallback={() => setOpenState({ ...openState, teams: false })}
|
||||
refreshCallback={refreshCallback}
|
||||
/>
|
||||
<RemoveSelfFromObject
|
||||
scope={{ kind: "grainbag", key: key() } as Scope}
|
||||
path={"bins"}
|
||||
label={label}
|
||||
isDialogOpen={openState.removeSelf}
|
||||
closeDialogCallback={() => {
|
||||
setOpenState({ ...openState, removeSelf: false });
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{permissions.includes(pond.Permission.PERMISSION_WRITE) && (
|
||||
<Tooltip title="Settings">
|
||||
<IconButton onClick={() => setOpenState({ ...openState, settings: true })}>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<IconButton
|
||||
aria-owns={anchorEl ? "groupMenu" : undefined}
|
||||
aria-haspopup="true"
|
||||
onClick={(event: React.MouseEvent<HTMLButtonElement>) => setAnchorEl(event.currentTarget)}>
|
||||
<MoreIcon />
|
||||
</IconButton>
|
||||
{dialogs()}
|
||||
{groupMenu()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
85
src/grainBag/grainBagCard.tsx
Normal file
85
src/grainBag/grainBagCard.tsx
Normal file
|
|
@ -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 (
|
||||
<Card>
|
||||
<Box padding={1}>
|
||||
<Box>
|
||||
<Typography
|
||||
align="left"
|
||||
variant="body1"
|
||||
color="textPrimary"
|
||||
noWrap
|
||||
style={{ fontSize: "0.8rem", fontWeight: 700 }}>
|
||||
{grainBag.name()}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="center" paddingTop={0}>
|
||||
<Box padding={0} style={{ flex: 1, textAlign: "left", flexDirection: "column" }}>
|
||||
<Typography variant="subtitle2" style={{ fontSize: "0.7rem", fontWeight: 700 }}>
|
||||
{grainBag.isCustom()
|
||||
? grainBag.settings.customGrain
|
||||
: GrainDescriber(grainBag.settings.supportedGrain).name}
|
||||
</Typography>
|
||||
<Typography variant="subtitle2" style={{ fontSize: "0.7rem", fontWeight: 700 }}>
|
||||
{grainBag.settings.grainSubtype}
|
||||
</Typography>
|
||||
<Box paddingTop={2} style={{ textAlign: "left" }}>
|
||||
<Typography variant="subtitle2" style={{ fontSize: 10 }}>
|
||||
Days in Field
|
||||
</Typography>
|
||||
<Typography variant="subtitle2" style={{ fontWeight: 700 }}>
|
||||
{timeDisplay()}
|
||||
</Typography>
|
||||
<Typography variant="subtitle2" style={{ fontSize: 10 }}>
|
||||
Initial Moisture
|
||||
</Typography>
|
||||
<Typography variant="subtitle2" style={{ fontWeight: 700 }}>
|
||||
{grainBag.settings.initialMoisture}%
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box padding={1} style={{ flex: 1, textAlign: "center", flexDirection: "column" }}>
|
||||
<GrainBagSVG fillPercent={fillLevel} height={128} />
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography align="center" color="textSecondary" noWrap style={{ fontSize: "0.65rem" }}>
|
||||
{grainBag.bushels() <= 0
|
||||
? "empty"
|
||||
: grainBag.bushels().toLocaleString() +
|
||||
"/" +
|
||||
grainBag.capacity().toLocaleString() +
|
||||
" bu"}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
106
src/grainBag/grainBagInventoryGraph.tsx
Normal file
106
src/grainBag/grainBagInventoryGraph.tsx
Normal file
|
|
@ -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<BarData[]>([]);
|
||||
const [loadingData, setLoadingData] = useState<boolean>(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 (
|
||||
<Card raised>
|
||||
<Box padding={2}>
|
||||
<Typography variant="h6" style={{ fontWeight: "bold" }}>
|
||||
Inventory over Time
|
||||
</Typography>
|
||||
</Box>
|
||||
{!loadingData && data.length > 0 ? (
|
||||
<BarGraph
|
||||
barColour={
|
||||
grainBag.storage() === pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN
|
||||
? GrainDescriber(grainBag.grain()).colour
|
||||
: "yellow"
|
||||
}
|
||||
data={data}
|
||||
yMin={0}
|
||||
yMax={
|
||||
getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT
|
||||
? grainBag.capacity() / grainBag.bushelsPerTonne()
|
||||
: grainBag.capacity()
|
||||
}
|
||||
customHeight={customHeight}
|
||||
useGradient
|
||||
labels
|
||||
labelColour="white"
|
||||
/>
|
||||
) : (
|
||||
<Box height={400} display="flex" justifyContent="center" alignItems="center">
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
92
src/grainBag/grainBagList.tsx
Normal file
92
src/grainBag/grainBagList.tsx
Normal file
|
|
@ -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 (
|
||||
<React.Fragment>
|
||||
<ScrollMenu
|
||||
wheel={false}
|
||||
alignCenter={false}
|
||||
inertiaScrolling
|
||||
hideArrows
|
||||
hideSingleArrow
|
||||
arrowDisabledClass={classes.hidden}
|
||||
onSelect={e => {
|
||||
goToBag(e as number);
|
||||
}}
|
||||
arrowLeft={
|
||||
<Button style={{ height: 184, display: isMobile ? "none" : "block" }}>
|
||||
<ArrowBack />
|
||||
</Button>
|
||||
}
|
||||
arrowRight={
|
||||
<Button style={{ height: 184, display: isMobile ? "none" : "block" }}>
|
||||
<ArrowForward />
|
||||
</Button>
|
||||
}
|
||||
data={grainBags.map((b, i) => (
|
||||
<Box key={i} className={classes.gridListTile}>
|
||||
<GrainBagCard grainBag={b} />
|
||||
</Box>
|
||||
))}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const grid = () => {
|
||||
return (
|
||||
<Grid container direction="row">
|
||||
{grainBags.map((b, i) => (
|
||||
<Box key={i} className={classes.gridListTile} onClick={() => goToBag(i)}>
|
||||
<GrainBagCard grainBag={b} />
|
||||
</Box>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{title && <Typography>{title}</Typography>}
|
||||
{gridView ? grid() : scroll()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
78
src/grainBag/grainBagSVG.tsx
Normal file
78
src/grainBag/grainBagSVG.tsx
Normal file
|
|
@ -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 (
|
||||
<g>
|
||||
{/* starts drawing top right corner */}
|
||||
<path
|
||||
className={classes.inventory}
|
||||
d={
|
||||
"M 193.27593," +
|
||||
fillLevel + //increase the y value to make the drawing start lower
|
||||
//this command makes the top curve
|
||||
"c -0.23147,-0.43237 -0.46296,-0.84552 -0.71457,-1.24905 -2.15378,-2.45005 -6.6425,-4.13146 -11.83573,-4.13146 -5.19323,0 -9.68195,1.68141 -11.83574,4.12186 -0.25161,0.40353 -0.48309,0.82628 -0.71457,1.24904 " +
|
||||
//this command draws down from the top left corner after finishing the first curve
|
||||
"V 235 " +
|
||||
//these 3 commands make the bottom curve
|
||||
"c 0.7045,0.21137 1.44927,0.41314 2.22423,0.58609 0,0 0,0 0.0101,0 0.11071,0.01 0.20128,0.0192 0.2516,0.0288 0.27175,0.0769 0.55355,0.14412 0.84542,0.20177 0.0503,0.01 0.11071,0.0192 0.16103,0.0288 0.26167,0.048 0.53341,0.0961 0.80515,0.14412 0.0302,0 0.0503,0 0.0704,0.01 2.06321,0.31707 4.72021,0.43236 8.1421,0.43236 " +
|
||||
"h 0.0906 " +
|
||||
"c 3.41183,0 6.06883,-0.11529 8.1421,-0.43236 0.0201,0 0.0503,0 0.0704,-0.01 0.27174,-0.048 0.54348,-0.0961 0.80515,-0.14412 0.0503,0 0.10065,-0.0192 0.16104,-0.0288 0.30193,-0.0576 0.58373,-0.13451 0.84541,-0.20177 0.0503,0 0.13083,-0.0192 0.25161,-0.0288 0,0 0,0 0.0201,0 0.77497,-0.17295 1.50967,-0.37472 2.21417,-0.58609 " +
|
||||
//this command connects where the line is at and the starting point
|
||||
"z"
|
||||
}
|
||||
id="path275"
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box height={height} width={height * 0.5}>
|
||||
<svg width="100%" height="100%" viewBox="0 0 10 20">
|
||||
<g id="layer1" transform="translate(-104.92152,-72.241116)">
|
||||
<g id="g121" transform="matrix(0.26458333,0,0,0.26458333,62.153011,29.46019)">
|
||||
{bagFillSVG()}
|
||||
<g id="g339" transform="matrix(0.93700864,0,0,0.92692889,161.64576,161.68242)">
|
||||
{/* this path draws the bag */}
|
||||
<path
|
||||
className={classes.bag}
|
||||
d="M 38.95,75.7 C 38.57,74.77 38.45,73.76 38.63,72.77 39.06,70.37 39.12,67.29 39.12,63.36 39.12,56.26 37.16,13.93 33.97,9.14 33.19,7.97 32.9,6.54 33.21,5.17 33.38,4.45 33.47,3.66 33.51,2.84 33.54,2.26 32.97,1.84 32.42,2.02 30.3,2.7 22.38,0.74 20.22,0.74 20.2,0.74 20.18,0.74 20.16,0.74 20.14,0.74 20.12,0.74 20.1,0.74 17.93,0.74 10.02,2.7 7.9,2.02 7.35,1.84 6.78,2.26 6.81,2.84 6.85,3.66 6.94,4.45 7.11,5.17 7.43,6.54 7.13,7.97 6.35,9.14 3.16,13.93 1.21,56.25 1.21,63.35 1.21,67.29 1.27,70.36 1.7,72.76 1.88,73.75 1.77,74.76 1.38,75.69 0.98,76.65 0.8,77.87 0.73,79.13 0.7,79.71 1.26,80.14 1.81,79.96 3.78,79.33 7.32,79.72 7.79,79.77 10.57,80.57 14.51,80.83 20.11,80.83 20.13,80.83 20.15,80.83 20.17,80.83 20.19,80.83 20.21,80.83 20.23,80.83 25.83,80.83 29.77,80.57 32.55,79.77 33.02,79.71 36.55,79.32 38.53,79.96 39.08,80.14 39.64,79.71 39.61,79.13 39.55,77.87 39.36,76.66 38.96,75.69 Z M 32.63,75.79 C 31.93,76.01 31.2,76.22 30.43,76.4 30.43,76.4 30.42,76.4 30.41,76.4 30.29,76.41 30.21,76.42 30.16,76.43 29.89,76.51 29.61,76.58 29.32,76.64 29.27,76.65 29.22,76.66 29.16,76.67 28.9,76.72 28.63,76.77 28.36,76.82 28.34,76.82 28.31,76.82 28.29,76.83 26.24,77.16 23.6,77.28 20.2,77.28 H 20.11 C 16.72,77.28 14.08,77.16 12.02,76.83 12,76.83 11.98,76.83 11.95,76.82 11.68,76.77 11.41,76.72 11.15,76.67 11.1,76.67 11.05,76.65 10.99,76.64 10.69,76.58 10.41,76.5 10.15,76.43 10.1,76.43 10.02,76.41 9.9,76.4 9.9,76.4 9.9,76.4 9.89,76.4 9.12,76.22 8.38,76.01 7.68,75.79
|
||||
V 13.2 C 7.91,12.75 8.14,12.32 8.39,11.9 10.53,9.36 14.99,7.61 20.15,7.61 25.31,7.61 29.77,9.36 31.91,11.9 32.16,12.32 32.39,12.76 32.62,13.2
|
||||
V 75.78 Z"
|
||||
id="path329"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
456
src/grainBag/grainBagSettings.tsx
Normal file
456
src/grainBag/grainBagSettings.tsx
Normal file
|
|
@ -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<boolean>(false);
|
||||
const [customType, setCustomType] = useState("");
|
||||
const [supportedGrainType, setSupportedGrainType] = useState(pond.Grain.GRAIN_INVALID);
|
||||
const [selectedGrainOp, setSelectedGrainOp] = useState<Option | null>(null);
|
||||
const grainOptions = GrainOptions();
|
||||
const [grainSubtype, setGrainSubtype] = useState("");
|
||||
const [grainCapacity, setGrainCapacity] = useState(0);
|
||||
const [grainFill, setGrainFill] = useState("");
|
||||
const [grainBushels, setGrainBushels] = useState(0);
|
||||
const [fillDate, setFillDate] = useState(moment().format("YYYY-MM-DD"));
|
||||
const [initialMoisture, setInitialMoisture] = useState(0);
|
||||
const history = useHistory();
|
||||
const { openSnack } = useSnackbar();
|
||||
const theme = useTheme();
|
||||
const [grainUpdate, setGrainUpdate] = useState(false);
|
||||
const [grainDiff, setGrainDiff] = useState(0);
|
||||
const [bushelsPerTonne, setBushelsPerTonne] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (grainBag) {
|
||||
//if a grainbag is passed in set all the state values
|
||||
setBagDiameterM(grainBag.settings.diameter);
|
||||
setBagDiameterDisplay(
|
||||
user.settings.distanceUnit === pond.DistanceUnit.DISTANCE_UNIT_FEET
|
||||
? grainBag.settings.diameter * 3.281
|
||||
: grainBag.settings.diameter
|
||||
);
|
||||
setBagLengthM(grainBag.settings.length);
|
||||
setBagLengthDisplay(
|
||||
user.settings.distanceUnit === pond.DistanceUnit.DISTANCE_UNIT_FEET
|
||||
? grainBag.settings.length * 3.281
|
||||
: grainBag.settings.length
|
||||
);
|
||||
let grainVal = grainBag.bushels();
|
||||
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT) {
|
||||
grainVal = grainBag.bushels() / grainBag.bushelsPerTonne();
|
||||
}
|
||||
setGrainFill(grainVal.toFixed(2));
|
||||
setBagName(grainBag.name());
|
||||
setIsCustom(grainBag.isCustom());
|
||||
setSupportedGrainType(grainBag.settings.supportedGrain);
|
||||
setSelectedGrainOp(ToGrainOption(grainBag.settings.supportedGrain));
|
||||
setCustomType(grainBag.settings.customGrain);
|
||||
setGrainSubtype(grainBag.settings.grainSubtype);
|
||||
setGrainCapacity(grainBag.capacity());
|
||||
setGrainBushels(grainBag.bushels());
|
||||
setFillDate(grainBag.settings.fillDate);
|
||||
setInitialMoisture(grainBag.settings.initialMoisture);
|
||||
}
|
||||
}, [grainBag, user]);
|
||||
|
||||
const removeGrainBag = () => {
|
||||
if (!grainBag) return;
|
||||
grainBagAPI
|
||||
.removeGrainBag(grainBag.key())
|
||||
.then(resp => {
|
||||
openSnack("Removed Grain Bag");
|
||||
close();
|
||||
history.push("/bins");
|
||||
})
|
||||
.catch(err => {
|
||||
openSnack("Failed to remove Grain Bag");
|
||||
});
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
//if a bag was passed in do an update
|
||||
if (grainBag) {
|
||||
grainBag.title = bagName;
|
||||
grainBag.settings.initialMoisture = initialMoisture;
|
||||
grainBag.settings.diameter = bagDiameterM;
|
||||
grainBag.settings.length = bagLengthM;
|
||||
grainBag.settings.customGrain = customType;
|
||||
grainBag.settings.supportedGrain = supportedGrainType;
|
||||
grainBag.settings.bushelCapacity = grainCapacity;
|
||||
grainBag.settings.grainSubtype = grainSubtype;
|
||||
grainBag.settings.fillDate = fillDate;
|
||||
grainBag.settings.bushelsPerTonne = bushelsPerTonne;
|
||||
grainBag.settings.storageType = isCustom
|
||||
? pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN
|
||||
: pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN;
|
||||
|
||||
if (grainBag.bushels() !== grainBushels) {
|
||||
//grainBag.settings.currentBushels = grainBushels;
|
||||
setGrainDiff(grainBushels - grainBag.bushels());
|
||||
setGrainUpdate(true);
|
||||
} else {
|
||||
grainBagAPI
|
||||
.updateGrainBag(grainBag.key(), bagName, grainBag.settings)
|
||||
.then(resp => {
|
||||
openSnack("Grain Bag Updated");
|
||||
let updatedBag = clone(grainBag);
|
||||
close(updatedBag);
|
||||
})
|
||||
.catch(err => {
|
||||
openSnack("Failed to update Grain Bag");
|
||||
close();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
//otherwise make a new one
|
||||
let settings = pond.GrainBagSettings.create();
|
||||
settings.initialMoisture = initialMoisture;
|
||||
settings.diameter = bagDiameterM;
|
||||
settings.length = bagLengthM;
|
||||
settings.customGrain = customType;
|
||||
settings.supportedGrain = supportedGrainType;
|
||||
settings.bushelCapacity = grainCapacity;
|
||||
settings.currentBushels = grainBushels;
|
||||
settings.grainSubtype = grainSubtype;
|
||||
settings.fillDate = fillDate;
|
||||
settings.storageType = isCustom
|
||||
? pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN
|
||||
: pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN;
|
||||
if (coordinates) {
|
||||
settings.startLocation = coordinates.start;
|
||||
settings.endLocation = coordinates.end;
|
||||
}
|
||||
|
||||
grainBagAPI
|
||||
.addGrainBag(bagName, settings)
|
||||
.then(resp => {
|
||||
openSnack("New grain bag added");
|
||||
let newBag = GrainBag.create(
|
||||
pond.GrainBag.fromObject({
|
||||
title: bagName,
|
||||
key: resp.data.key,
|
||||
settings: settings
|
||||
})
|
||||
);
|
||||
close(newBag);
|
||||
})
|
||||
.catch(() => {
|
||||
openSnack("Failed to add new grain bag");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const grainBox = () => {
|
||||
if (isCustom) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TextField
|
||||
label="Grain"
|
||||
value={customType}
|
||||
type="text"
|
||||
onChange={event => {
|
||||
setCustomType(event.target.value);
|
||||
}}
|
||||
fullWidth
|
||||
className={classes.bottomSpacing}
|
||||
variant="outlined"
|
||||
/>
|
||||
<TextField
|
||||
label="Bushels Per Tonne"
|
||||
value={bushelsPerTonne}
|
||||
type="number"
|
||||
onChange={event => {
|
||||
setBushelsPerTonne(+event.target.value);
|
||||
}}
|
||||
fullWidth
|
||||
className={classes.bottomSpacing}
|
||||
variant="outlined"
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<SearchSelect
|
||||
style={{ marginBottom: theme.spacing(1) }}
|
||||
label="Type"
|
||||
selected={selectedGrainOp}
|
||||
changeSelection={option => {
|
||||
let newGrainType = option
|
||||
? pond.Grain[option.value as keyof typeof pond.Grain]
|
||||
: pond.Grain.GRAIN_INVALID;
|
||||
setSupportedGrainType(newGrainType);
|
||||
setBushelsPerTonne(GrainDescriber(newGrainType).bushelsPerTonne);
|
||||
setSelectedGrainOp(option);
|
||||
}}
|
||||
group
|
||||
//disabled={!canEdit}
|
||||
options={grainOptions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const formInvalid = () => {
|
||||
let invalid = true;
|
||||
if (bagName !== "" && grainCapacity > 0) {
|
||||
invalid = false;
|
||||
}
|
||||
return invalid;
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<GrainTransaction
|
||||
open={grainUpdate}
|
||||
mainObject={grainBag ?? GrainBag.create()}
|
||||
grainAdjustment={grainDiff}
|
||||
close={() => {
|
||||
setGrainUpdate(false);
|
||||
setGrainDiff(0);
|
||||
//setPendingGrainAmount(undefined);
|
||||
//resetFillPercentage();
|
||||
//setGrainDiff(0);
|
||||
}}
|
||||
callback={() => {
|
||||
let updatedBag = clone(grainBag);
|
||||
close(updatedBag);
|
||||
}}
|
||||
/>
|
||||
<ResponsiveDialog
|
||||
open={open}
|
||||
onClose={() => {
|
||||
close();
|
||||
}}>
|
||||
<DialogTitle>Grain Bag Settings</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
label="Grain Bag Name"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={bagName}
|
||||
onChange={e => setBagName(e.target.value)}
|
||||
className={classes.bottomSpacing}
|
||||
/>
|
||||
<TextField
|
||||
label="Bag Diameter"
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
{user.settings.distanceUnit === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "ft" : "m"}
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
type="number"
|
||||
value={bagDiameterDisplay}
|
||||
onChange={e => {
|
||||
setBagDiameterDisplay(+e.target.value);
|
||||
let inMeters = +e.target.value;
|
||||
if (user.settings.distanceUnit === pond.DistanceUnit.DISTANCE_UNIT_FEET) {
|
||||
//if the users preferred unit is in feet that means the value that will be saved needs to be converted to meters
|
||||
inMeters = inMeters / 3.281;
|
||||
}
|
||||
setBagDiameterM(inMeters);
|
||||
}}
|
||||
className={classes.bottomSpacing}
|
||||
/>
|
||||
<TextField
|
||||
label="Bag Length"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
{user.settings.distanceUnit === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "ft" : "m"}
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
type="number"
|
||||
value={bagLengthDispla}
|
||||
onChange={e => {
|
||||
setBagLengthDisplay(+e.target.value);
|
||||
let inMeters = +e.target.value;
|
||||
if (user.settings.distanceUnit === pond.DistanceUnit.DISTANCE_UNIT_FEET) {
|
||||
//if the users preferred unit is in feet that means the value that will be saved needs to be converted to meters
|
||||
inMeters = inMeters / 3.281;
|
||||
}
|
||||
setBagLengthM(inMeters);
|
||||
}}
|
||||
className={classes.bottomSpacing}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
type="date"
|
||||
label="Fill Date"
|
||||
value={fillDate}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
onChange={e => setFillDate(e.target.value)}
|
||||
/>
|
||||
<Grid container alignItems="center">
|
||||
<Grid item>Grain</Grid>
|
||||
<Grid item>
|
||||
<Switch
|
||||
color="default"
|
||||
value={isCustom}
|
||||
checked={isCustom}
|
||||
onChange={(_, checked) => {
|
||||
setIsCustom(checked);
|
||||
if (checked) {
|
||||
setSupportedGrainType(pond.Grain.GRAIN_CUSTOM);
|
||||
} else {
|
||||
setCustomType("");
|
||||
}
|
||||
}}
|
||||
name="storage"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>Custom</Grid>
|
||||
</Grid>
|
||||
{grainBox()}
|
||||
<TextField
|
||||
label="Grain Subtype"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={grainSubtype}
|
||||
onChange={e => setGrainSubtype(e.target.value)}
|
||||
className={classes.bottomSpacing}
|
||||
/>
|
||||
<TextField
|
||||
label="Initial Moisture"
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">%</InputAdornment>
|
||||
}}
|
||||
value={initialMoisture}
|
||||
onChange={e => setInitialMoisture(+e.target.value)}
|
||||
className={classes.bottomSpacing}
|
||||
/>
|
||||
<TextField
|
||||
label="Capacity"
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">bu</InputAdornment>
|
||||
}}
|
||||
value={grainCapacity}
|
||||
onChange={e => setGrainCapacity(+e.target.value)}
|
||||
className={classes.bottomSpacing}
|
||||
/>
|
||||
<TextField
|
||||
label={
|
||||
"Current " +
|
||||
(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && bushelsPerTonne > 0
|
||||
? "Weight"
|
||||
: "Bushels")
|
||||
}
|
||||
fullWidth
|
||||
type="number"
|
||||
variant="outlined"
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
{getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && bushelsPerTonne > 0
|
||||
? "mT"
|
||||
: "bu"}
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
value={grainFill}
|
||||
onChange={e => {
|
||||
let bushelVal = +e.target.value;
|
||||
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT) {
|
||||
//convert the number as a weight into the bushel value
|
||||
bushelVal = +e.target.value * (bushelsPerTonne > 0 ? bushelsPerTonne : 1);
|
||||
}
|
||||
if (bushelVal > grainCapacity) {
|
||||
bushelVal = grainCapacity;
|
||||
}
|
||||
setGrainBushels(bushelVal);
|
||||
setGrainFill(e.target.value);
|
||||
}}
|
||||
className={classes.bottomSpacing}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Grid container direction="row" justify="space-between">
|
||||
<Grid item>
|
||||
{grainBag && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
removeGrainBag();
|
||||
}}
|
||||
variant="contained"
|
||||
style={{ background: "red" }}>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button
|
||||
onClick={() => {
|
||||
close();
|
||||
}}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={submit} disabled={formInvalid()} color="primary" variant="contained">
|
||||
Confirm
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogActions>
|
||||
</ResponsiveDialog>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
322
src/grainBag/grainBagVisualizer.tsx
Normal file
322
src/grainBag/grainBagVisualizer.tsx
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
import {
|
||||
Box,
|
||||
Card,
|
||||
Grid,
|
||||
Slider,
|
||||
makeStyles,
|
||||
createStyles,
|
||||
Typography,
|
||||
IconButton,
|
||||
Theme,
|
||||
useTheme,
|
||||
darken
|
||||
} from "@material-ui/core";
|
||||
import GrainDescriber from "grain/GrainDescriber";
|
||||
import GrainTransaction from "grain/GrainTransaction";
|
||||
import { useMobile } from "hooks";
|
||||
import useViewport from "hooks/useViewport";
|
||||
import { round } from "lodash";
|
||||
import { GrainBag } from "models/GrainBag";
|
||||
import moment from "moment";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { getThemeType } from "theme";
|
||||
import { getGrainUnit } from "utils";
|
||||
import GrainBagSVG from "./grainBagSVG";
|
||||
|
||||
interface Props {
|
||||
grainBag: GrainBag;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
enum zIndexPriority {
|
||||
low = 1,
|
||||
medium = 2,
|
||||
high = 3
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
return createStyles({
|
||||
grainOVerlay: {
|
||||
position: "absolute",
|
||||
height: "10%",
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
textShadow: "4px 4px 10px black"
|
||||
},
|
||||
cardContent: {
|
||||
padding: theme.spacing(2)
|
||||
},
|
||||
bgItem: {
|
||||
background: darken(theme.palette.background.paper, getThemeType() === "light" ? 0.05 : 0.25),
|
||||
zIndex: zIndexPriority.low,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
position: "relative",
|
||||
width: "auto",
|
||||
marginRight: theme.spacing(1)
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export default function GrainBagVisualizer(props: Props) {
|
||||
const { grainBag } = props;
|
||||
const [fillLevel, setFillLevel] = useState(0);
|
||||
const isMobile = useMobile();
|
||||
const viewport = useViewport();
|
||||
const [sliderColour, setSliderCoulour] = useState("gold");
|
||||
const [pendingGrainAmount, setPendingGrainAmount] = useState<number | null>();
|
||||
const classes = useStyles();
|
||||
const [grainDiff, setGrainDiff] = useState<number | undefined>();
|
||||
const theme = useTheme();
|
||||
const [openTransaction, setOpenTransaction] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setFillLevel((grainBag.bushels() / grainBag.capacity()) * 100);
|
||||
}, [grainBag]);
|
||||
|
||||
const getSVGHeight = () => {
|
||||
if (isMobile) {
|
||||
return viewport.width * 0.8;
|
||||
}
|
||||
return viewport.height * 0.4;
|
||||
};
|
||||
|
||||
const grainOverlay = () => {
|
||||
let displayPending = pendingGrainAmount;
|
||||
let displayDiff = grainDiff;
|
||||
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && grainBag.bushelsPerTonne() > 1) {
|
||||
if (displayPending && displayDiff) {
|
||||
displayPending = Math.round((displayPending / grainBag.bushelsPerTonne()) * 100) / 100;
|
||||
displayDiff = Math.round((displayDiff / grainBag.bushelsPerTonne()) * 100) / 100;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Box>
|
||||
{grainDiff !== undefined && grainDiff > 0 && (
|
||||
<div
|
||||
className={classes.grainOVerlay}
|
||||
style={{
|
||||
top: "38%",
|
||||
color: "#5CE422"
|
||||
}}>
|
||||
<Typography variant={"h5"} style={{ fontWeight: 750 }}>
|
||||
+{displayDiff}
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={classes.grainOVerlay}
|
||||
style={{
|
||||
top: "45%"
|
||||
}}>
|
||||
<Typography variant={"h4"} style={{ fontWeight: 750 }}>
|
||||
{displayPending}
|
||||
</Typography>
|
||||
</div>
|
||||
{grainDiff !== undefined && grainDiff < 0 && (
|
||||
<div
|
||||
className={classes.grainOVerlay}
|
||||
style={{
|
||||
top: "55%",
|
||||
color: "#E42222"
|
||||
}}>
|
||||
<Typography variant={"h5"} style={{ fontWeight: 750 }}>
|
||||
{displayDiff}
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
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 in bag";
|
||||
};
|
||||
|
||||
const grainAmountDisplay = () => {
|
||||
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && grainBag.bushelsPerTonne() > 1) {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "row" }}>
|
||||
<Typography variant="body2" style={{ fontWeight: "bold", marginRight: "4px" }}>
|
||||
{(grainBag.bushels() / grainBag.bushelsPerTonne()).toFixed(2) + " mT"}
|
||||
</Typography>
|
||||
<Typography variant="body2">({grainBag.fillPercent()}%)</Typography>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "row" }}>
|
||||
<Typography variant="body2" style={{ fontWeight: "bold", marginRight: "4px" }}>
|
||||
{grainBag.bushels() <= 0 ? "Empty" : grainBag.bushels().toLocaleString()}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{grainBag.bushels() <= 0 ? "" : " / " + grainBag.capacity().toLocaleString() + " bu"}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const overview = () => {
|
||||
return (
|
||||
<Box width={1} position="absolute" top={0}>
|
||||
<Typography variant="subtitle2" color="textPrimary" style={{ fontWeight: 800 }}>
|
||||
{grainBag.storage() === pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN
|
||||
? GrainDescriber(grainBag.grain()).name
|
||||
: grainBag.customType()}
|
||||
{grainBag.subtype() !== "" ? " - " + grainBag.subtype() : ""}
|
||||
</Typography>
|
||||
<Box className={classes.bgItem} padding={1}>
|
||||
{grainAmountDisplay()}
|
||||
</Box>
|
||||
<Box padding="0" width={1} position="absolute" top={70}>
|
||||
<Typography variant="subtitle2" color="textPrimary" style={{ fontWeight: 600 }}>
|
||||
Fill Date
|
||||
</Typography>
|
||||
<Box className={classes.bgItem} display="flex" flexDirection={"column"} padding={1}>
|
||||
<Typography variant="body2" style={{ fontWeight: "bold" }}>
|
||||
{grainBag.settings.fillDate}
|
||||
</Typography>
|
||||
<Typography variant="body2" style={{ fontWeight: "bold" }}>
|
||||
{timeDisplay()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box padding="0" width={1} position="absolute" top={160}>
|
||||
<Typography variant="subtitle2" color="textPrimary" style={{ fontWeight: 600 }}>
|
||||
Initial Moisture
|
||||
</Typography>
|
||||
<Box className={classes.bgItem} display="flex" flexDirection={"column"} padding={1}>
|
||||
<Typography variant="body2" style={{ fontWeight: "bold" }}>
|
||||
{grainBag.settings.initialMoisture}%
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const visual = () => {
|
||||
return (
|
||||
<Box display="flex" width={1} justifyContent="flex-end">
|
||||
<Box
|
||||
position="relative"
|
||||
height={1}
|
||||
width={1}
|
||||
bgcolor={theme.palette.background.paper}
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
alignContent="flex-end">
|
||||
<Box zIndex={zIndexPriority.high}>
|
||||
{grainOverlay()}
|
||||
<GrainBagSVG fillPercent={fillLevel} height={getSVGHeight()} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const controls = () => {
|
||||
return (
|
||||
<Box
|
||||
height={1}
|
||||
width={1}
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
justifyContent="flex-end"
|
||||
alignContent="flex-end">
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setOpenTransaction(true);
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: "gold",
|
||||
color: "black",
|
||||
height: 40,
|
||||
width: 40,
|
||||
marginBottom: 10
|
||||
}}>
|
||||
+/-
|
||||
</IconButton>
|
||||
<Box
|
||||
height={0.7}
|
||||
width={1}
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignContent="flex-end"
|
||||
zIndex={zIndexPriority.high}>
|
||||
<Slider
|
||||
orientation="vertical"
|
||||
value={fillLevel}
|
||||
style={{ color: sliderColour }}
|
||||
min={0}
|
||||
max={100}
|
||||
valueLabelDisplay="auto"
|
||||
valueLabelFormat={value => value.toFixed() + "%"}
|
||||
onChange={(_, value) => {
|
||||
setFillLevel(value as number);
|
||||
const capacity = grainBag.capacity();
|
||||
const current = grainBag.bushels();
|
||||
let grainAmount = ((value as number) / 100) * capacity;
|
||||
if (grainAmount < current) {
|
||||
setSliderCoulour("#E42222");
|
||||
} else if (grainAmount > current) {
|
||||
setSliderCoulour("#5CE422");
|
||||
} else {
|
||||
setSliderCoulour("gold");
|
||||
}
|
||||
setGrainDiff(round(grainAmount - current, 0));
|
||||
setPendingGrainAmount(round(grainAmount, 0));
|
||||
}}
|
||||
onChangeCommitted={() => {
|
||||
setOpenTransaction(true);
|
||||
}}
|
||||
aria-labelledby="grain-amount"
|
||||
/>
|
||||
</Box>
|
||||
<Box height={0.2} width={1} position="relative">
|
||||
<Box width={1} position="absolute" bottom="0" textAlign="center" />{" "}
|
||||
{/* this box is used to push the slider up */}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card raised className={classes.cardContent}>
|
||||
<Grid container justify="flex-start" alignItems="stretch">
|
||||
<Grid item xs sm md style={{ position: "relative" }}>
|
||||
{overview()}
|
||||
</Grid>
|
||||
<Grid item style={{ position: "relative" }}>
|
||||
{visual()}
|
||||
</Grid>
|
||||
<Grid item>{controls()}</Grid>
|
||||
</Grid>
|
||||
{/* dialog for grain inventory transactions */}
|
||||
<GrainTransaction
|
||||
open={openTransaction}
|
||||
close={() => {
|
||||
setOpenTransaction(false);
|
||||
}}
|
||||
mainObject={grainBag}
|
||||
grainAdjustment={grainDiff}
|
||||
callback={() => {
|
||||
setFillLevel((grainBag.bushels() / grainBag.capacity()) * 100);
|
||||
setGrainDiff(undefined);
|
||||
setPendingGrainAmount(null);
|
||||
setSliderCoulour("gold");
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
38
src/models/BinYard.ts
Normal file
38
src/models/BinYard.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { cloneDeep } from "lodash";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { or } from "utils/types";
|
||||
|
||||
//model not used
|
||||
export class BinYard {
|
||||
public settings: pond.BinYardSettings = pond.BinYardSettings.create();
|
||||
public permissions: pond.Permission[] = [];
|
||||
|
||||
public static create(pb?: pond.BinYard): BinYard {
|
||||
let my = new BinYard();
|
||||
if (pb) {
|
||||
my.settings = pond.BinYardSettings.fromObject(cloneDeep(or(pb.settings, {})));
|
||||
my.permissions = pb.yardPermissions;
|
||||
}
|
||||
return my;
|
||||
}
|
||||
|
||||
public static clone(other?: BinYard): BinYard {
|
||||
if (other) {
|
||||
return BinYard.create(
|
||||
pond.BinYard.fromObject({
|
||||
settings: cloneDeep(other.settings),
|
||||
binPermissions: cloneDeep(other.permissions)
|
||||
})
|
||||
);
|
||||
}
|
||||
return BinYard.create();
|
||||
}
|
||||
|
||||
public static any(data: any): BinYard {
|
||||
return BinYard.create(pond.BinYard.fromObject(cloneDeep(data)));
|
||||
}
|
||||
|
||||
public key(): string {
|
||||
return this.settings.key;
|
||||
}
|
||||
}
|
||||
135
src/models/GrainBag.ts
Normal file
135
src/models/GrainBag.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import { cloneDeep } from "lodash";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { or } from "utils/types";
|
||||
|
||||
export class GrainBag {
|
||||
public settings: pond.GrainBagSettings = pond.GrainBagSettings.create();
|
||||
public title: string = "Bag";
|
||||
private objKey: string = "";
|
||||
|
||||
public static create(pb?: pond.GrainBag): GrainBag {
|
||||
let my = new GrainBag();
|
||||
if (pb) {
|
||||
my.settings = pond.GrainBagSettings.fromObject(cloneDeep(or(pb.settings, {})));
|
||||
my.title = pb.name;
|
||||
my.objKey = pb.key;
|
||||
}
|
||||
return my;
|
||||
}
|
||||
|
||||
public static clone(other?: GrainBag): GrainBag {
|
||||
if (other) {
|
||||
return GrainBag.create(
|
||||
pond.GrainBag.fromObject({
|
||||
title: other.title,
|
||||
key: other.objKey,
|
||||
settings: cloneDeep(other.settings)
|
||||
})
|
||||
);
|
||||
}
|
||||
return GrainBag.create();
|
||||
}
|
||||
|
||||
public static any(data: any): GrainBag {
|
||||
return GrainBag.create(pond.GrainBag.fromObject(cloneDeep(data)));
|
||||
}
|
||||
|
||||
public key(): string {
|
||||
if (this) {
|
||||
return this.objKey;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public startLocation(): pond.Location {
|
||||
let start = pond.Location.create();
|
||||
start.latitude = this.settings.startLocation?.latitude ?? 0;
|
||||
start.longitude = this.settings.startLocation?.longitude ?? 0;
|
||||
return start;
|
||||
}
|
||||
|
||||
public endLocation(): pond.Location {
|
||||
let end = pond.Location.create();
|
||||
end.latitude = this.settings.endLocation?.latitude ?? 0;
|
||||
end.longitude = this.settings.endLocation?.longitude ?? 0;
|
||||
return end;
|
||||
}
|
||||
|
||||
public bushels(): number {
|
||||
return this.settings.currentBushels;
|
||||
}
|
||||
|
||||
public capacity(): number {
|
||||
return this.settings.bushelCapacity;
|
||||
}
|
||||
|
||||
public isCustom(): boolean {
|
||||
return this.settings.storageType !== pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN;
|
||||
}
|
||||
|
||||
public name(): string {
|
||||
if (this) {
|
||||
return this.title;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public fillPercent(): number {
|
||||
let fill = 0;
|
||||
if (this.settings.bushelCapacity > 0) {
|
||||
fill = Math.round((this.settings.currentBushels / this.settings.bushelCapacity) * 100);
|
||||
}
|
||||
return fill;
|
||||
}
|
||||
|
||||
public objectType(): pond.ObjectType {
|
||||
return pond.ObjectType.OBJECT_TYPE_GRAIN_BAG;
|
||||
}
|
||||
|
||||
public objectTypeString(): string {
|
||||
return "Grain Bag";
|
||||
}
|
||||
|
||||
public storage(): pond.BinStorage {
|
||||
return this.settings.storageType;
|
||||
}
|
||||
|
||||
public grain(): pond.Grain {
|
||||
return this.settings.supportedGrain;
|
||||
}
|
||||
|
||||
public customType(): string {
|
||||
return this.settings.customGrain;
|
||||
}
|
||||
|
||||
public subtype(): string {
|
||||
return this.settings.grainSubtype;
|
||||
}
|
||||
|
||||
public centerLocation(): pond.Location {
|
||||
let center = pond.Location.create();
|
||||
if (this.startLocation() && this.endLocation()) {
|
||||
center.longitude = (this.startLocation().longitude + this.endLocation().longitude) / 2;
|
||||
center.latitude = (this.startLocation().latitude + this.endLocation().latitude) / 2;
|
||||
}
|
||||
return center;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the bushels per tonne set in the bins settings, if not set will return 1
|
||||
* @returns 1 or bushels per tonne
|
||||
*/
|
||||
public bushelsPerTonne(): number {
|
||||
//trying to avoid a divide by 0 error by only returning a value greater than 0
|
||||
//since to get the weight you divide the current bushels by the bushels per tonne
|
||||
let bpt = 1;
|
||||
if (this.settings.currentBushels) {
|
||||
if (this.settings.bushelsPerTonne > 0) {
|
||||
bpt = this.settings.bushelsPerTonne;
|
||||
}
|
||||
}
|
||||
return bpt;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
export * from "./Backpack";
|
||||
export * from "./Bin";
|
||||
// export * from "./BinYard";
|
||||
export * from "./BinYard";
|
||||
export * from "./Component";
|
||||
export * from "./Device";
|
||||
export * from "./Firmware";
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ import GrainDescriber, { grainName } from "grain/GrainDescriber";
|
|||
import { useMobile, useWidth } from "hooks";
|
||||
import { Dictionary } from "lodash";
|
||||
// import { Bin, Interaction, BinYard as BinYardModel } from "models";
|
||||
import { Bin, Interaction } from "models";
|
||||
import { Bin, Interaction, BinYard as BinYardModel } from "models";
|
||||
// import { GrainBag } from "models/GrainBag";
|
||||
import { pond, quack } from "protobuf-ts/pond";
|
||||
import {
|
||||
|
|
@ -80,6 +80,7 @@ import { makeStyles } 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 { useHistory } from "react-router";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
|
|
@ -168,7 +169,7 @@ export default function Bins(props: Props) {
|
|||
const [grainBins, setGrainBins] = useState<Bin[]>([]);
|
||||
const [fertilizerBins, setFertilizerBins] = useState<Bin[]>([]);
|
||||
const [emptyBins, setEmptyBins] = useState<Bin[]>([]);
|
||||
// const [grainBags, setGrainBags] = useState<GrainBag[]>([]);
|
||||
const [grainBags, setGrainBags] = useState<GrainBag[]>([]);
|
||||
const [displayedInventory, setDisplayedInventory] = useState<GrainAmount[]>([]);
|
||||
// const [displayedFertilizer, setDisplayedFertilizer] = useState<GrainAmount[]>([]);
|
||||
const [carouselIndex, setCarouselIndex] = useState(0);
|
||||
|
|
@ -372,22 +373,22 @@ export default function Bins(props: Props) {
|
|||
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);
|
||||
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
|
||||
|
|
@ -637,7 +638,7 @@ export default function Bins(props: Props) {
|
|||
<Divider />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
{/*<BinsList
|
||||
<BinsList
|
||||
valDisplay={cardValDisplay}
|
||||
gridView={binView === "grid"}
|
||||
bins={fertilizerBins}
|
||||
|
|
@ -650,7 +651,7 @@ export default function Bins(props: Props) {
|
|||
setScrollTranslations({ ...scrollTranslations, fertilizer: newTranslation });
|
||||
}
|
||||
}}
|
||||
/>*/}
|
||||
/>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
|
@ -660,7 +661,7 @@ export default function Bins(props: Props) {
|
|||
<Divider />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
{/*<BinsList
|
||||
<BinsList
|
||||
valDisplay={cardValDisplay}
|
||||
gridView={binView === "grid"}
|
||||
bins={emptyBins}
|
||||
|
|
@ -673,7 +674,7 @@ export default function Bins(props: Props) {
|
|||
setScrollTranslations({ ...scrollTranslations, empty: newTranslation });
|
||||
}
|
||||
}}
|
||||
/>*/}
|
||||
/>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
|
@ -683,16 +684,16 @@ export default function Bins(props: Props) {
|
|||
};
|
||||
|
||||
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 (!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 (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue