Merge branch 'dev_environment' of gitlab.com:brandx/bxt-app into dev_environment

This commit is contained in:
Carter 2025-03-28 10:59:06 -06:00
commit 6d21ec0fc6
24 changed files with 2534 additions and 101 deletions

View file

@ -679,16 +679,16 @@ export default function BinyardDisplay(props: Props) {
className={classes.gridList} className={classes.gridList}
cols={ cols={
props.insert && !isMobile props.insert && !isMobile
? 3.25 ? 3
: width === "xs" : width === "xs"
? 3.25 ? 3
: width === "sm" : width === "sm"
? 5.5 ? 5
: width === "md" : width === "md"
? 6.5 ? 6
: width === "lg" : width === "lg"
? 7.5 ? 7
: 8.5 : 8
}> }>
{binMetrics && {binMetrics &&
binMetrics.grainInventory.map((inv, key) => ( binMetrics.grainInventory.map((inv, key) => (

View file

@ -0,0 +1,220 @@
import {
Grid2 as Grid,
IconButton,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Tooltip,
Theme,
Box
} from "@mui/material";
//import ObjectTeamsIcon from "@material-ui/icons/SupervisedUserCircle";
import FieldsIcon from "products/AgIcons/FieldsIcon";
import React, { useState } from "react";
import DeviceIcon from "products/Bindapt/BindaptIcon";
import { Device, Scope } from "models";
import ObjectTeams from "teams/ObjectTeams";
import { pond } from "protobuf-ts/pond";
import { Link, SupervisedUserCircle as ObjectTeamsIcon } from "@mui/icons-material";
//import { useHistory } from "react-router";
import { useNavigate } from "react-router-dom";
import { useThemeType } from "hooks";
import { makeStyles } from "@mui/styles";
interface Props {
actions: JSX.Element;
objectButton?: JSX.Element;
mapFunction?: () => void;
linkDeviceFunction?: () => void;
notesButton?: JSX.Element;
teamScope?: Scope;
permissions?: pond.Permission[];
devices?: Device[];
}
const useStyles = makeStyles((theme: Theme) => ({
avatar: {
color: useThemeType() === "light" ? theme.palette.common.black : theme.palette.common.white,
backgroundColor: "transparent",
width: theme.spacing(5),
height: theme.spacing(5),
border: "1px solid",
borderColor: theme.palette.divider
}
}));
export default function ObjectControls(props: Props) {
const {
mapFunction,
linkDeviceFunction,
teamScope,
permissions,
actions,
devices,
notesButton,
objectButton
} = props;
const [openTeams, setOpenTeams] = useState(false);
//const history = useHistory();
const navigate = useNavigate();
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const classes = useStyles();
// const goToDevice = (dev: Device) => {
// history.push("/devices/" + dev.id());
// };
const goToDevice = (dev: Device) => {
let path = "/devices/" + dev.id();
navigate(path);
};
const dialogs = () => {
return (
<React.Fragment>
{teamScope && permissions && (
<ObjectTeams
scope={teamScope}
label="Teams"
permissions={permissions}
isDialogOpen={openTeams}
refreshCallback={() => {}}
closeDialogCallback={() => {
setOpenTeams(false);
}}
/>
)}
</React.Fragment>
);
};
const deviceNavButton = () => {
if (devices) {
if (devices.length > 1) {
return (
<Tooltip key="devices" title="devices">
<IconButton
key="devButton"
onClick={(event: React.MouseEvent<HTMLButtonElement>) =>
setAnchorEl(event.currentTarget)
}
size="small"
style={{
marginTop: "0.3625rem",
marginBottom: "0.3625rem",
marginRight: "0.5rem"
}}
className={classes.avatar}
component="span">
<DeviceIcon />
</IconButton>
</Tooltip>
);
} else {
return devices.map(dev => (
<Tooltip key={dev.id()} title={dev.name()}>
<IconButton
key={dev.id()}
onClick={() => goToDevice(dev)}
size="small"
style={{
marginTop: "0.3625rem",
marginBottom: "0.3625rem",
marginRight: "0.5rem"
}}
className={classes.avatar}
component="span">
<DeviceIcon />
</IconButton>
</Tooltip>
));
}
}
};
const deviceMenu = () => {
return (
<Menu
id="groupMenu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
keepMounted
//classes={{ paper: classes.menuPaper }}
disableAutoFocusItem>
{devices &&
devices.map(dev => (
<MenuItem key={dev.id()} onClick={() => goToDevice(dev)}>
<ListItemIcon>
<DeviceIcon />
</ListItemIcon>
<ListItemText primary={dev.name()} />
</MenuItem>
))}
</Menu>
);
};
return (
<Box style={{ padding: 10 }}>
{dialogs()}
<Grid container direction="row">
{/* buttons on the left side start here */}
<Grid>
<Grid container direction="row">
{objectButton}
{notesButton}
{deviceNavButton()}
{deviceMenu()}
{mapFunction && (
<Grid>
<IconButton
onClick={mapFunction}
size="small"
style={{
marginTop: "0.3625rem",
marginBottom: "0.3625rem",
marginRight: "0.5rem"
}}
className={classes.avatar}
component="span">
<FieldsIcon />
</IconButton>
</Grid>
)}
{teamScope && permissions && (
<Grid>
<IconButton
onClick={() => {
setOpenTeams(true);
}}
size="small"
style={{
marginTop: "0.3625rem",
marginBottom: "0.3625rem",
marginRight: "0.5rem"
}}
className={classes.avatar}
component="span">
<ObjectTeamsIcon />
</IconButton>
</Grid>
)}
</Grid>
</Grid>
{/* bottons on the right side start here */}
<Grid>
{linkDeviceFunction && (
<Tooltip title="Link Devices">
<IconButton onClick={linkDeviceFunction}>
<Link />
</IconButton>
</Tooltip>
)}
{actions}
</Grid>
</Grid>
</Box>
);
}

181
src/field/FieldActions.tsx Normal file
View file

@ -0,0 +1,181 @@
import {
IconButton,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Theme
} from "@mui/material";
import { blue } from "@mui/material/colors";
import RemoveSelfIcon from "@mui/icons-material/ExitToApp";
import MoreIcon from "@mui/icons-material/MoreVert";
import ShareObjectIcon from "@mui/icons-material/Share";
import ObjectUsersIcon from "@mui/icons-material/AccountCircle";
import ObjectTeamsIcon from "@mui/icons-material/SupervisedUserCircle";
import { pond } from "protobuf-ts/pond";
import React, { useState } from "react";
import ObjectUsers from "user/ObjectUsers";
import RemoveSelfFromObject from "user/RemoveSelfFromObject";
import ShareObject from "user/ShareObject";
import { isOffline } from "utils/environment";
import ObjectTeams from "teams/ObjectTeams";
import { Field, fieldScope } from "models";
import { makeStyles } from "@mui/styles";
const useStyles = makeStyles((theme: Theme) => ({
shareIcon: {
color: blue["500"],
"&:hover": {
color: blue["600"]
}
},
removeIcon: {
color: "var(--status-alert)"
},
red: {
color: "var(--status-alert)"
}
}));
interface Props {
field: Field;
permissions: pond.Permission[];
refreshCallback: () => void;
}
interface OpenState {
share: boolean;
users: boolean;
teams: boolean;
settings: boolean;
removeSelf: boolean;
}
export default function FieldActions(props: Props) {
const classes = useStyles();
const { field, permissions, refreshCallback } = props;
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const [openState, setOpenState] = useState<OpenState>({
share: false,
users: false,
teams: false,
settings: false,
removeSelf: false
});
const groupMenu = () => {
const canShare = permissions.includes(pond.Permission.PERMISSION_SHARE);
const canManageUsers = permissions.includes(pond.Permission.PERMISSION_USERS);
return (
<Menu
id="groupMenu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
keepMounted
disableAutoFocusItem>
{!isOffline() && canShare && (
<MenuItem
dense
onClick={() => {
setOpenState({ ...openState, share: true });
setAnchorEl(null);
}}>
<ListItemIcon>
<ShareObjectIcon className={classes.shareIcon} />
</ListItemIcon>
<ListItemText primary="Share" />
</MenuItem>
)}
{!isOffline() && canManageUsers && (
<MenuItem
dense
onClick={() => {
setOpenState({ ...openState, users: true });
setAnchorEl(null);
}}>
<ListItemIcon>
<ObjectUsersIcon />
</ListItemIcon>
<ListItemText primary="Users" />
</MenuItem>
)}
{!isOffline() && canManageUsers && (
<MenuItem
dense
onClick={() => {
setOpenState({ ...openState, teams: true });
setAnchorEl(null);
}}>
<ListItemIcon>
<ObjectTeamsIcon />
</ListItemIcon>
<ListItemText primary="Teams" />
</MenuItem>
)}
<MenuItem
dense
onClick={() => {
setOpenState({ ...openState, removeSelf: true });
setAnchorEl(null);
}}>
<ListItemIcon>
<RemoveSelfIcon className={classes.red} />
</ListItemIcon>
<ListItemText primary="Leave" />
</MenuItem>
</Menu>
);
};
const dialogs = () => {
const key = field.key();
const label = field.fieldName();
return (
<React.Fragment>
<ShareObject
scope={fieldScope(key)}
label={label}
permissions={permissions}
isDialogOpen={openState.share}
closeDialogCallback={() => setOpenState({ ...openState, share: false })}
/>
<ObjectUsers
scope={fieldScope(key)}
label={label}
permissions={permissions}
isDialogOpen={openState.users}
closeDialogCallback={() => setOpenState({ ...openState, users: false })}
refreshCallback={refreshCallback}
/>
<RemoveSelfFromObject
scope={fieldScope(key)}
label={label}
isDialogOpen={openState.removeSelf}
closeDialogCallback={() => setOpenState({ ...openState, removeSelf: false })}
/>
</React.Fragment>
);
};
return (
<React.Fragment>
<IconButton
aria-owns={anchorEl ? "groupMenu" : undefined}
aria-haspopup="true"
onClick={(event: React.MouseEvent<HTMLButtonElement>) => setAnchorEl(event.currentTarget)}>
<MoreIcon />
</IconButton>
{groupMenu()}
{dialogs()}
<ObjectTeams
scope={fieldScope(field.key())}
label="Teams"
permissions={permissions}
isDialogOpen={openState.teams}
refreshCallback={refreshCallback}
closeDialogCallback={() => setOpenState({ ...openState, teams: false })}
/>
</React.Fragment>
);
}

293
src/field/FieldSettings.tsx Normal file
View file

@ -0,0 +1,293 @@
import {
Box,
Button,
DialogActions,
DialogContent,
DialogTitle,
Grid2 as Grid,
Switch,
TextField,
Typography
} from "@mui/material";
import ColourPicker from "common/ColourPicker";
import ResponsiveDialog from "common/ResponsiveDialog";
import React from "react";
import { useState } from "react";
import { useEffect } from "react";
import { useFieldAPI } from "providers";
import { pond } from "protobuf-ts/pond";
import { useSnackbar } from "hooks";
import { Field } from "models";
import { GrainOptions, ToGrainOption } from "grain";
import SearchSelect from "common/SearchSelect";
import { clone } from "lodash";
import GrainDescriber from "grain/GrainDescriber";
interface Props {
selectedField?: Field;
borders?: pond.Shape[];
//holes?: pond.Shape[];
open: boolean;
onClose: () => void;
updateFields?: (key: string, settings: pond.FieldSettings) => void;
removeField?: (field: Field) => void;
}
export default function FieldSettings(props: Props) {
const { selectedField, onClose, removeField, updateFields, borders } = props;
const [fieldName, setFieldName] = useState("");
const [acres, setAcres] = useState<number>(0);
const [cropType, setCropType] = useState<pond.Grain>(pond.Grain.GRAIN_INVALID);
const [grainSubtype, setGrainSubtype] = useState("");
const [landLocation, setLandLocation] = useState("");
const fieldAPI = useFieldAPI();
const [featColor, setFeatColor] = useState("green");
const { openSnack } = useSnackbar();
const [isCustom, setIsCustom] = useState(false);
const [customType, setCustomType] = useState("");
const [bushelsPerTonne, setBushelsPerTonne] = useState("1");
useEffect(() => {
if (selectedField) {
setFieldName(selectedField.fieldName());
setCropType(selectedField.crop());
setAcres(selectedField.settings.acres);
setLandLocation(selectedField.landLoc() ? selectedField.landLoc() : "");
setGrainSubtype(selectedField.subtype());
setFeatColor(selectedField.settings.fieldGeoData?.colour ?? "green");
setBushelsPerTonne(selectedField.bushelsPerTonne().toString());
if (selectedField.grain() === pond.Grain.GRAIN_CUSTOM) {
setIsCustom(true);
setCustomType(selectedField.customType());
} else {
setIsCustom(false);
}
} else {
setFieldName("");
setCropType(0);
setLandLocation("");
setFeatColor("");
}
}, [selectedField]);
const checkValidation = () => {
return fieldName.length > 0 && cropType !== 0 ? false : true;
};
const submit = () => {
if (selectedField) {
//update existing field
let settings = selectedField.settings;
settings.fieldName = fieldName;
settings.crop = cropType;
if (settings.fieldGeoData) {
settings.fieldGeoData.colour = featColor;
}
settings.customGrain = customType;
settings.grainSubtype = grainSubtype;
settings.landLocation = landLocation;
settings.acres = acres;
fieldAPI
.updateField(selectedField.key(), settings)
.then(resp => {
let newField = clone(selectedField);
newField.settings = settings;
openSnack("Updated Field Settings");
updateFields && updateFields(selectedField.key(), selectedField.settings);
onClose();
})
.catch(err => {
openSnack("Failed to Update Field Settings");
});
} else {
//create new field
let geo = pond.GeoData.create();
let settings: pond.FieldSettings = pond.FieldSettings.create();
//set the basic settings
settings.fieldName = fieldName;
settings.crop = cropType;
settings.customGrain = customType;
settings.grainSubtype = grainSubtype;
settings.landLocation = landLocation;
settings.acres = acres;
//set the geodata
geo.colour = featColor;
geo.title = fieldName;
if (borders) {
geo.geoShape = borders.length > 1 ? "MultiPolygon" : "Polygon";
geo.shapes = borders;
}
settings.fieldGeoData = geo;
fieldAPI
.addField(settings)
.then(resp => {
updateFields && updateFields(resp.data.field, settings);
openSnack("New Field Created");
})
.catch(err => {
openSnack("failed to add field");
});
onClose();
}
};
const deleteField = () => {
if (selectedField) {
fieldAPI
.removeField(selectedField?.key())
.then(resp => {
openSnack("Field Mapping Deleted");
//setActiveStep(0);
removeField && removeField(selectedField);
onClose();
})
.catch(err => {
openSnack("Failed to delete field mapping");
});
}
};
const fieldInformation = () => {
return (
<Box>
<Box>
<DialogTitle>Field Information</DialogTitle>
<TextField
value={fieldName}
margin="normal"
color="primary"
label="Field Name"
type="text"
fullWidth
autoFocus
onChange={e => setFieldName(e.target.value)}
/>
<TextField
value={acres}
margin="normal"
color="primary"
label="Acres"
type="number"
fullWidth
autoFocus
//helperText="If you are unsure of the acres they will be calculated automatically based on the drawn field boundaries"
onChange={e => setAcres(+e.target.value)}
/>
{/* Select box for grain type */}
<Grid container alignItems="center">
<Grid>Supported Grain</Grid>
<Grid>
<Switch
color="default"
value={isCustom}
checked={isCustom}
onChange={(_, checked) => {
setIsCustom(checked);
if (checked) {
setCropType(pond.Grain.GRAIN_CUSTOM);
} else {
setCustomType("");
}
}}
name="storage"
/>
</Grid>
<Grid>Custom Grain</Grid>
</Grid>
{isCustom ? (
<React.Fragment>
<TextField
label="Grain Type"
margin="normal"
style={{ marginTop: 0 }}
type="text"
fullWidth
value={customType}
color="primary"
onChange={e => setCustomType(e.target.value)}
/>
<TextField
label="Bushels Per Tonne"
margin="normal"
style={{ marginTop: 0 }}
type="number"
fullWidth
value={bushelsPerTonne}
color="primary"
onChange={e => setBushelsPerTonne(e.target.value)}
/>
</React.Fragment>
) : (
<SearchSelect
label="Type"
selected={ToGrainOption(cropType)}
changeSelection={option => {
if (option) {
let grainType = pond.Grain[option.value as keyof typeof pond.Grain];
setCropType(grainType);
setBushelsPerTonne(GrainDescriber(grainType).bushelsPerTonne.toString());
}
}}
group
options={GrainOptions()}
/>
)}
<TextField
label="Grain Variant"
margin="normal"
type="text"
fullWidth
value={grainSubtype}
color="primary"
onChange={e => setGrainSubtype(e.target.value)}
/>
<TextField
label="Land Location"
margin="normal"
type="text"
fullWidth
value={landLocation}
color="primary"
onChange={e => setLandLocation(e.target.value)}
/>
<Box marginTop={3}>
Field Colour
<ColourPicker onChange={color => setFeatColor(color)} />
</Box>
</Box>
<DialogActions>
<Grid container direction="row">
<Grid>
{selectedField && (
<Button onClick={deleteField} variant="contained" style={{ background: "red" }}>
<Typography>Delete Field</Typography>
</Button>
)}
</Grid>
<Grid>
<Button onClick={onClose} color="primary">
Cancel
</Button>
<Button
variant="contained"
onClick={submit}
disabled={checkValidation()}
color="primary">
Submit
</Button>
</Grid>
</Grid>
</DialogActions>
</Box>
);
};
return (
<ResponsiveDialog fullWidth fullScreen={false} open={props.open} onClose={onClose}>
<DialogContent>{fieldInformation()}</DialogContent>
</ResponsiveDialog>
);
}

View file

@ -53,6 +53,29 @@ const useStyles = makeStyles((theme: Theme) => {
position: "relative", position: "relative",
width: "auto", width: "auto",
marginRight: theme.spacing(1) marginRight: theme.spacing(1)
},
sliderRoot: {},
sliderThumb: {
left: 23
},
mobileSliderThumb: {
left: 28
},
sliderTrack: {
height: "100%",
},
sliderRail: {
height: "100%",
},
sliderValLabel: {
// left: -20,
height: 30,
width: 30,
top: -15,
background: "transparent"
},
sliderLabel: {
background: "gold"
} }
}); });
}); });
@ -210,7 +233,7 @@ export default function GrainBagVisualizer(props: Props) {
position="relative" position="relative"
height={1} height={1}
width={1} width={1}
bgcolor={theme.palette.background.paper} //bgcolor={theme.palette.background.paper}
display="flex" display="flex"
justifyContent="center" justifyContent="center"
alignItems="center" alignItems="center"
@ -256,7 +279,14 @@ export default function GrainBagVisualizer(props: Props) {
<Slider <Slider
orientation="vertical" orientation="vertical"
value={fillLevel} value={fillLevel}
style={{ color: sliderColour }} classes={{
root: classes.sliderRoot,
rail: classes.sliderRail,
track: classes.sliderTrack,
thumb: isMobile ? classes.mobileSliderThumb : classes.sliderThumb,
valueLabel: classes.sliderValLabel,
}}
style={{height: "100%", color: sliderColour }}
min={0} min={0}
max={100} max={100}
valueLabelDisplay="auto" valueLabelDisplay="auto"
@ -292,14 +322,14 @@ export default function GrainBagVisualizer(props: Props) {
return ( return (
<Card raised className={classes.cardContent}> <Card raised className={classes.cardContent}>
<Grid container justifyContent="flex-start" alignItems="stretch"> <Grid container justifyContent="flex-start" alignItems="stretch" width={"100%"}>
<Grid style={{ position: "relative" }}> <Grid style={{ position: "relative" }} size={6}>
{overview()} {overview()}
</Grid> </Grid>
<Grid style={{ position: "relative" }}> <Grid style={{ position: "relative" }} size={5}>
{visual()} {visual()}
</Grid> </Grid>
<Grid>{controls()}</Grid> <Grid size={1}>{controls()}</Grid>
</Grid> </Grid>
{/* dialog for grain inventory transactions */} {/* dialog for grain inventory transactions */}
<GrainTransaction <GrainTransaction

View file

@ -0,0 +1,108 @@
import {
Button,
DialogActions,
DialogContent,
DialogTitle,
MenuItem,
TextField
} from "@mui/material";
import ResponsiveDialog from "common/ResponsiveDialog";
import { Field, HarvestPlan, /*Task*/ } from "models";
import moment from "moment";
import { pond } from "protobuf-ts/pond";
import { useHarvestPlanAPI/*, useTaskAPI*/ } from "providers";
import { useCallback, useEffect, useState } from "react";
interface Props {
open: boolean;
close: () => void;
fields: Field[];
plan: HarvestPlan;
}
export default function DuplicateHarvestPlan(props: Props) {
const { open, close, fields, plan } = props;
const [newFieldIndex, setNewFieldIndex] = useState(0);
//const [planTasks, setPlanTasks] = useState<Task[]>([]);
const [title, setTitle] = useState("");
const harvestPlanAPI = useHarvestPlanAPI();
//const taskAPI = useTaskAPI();
const [ready, setReady] = useState(false);
// const loadTasks = useCallback(() => {
// taskAPI.listTasks(50, 0, "asc", "start", plan.key()).then(resp => {
// setPlanTasks(resp.data.tasks.map(t => Task.any(t)));
// setReady(true);
// });
// }, [taskAPI, plan]);
// useEffect(() => {
// loadTasks();
// }, [loadTasks]);
const duplicate = () => {
let planSettings = HarvestPlan.clone(plan).settings;
planSettings.field = fields[newFieldIndex].key();
planSettings.createDate = moment.now();
if (title !== "") {
planSettings.title = title;
}
harvestPlanAPI.addHarvestPlan(planSettings).then(resp => {
// let tasks = planTasks;
// tasks.forEach(task => {
// task.settings.objectKey = resp.data.harvestPlan;
// taskAPI.addTask(task.settings);
// });
});
close();
};
return (
<ResponsiveDialog open={open} onClose={close}>
<DialogTitle>Duplicate Crop Plan</DialogTitle>
<DialogContent>
<TextField
margin="normal"
fullWidth
select
label="Field"
value={newFieldIndex}
onChange={e => setNewFieldIndex(+e.target.value)}
color="primary">
{fields.map((option, i) => (
<MenuItem key={option.key()} value={i}>
{option.fieldName()}
</MenuItem>
))}
</TextField>
<TextField
fullWidth
margin="normal"
type="text"
label="Title"
value={title}
helperText={"Leave blank to use the same title*"}
onChange={e => setTitle(e.target.value)}
/>
</DialogContent>
<DialogActions>
<Button
onClick={close}
variant="contained"
style={{ color: "black", backgroundColor: "red" }}>
Cancel
</Button>
<Button
onClick={duplicate}
variant="contained"
color="primary"
disabled={
!ready ||
(fields[newFieldIndex] &&
!fields[newFieldIndex].permissions.includes(pond.Permission.PERMISSION_WRITE))
}>
Duplicate
</Button>
</DialogActions>
</ResponsiveDialog>
);
}

View file

@ -0,0 +1,235 @@
import {
Box,
Button,
IconButton,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Theme
} from "@mui/material";
import MoreIcon from "@mui/icons-material/MoreVert";
import ShareObjectIcon from "@mui/icons-material/Share";
import ObjectUsersIcon from "@mui/icons-material/AccountCircle";
import ObjectTeamsIcon from "@mui/icons-material/SupervisedUserCircle";
import { pond } from "protobuf-ts/pond";
import React, { useState } from "react";
import ObjectUsers from "user/ObjectUsers";
import ShareObject from "user/ShareObject";
import { isOffline } from "utils/environment";
import ObjectTeams from "teams/ObjectTeams";
import { planScope, HarvestPlan, Field } from "models";
import HarvestSettings from "./HarvestSettings";
import DuplicateHarvestPlan from "./DuplicateHarvestPlan";
import { Edit } from "@mui/icons-material";
import { makeStyles } from "@mui/styles";
import { blue } from "@mui/material/colors";
const useStyles = makeStyles((theme: Theme) => ({
shareIcon: {
color: blue["500"],
"&:hover": {
color: blue["600"]
}
},
removeIcon: {
color: "var(--status-alert)"
},
red: {
color: "var(--status-alert)"
}
}));
interface Props {
plan: HarvestPlan;
planField: Field;
fieldList: Field[];
permissions: pond.Permission[];
refreshCallback: (updatedPlan?: HarvestPlan) => void;
hideNew?: boolean;
}
interface OpenState {
share: boolean;
users: boolean;
teams: boolean;
settings: boolean;
removeSelf: boolean;
duplicate: boolean;
}
export default function HarvestPlanActions(props: Props) {
const classes = useStyles();
const { plan, planField, fieldList, permissions, refreshCallback, hideNew } = props;
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const [openState, setOpenState] = useState<OpenState>({
share: false,
users: false,
teams: false,
settings: false,
removeSelf: false,
duplicate: false
});
const [updatePlan, setUpdatePlan] = useState(false);
const groupMenu = () => {
const canShare = permissions.includes(pond.Permission.PERMISSION_SHARE);
const canManageUsers = permissions.includes(pond.Permission.PERMISSION_USERS);
return (
<Menu
id="groupMenu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
keepMounted
disableAutoFocusItem>
{!isOffline() &&
planField.permissions.includes(pond.Permission.PERMISSION_WRITE) &&
!hideNew && (
<MenuItem
dense
onClick={() => {
setUpdatePlan(false);
setOpenState({ ...openState, settings: true });
setAnchorEl(null);
}}>
<ListItemIcon></ListItemIcon>
<ListItemText primary="New Plan" />
</MenuItem>
)}
{!isOffline() && planField.permissions.includes(pond.Permission.PERMISSION_WRITE) && (
<MenuItem
dense
onClick={() => {
setOpenState({ ...openState, duplicate: true });
setAnchorEl(null);
}}>
<ListItemIcon></ListItemIcon>
<ListItemText primary="Duplicate Plan" />
</MenuItem>
)}
{!isOffline() && canShare && (
<MenuItem
dense
onClick={() => {
setOpenState({ ...openState, share: true });
setAnchorEl(null);
}}>
<ListItemIcon>
<ShareObjectIcon className={classes.shareIcon} />
</ListItemIcon>
<ListItemText primary="Share" />
</MenuItem>
)}
{!isOffline() && canManageUsers && (
<MenuItem
dense
onClick={() => {
setOpenState({ ...openState, users: true });
setAnchorEl(null);
}}>
<ListItemIcon>
<ObjectUsersIcon />
</ListItemIcon>
<ListItemText primary="Users" />
</MenuItem>
)}
{!isOffline() && canManageUsers && (
<MenuItem
dense
onClick={() => {
setOpenState({ ...openState, teams: true });
setAnchorEl(null);
}}>
<ListItemIcon>
<ObjectTeamsIcon />
</ListItemIcon>
<ListItemText primary="Teams" />
</MenuItem>
)}
</Menu>
);
};
const dialogs = () => {
const key = plan.key();
const label = plan.settings.title;
return (
<React.Fragment>
<HarvestSettings
field={planField}
update={updatePlan}
open={openState.settings}
plan={plan.key() !== "" ? plan : undefined}
close={(refresh, updatedPlan) => {
setOpenState({ ...openState, settings: false });
if (refresh) {
refreshCallback(updatedPlan);
}
}}
/>
<DuplicateHarvestPlan
open={openState.duplicate}
close={() => {
setOpenState({ ...openState, duplicate: false });
refreshCallback();
}}
fields={fieldList}
plan={plan}
/>
<ShareObject
scope={planScope(key)}
label={label}
permissions={permissions}
isDialogOpen={openState.share}
closeDialogCallback={() => setOpenState({ ...openState, share: false })}
/>
<ObjectUsers
scope={planScope(key)}
label={label}
permissions={permissions}
isDialogOpen={openState.users}
closeDialogCallback={() => setOpenState({ ...openState, users: false })}
refreshCallback={refreshCallback}
/>
<ObjectTeams
scope={planScope(key)}
label="Teams"
permissions={permissions}
isDialogOpen={openState.teams}
refreshCallback={refreshCallback}
closeDialogCallback={() => setOpenState({ ...openState, teams: false })}
/>
</React.Fragment>
);
};
return (
<React.Fragment>
<Box style={{ display: "flex", justifyContent: "space-between" }}>
{plan.key() !== "" && (
<Button
variant="contained"
color="primary"
onClick={() => {
setUpdatePlan(true);
setOpenState({ ...openState, settings: true });
}}>
<Edit /> Edit or Add Activity
</Button>
)}
<IconButton
aria-owns={anchorEl ? "groupMenu" : undefined}
aria-haspopup="true"
onClick={(event: React.MouseEvent<HTMLButtonElement>) =>
setAnchorEl(event.currentTarget)
}>
<MoreIcon />
</IconButton>
</Box>
{groupMenu()}
{dialogs()}
</React.Fragment>
);
}

View file

@ -0,0 +1,209 @@
import {
Box,
Grid2 as Grid,
Theme,
Typography,
IconButton,
LinearProgress,
Drawer
} from "@mui/material";
import { Notes } from "@mui/icons-material";
import GrainDescriber from "grain/GrainDescriber";
import { Field, HarvestPlan } from "models";
import React, { useState } from "react";
import { getThemeType } from "theme";
import Chat from "chat/Chat";
import { pond } from "protobuf-ts/pond";
import { useMobile } from "hooks";
import HarvestPlanActions from "./HarvestPlanActions";
import { makeStyles } from "@mui/styles";
interface Props {
plan: HarvestPlan;
permissions: pond.Permission[];
planField: Field;
loading: boolean;
fieldList: Field[];
changePlan: (updatedPlan?: HarvestPlan) => void;
}
const useStyles = makeStyles((theme: Theme) => ({
dark: {
backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(50, 50, 50)",
padding: 5
},
light: {
backgroundColor: getThemeType() === "light" ? "rgb(235, 235, 235)" : "rgb(60, 60, 60)",
padding: 5
},
avatar: {
color: getThemeType() === "light" ? theme.palette.common.black : theme.palette.common.white,
backgroundColor: "transparent",
width: theme.spacing(5),
height: theme.spacing(5),
border: "1px solid",
borderColor: theme.palette.divider
}
}));
export default function HarvestPlanDisplay(props: Props) {
const { plan, loading, planField, fieldList, changePlan, permissions } = props;
const classes = useStyles();
const [openNote, setOpenNote] = useState(false);
const isMobile = useMobile();
const planTable = () => {
return (
<React.Fragment>
<Grid container direction="column">
<Grid className={classes.dark}>
<Box style={{ display: "flex", justifyContent: "space-between" }}>
<Typography variant="subtitle1">Break Even Yield:</Typography>
<Typography variant="subtitle1">
{(
(plan.totalEquipmentCost() + plan.totalMaterialCost()) /
plan.bushelPrice()
).toFixed(2)}{" "}
BU
</Typography>
</Box>
</Grid>
<Grid className={classes.light}>
<Box style={{ display: "flex", justifyContent: "space-between" }}>
<Typography variant="subtitle1">Break Even Sales Price</Typography>
<Typography variant="subtitle1">
$
{(
(plan.totalEquipmentCost() + plan.totalMaterialCost()) /
plan.yieldTarget()
).toFixed(2)}
</Typography>
</Box>
</Grid>
</Grid>
<Grid container direction="column" style={{ marginTop: 20 }}>
<Grid className={classes.dark}>
<Box style={{ display: "flex", justifyContent: "space-between" }}>
<Typography variant="subtitle1">Harvest Year:</Typography>
<Typography variant="subtitle1">{plan.harvestYear()}</Typography>
</Box>
</Grid>
<Grid className={classes.light}>
<Box style={{ display: "flex", justifyContent: "space-between" }}>
<Typography variant="subtitle1">Title:</Typography>
<Typography variant="subtitle1">{plan.settings.title}</Typography>
</Box>
</Grid>
<Grid className={classes.dark}>
<Box style={{ display: "flex", justifyContent: "space-between" }}>
<Typography variant="subtitle1">Grain:</Typography>
<Typography variant="subtitle1">{GrainDescriber(plan.cropType()).name}</Typography>
</Box>
</Grid>
<Grid className={classes.light}>
<Box style={{ display: "flex", justifyContent: "space-between" }}>
<Typography variant="subtitle1">Variant:</Typography>
<Typography variant="subtitle1">{plan.grainType()}</Typography>
</Box>
</Grid>
<Grid className={classes.dark}>
<Box style={{ display: "flex", justifyContent: "space-between" }}>
<Typography variant="subtitle1">Bushel Price:</Typography>
<Typography variant="subtitle1">${plan.bushelPrice()}</Typography>
</Box>
</Grid>
<Grid className={classes.light}>
<Box style={{ display: "flex", justifyContent: "space-between" }}>
<Typography variant="subtitle1">Yield Target:</Typography>
<Typography variant="subtitle1">{plan.yieldTarget()}</Typography>
</Box>
</Grid>
<Grid className={classes.dark}>
<Box style={{ display: "flex", justifyContent: "space-between" }}>
<Typography variant="subtitle1">Total Cost (acre):</Typography>
<Typography variant="subtitle1">
${(plan.totalEquipmentCost() + plan.totalMaterialCost()).toFixed(2)}
</Typography>
</Box>
</Grid>
<Grid className={classes.light}>
<Box style={{ display: "flex", justifyContent: "space-between" }}>
<Typography variant="subtitle1">Total Cost:</Typography>
<Typography variant="subtitle1">
$
{(
(plan.totalEquipmentCost() + plan.totalMaterialCost()) *
planField.acres()
).toFixed(2)}
</Typography>
</Box>
</Grid>
</Grid>
</React.Fragment>
);
};
const noteDrawer = () => {
return (
<Drawer
anchor={isMobile ? "bottom" : "right"}
open={openNote}
onClose={() => setOpenNote(false)}>
<Box height={isMobile ? "50vh" : "100vh"} padding={2}>
<Typography style={{ fontWeight: 650 }}>Notes</Typography>
<Chat type={pond.NoteType.NOTE_TYPE_HARVEST_PLAN} objectKey={plan.key()} />
</Box>
</Drawer>
);
};
return (
<React.Fragment>
{loading ? (
<Box paddingTop="20px">
<LinearProgress color="primary" />
</Box>
) : (
<React.Fragment>
<IconButton
disabled={plan.key() === ""}
className={classes.avatar}
onClick={() => setOpenNote(true)}>
<Notes />
</IconButton>
<Box style={{ display: "flex", justifyContent: "space-between" }}>
<Typography
variant="h5"
style={{ fontWeight: 700, marginTop: "auto", marginBottom: "auto" }}>
Crop Plan{plan.key() !== "" ? " - " + plan.settings.title : " - None Found"}
</Typography>
{!isMobile && permissions.includes(pond.Permission.PERMISSION_WRITE) && (
<HarvestPlanActions
planField={planField}
fieldList={fieldList}
plan={plan}
permissions={permissions}
refreshCallback={updatedPlan => {
changePlan(updatedPlan);
}} //possibly need to re-load plan here
/>
)}
</Box>
{isMobile && permissions.includes(pond.Permission.PERMISSION_WRITE) && (
<HarvestPlanActions
planField={planField}
fieldList={fieldList}
plan={plan}
permissions={permissions}
refreshCallback={updatedPlan => {
changePlan(updatedPlan);
}} //possibly need to re-load plan here
/>
)}
{plan.key() !== "" && planTable()}
</React.Fragment>
)}
{noteDrawer()}
</React.Fragment>
);
}

View file

@ -0,0 +1,728 @@
import {
Box,
Button,
DialogActions,
DialogContent,
DialogTitle,
Grid2 as Grid,
InputAdornment,
MenuItem,
Tab,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Tabs,
TextField,
Theme,
Tooltip,
Typography
} from "@mui/material";
import { makeStyles } from "@mui/styles";
import ResponsiveDialog from "common/ResponsiveDialog";
import { useMobile, useSnackbar } from "hooks";
import { Field, HarvestPlan/*, Task*/ } from "models";
import moment from "moment";
import { pond } from "protobuf-ts/pond";
import { useHarvestPlanAPI/*, useTaskAPI*/ } from "providers";
import React, { useCallback, useEffect, useState } from "react";
//import TaskSettings from "tasks/TaskSettings";
interface Props {
open: boolean;
close: (reload?: boolean, updatedPlan?: HarvestPlan) => void;
field: Field;
update?: boolean;
plan?: HarvestPlan;
}
const cropOptions = [
{
value: 2,
label: "Barley"
},
{
value: 4,
label: "Canola"
},
{
value: 5,
label: "Corn"
},
{
value: 6,
label: "Oats"
},
{
value: 7,
label: "Peanuts"
},
{
value: 8,
label: "Rapeseed"
},
{
value: 9,
label: "Long Grain Rice"
},
{
value: 10,
label: "Medium Grain Rice"
},
{
value: 11,
label: "Short Grain Rice"
},
{
value: 12,
label: "Sorghum"
},
{
value: 13,
label: "Soybeans"
},
{
value: 14,
label: "Sunflower"
},
{
value: 3,
label: "Buckwheat"
},
{
value: 15,
label: "Durum Wheat"
},
{
value: 16,
label: "Hard Red Wheat"
},
{
value: 17,
label: "Unretted Flax"
},
{
value: 18,
label: "Dew Retted Flax"
},
{
value: 19,
label: "Yellow Peas"
},
{
value: 20,
label: "Robin Lentils"
},
{
value: 21,
label: "Redberry Lentils"
},
{
value: 22,
label: "Blaze Lentils"
}
];
const useStyles = makeStyles((theme: Theme) => ({
stepper: {
padding: theme.spacing(0.5)
},
secondaryColor: {
color: theme.palette.secondary.main
},
textSecondaryColor: {
color: theme.palette.text.secondary
},
bottomSpacing: {
marginBottom: theme.spacing(1)
},
tabs: {
width: "100%",
boxSizing: "border-box",
flexShrink: 1
},
tabSmall: {
width: "100%",
boxSizing: "border-box",
flexShrink: 1,
minWidth: "48px"
},
tab: {
width: "16%",
minWidth: "16%"
},
buttonSpacing: {
margin: 5
}
}));
const steps = ["Pre-Seeding", "Seeding", "Post-Seeding", "Harvest", "Fall"];
export default function HarvestSettings(props: Props) {
const { open, plan, close, field, update } = props;
const [planKey, setPlanKey] = useState<string>();
const [newPlan, setNewPlan] = useState(HarvestPlan.create());
const [activeStep, setActiveStep] = useState(0);
const isMobile = useMobile();
const classes = useStyles();
//const [planTasks, setPlanTasks] = useState<Task[]>([]);
//const taskAPI = useTaskAPI();
const harvestPlanAPI = useHarvestPlanAPI();
const [title, setTitle] = useState("");
const [cropType, setCropType] = useState(0);
const [customCropType, setCustomCropType] = useState("");
const [useCustom, setUseCustom] = useState(false);
const [variety, setVariety] = useState("");
const [harvestYear, setHarvestYear] = useState(new Date().getFullYear());
const [targetYield, setTargetYield] = useState(0);
const [price, setPrice] = useState("");
const [newTaskDialog, setNewTaskDialog] = useState(false);
const [type, setType] = useState("");
//const [taskToEdit, setTaskToEdit] = useState<Task>();
const { openSnack } = useSnackbar();
const [deleteOpen, setDeleteOpen] = useState(false);
// const loadTasks = useCallback(() => {
// if (planKey) {
// taskAPI.getMultiTasks([planKey]).then(resp => {
// setPlanTasks(resp.data.tasks.map(t => Task.any(t)));
// });
// }
// }, [taskAPI, planKey]);
// useEffect(() => {
// loadTasks();
// }, [loadTasks]);
useEffect(() => {
if (!plan || (plan && plan.permissions.includes(pond.Permission.PERMISSION_WRITE))) {
setActiveStep(0);
} else {
setActiveStep(1);
}
setPlanKey(undefined);
setTitle("");
setVariety("");
setHarvestYear(new Date().getFullYear());
setTargetYield(0);
setPrice("");
if (update && plan) {
setPlanKey(plan.key());
setTitle(plan.settings.title);
setCropType(plan.cropType());
setCustomCropType(plan.customCropType());
setVariety(plan.grainType());
setHarvestYear(plan.harvestYear());
setTargetYield(plan.yieldTarget());
setPrice(plan.settings.bushelPrice.toFixed(2).toString());
} else {
setCropType(field.crop());
setCustomCropType(field.customType());
setUseCustom(field.crop() === pond.Grain.GRAIN_CUSTOM);
}
}, [plan, field, update]);
const closeDialog = (reload?: boolean, updatedPlan?: HarvestPlan) => {
close(reload, updatedPlan);
};
// const showTasks = (type: string) => {
// let tasks: Task[] = [];
// let costTotal = 0;
// let secondaryCostTotal = 0;
// planTasks.forEach(task => {
// if (task.settings.type === type) {
// tasks.push(task);
// costTotal += task.cost();
// secondaryCostTotal += task.secondaryCost();
// }
// });
// let frag = (
// <React.Fragment>
// <Table>
// <TableHead>
// <TableRow>
// <TableCell>Title</TableCell>
// <TableCell>Description</TableCell>
// {!isMobile && <TableCell>Material Cost(acre)</TableCell>}
// {!isMobile && <TableCell>Equipment Cost(acre)</TableCell>}
// <TableCell>Total(acre)</TableCell>
// {!isMobile && <TableCell>Start -End</TableCell>}
// <TableCell>Edit</TableCell>
// </TableRow>
// </TableHead>
// <TableBody>
// {tasks.map((task, i) => (
// <TableRow key={i}>
// <TableCell>{task.title()}</TableCell>
// <TableCell>{task.description()}</TableCell>
// {!isMobile && <TableCell>${task.cost().toFixed(2)}</TableCell>}
// {!isMobile && <TableCell>${task.secondaryCost().toFixed(2)}</TableCell>}
// <TableCell>
// ${(Math.round((task.cost() + task.secondaryCost()) * 100) / 100).toFixed(2)}
// </TableCell>
// {!isMobile && (
// <TableCell>
// {task.start()} {task.end()}
// </TableCell>
// )}
// <TableCell>
// <Button
// color="primary"
// onClick={() => {
// setTaskToEdit(task);
// setNewTaskDialog(true);
// }}>
// Edit
// </Button>
// </TableCell>
// </TableRow>
// ))}
// </TableBody>
// </Table>
// <Box>
// <Typography>
// Total: $
// {(Math.round((costTotal + secondaryCostTotal) * field.acres() * 100) / 100).toFixed(2)}
// </Typography>
// </Box>
// </React.Fragment>
// );
// return frag;
// };
const savePlan = () => {
let newPlanSettings = pond.HarvestPlanSettings.create({
title: title,
field: field.key(),
createDate: moment.now(),
cropType: cropType,
grainType: variety,
harvestYear: harvestYear,
yieldTarget: targetYield,
bushelPrice: !isNaN(parseFloat(price)) ? Math.round(parseFloat(price) * 100) / 100 : 0
});
setNewPlan(HarvestPlan.create(pond.HarvestPlan.fromObject({ settings: newPlanSettings })));
harvestPlanAPI.addHarvestPlan(newPlanSettings).then(resp => {
setPlanKey(resp.data.harvestPlan);
openSnack("Plan Created");
});
};
const updatePlan = () => {
//material costs per acre
let preseedMatCost = 0;
let seedMatCost = 0;
let postseedMatCost = 0;
let harvestMatCost = 0;
let fallMatCost = 0;
//equipment costs per acre
let preseedEqCost = 0;
let seedEqCost = 0;
let postseedEqCost = 0;
let harvestEqCost = 0;
let fallEqCost = 0;
// planTasks.forEach(task => {
// switch (task.settings.type) {
// case "preseed":
// preseedMatCost += task.cost();
// preseedEqCost += task.secondaryCost();
// break;
// case "seed":
// seedMatCost += task.cost();
// seedEqCost += task.secondaryCost();
// break;
// case "postseed":
// postseedMatCost += task.cost();
// postseedEqCost += task.secondaryCost();
// break;
// case "harvest":
// harvestMatCost += task.cost();
// harvestEqCost += task.secondaryCost();
// break;
// default:
// fallMatCost += task.cost();
// fallEqCost += task.secondaryCost();
// break;
// }
// });
let tempPlan = plan ?? newPlan;
tempPlan.settings.cropType = cropType;
tempPlan.settings.field = field.key();
tempPlan.settings.title = title;
tempPlan.settings.grainType = variety;
tempPlan.settings.harvestYear = harvestYear;
tempPlan.settings.yieldTarget = targetYield;
tempPlan.settings.bushelPrice = !isNaN(parseFloat(price))
? Math.round(parseFloat(price) * 100) / 100
: 0;
tempPlan.settings.preSeedMaterials = preseedMatCost;
tempPlan.settings.preSeedEquipment = preseedEqCost;
tempPlan.settings.seedMaterials = seedMatCost;
tempPlan.settings.seedEquipment = seedEqCost;
tempPlan.settings.postSeedMaterials = postseedMatCost;
tempPlan.settings.postSeedEquipment = postseedEqCost;
tempPlan.settings.harvestMaterials = harvestMatCost;
tempPlan.settings.harvestEquipment = harvestEqCost;
tempPlan.settings.fallMaterials = fallMatCost;
tempPlan.settings.fallEquipment = fallEqCost;
if (planKey) {
harvestPlanAPI
.updateHarvestPlan(planKey, tempPlan.settings)
.then(resp => openSnack("Plan Updated"));
}
closeDialog(true, tempPlan);
};
const deletePlan = () => {
if (planKey) {
harvestPlanAPI.removeHarvestPlan(planKey).then(resp => {
openSnack("Plan Deleted");
});
}
setDeleteOpen(false);
closeDialog(true);
};
const handleNextStep = () => {
setActiveStep(prevStep => prevStep + 1);
};
const handleBack = () => {
setActiveStep(prevActiveStep => prevActiveStep - 1);
};
const stepper = () => {
return (
<Box display="flex" justifyContent="center" width="100%">
<Tabs
value={activeStep}
onChange={(_, value) => setActiveStep(value)}
indicatorColor="primary"
textColor="primary"
variant="fullWidth"
aria-label="bin tabs"
classes={{ root: classes.tabs }}>
<Tab
key={0}
classes={{ root: classes.tab }}
disabled={
!planKey || !(plan && plan.permissions.includes(pond.Permission.PERMISSION_WRITE))
}
label={"General"}
aria-label={"general"}
/>
{steps.map((label, i) => (
<Tab
key={i + 1}
classes={{ root: classes.tab }}
disabled={!planKey}
label={label}
aria-label={label}
/>
))}
</Tabs>
</Box>
);
};
const general = () => {
return (
<Box>
<TextField
fullWidth
margin="normal"
type="text"
label="Title"
value={title}
onChange={e => setTitle(e.target.value)}
/>
{useCustom ? (
<TextField
margin="normal"
fullWidth
label="Crop"
value={customCropType}
onChange={e => setCustomCropType(e.target.value)}
color="primary"
/>
) : (
<TextField
margin="normal"
fullWidth
select
label="Crop"
value={cropType}
onChange={e => setCropType(+e.target.value)}
color="primary">
{cropOptions.map(option => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</TextField>
)}
<TextField
fullWidth
margin="normal"
type="text"
label="Variety"
value={variety}
onChange={e => setVariety(e.target.value)}
/>
<TextField
fullWidth
margin="normal"
type="number"
label="Year"
value={harvestYear}
onChange={e => {
!isNaN(+e.target.value) && setHarvestYear(+e.target.value);
}}
/>
<TextField
fullWidth
margin="normal"
type="number"
label="Target Yield per acre"
value={targetYield}
onChange={e => {
!isNaN(+e.target.value) && setTargetYield(+e.target.value);
}}
/>
<TextField
margin="dense"
id="bushPrice"
label="Estimated sale price per bushel"
type="text"
fullWidth
helperText="Must be a valid number*"
value={price}
error={isNaN(+price) && price !== ""}
InputProps={{
startAdornment: <InputAdornment position="start">$</InputAdornment>
}}
onChange={e => {
setPrice(e.target.value);
}}
/>
</Box>
);
};
const preSeed = () => {
return (
<Box textAlign="center">
<Button
color="primary"
onClick={() => {
setType("preseed");
setNewTaskDialog(true);
}}>
Add Crop Activity
</Button>
<Typography>Planned Activities</Typography>
{/* {showTasks("preseed")} */}
</Box>
);
};
const seed = () => {
return (
<Box textAlign="center">
<Button
color="primary"
onClick={() => {
setType("seed");
setNewTaskDialog(true);
}}>
Add Crop Activity
</Button>
<Typography>Planned Activities</Typography>
{/* {showTasks("seed")} */}
</Box>
);
};
const postSeed = () => {
return (
<Box textAlign="center">
<Button
color="primary"
onClick={() => {
setType("postseed");
setNewTaskDialog(true);
}}>
Add Crop Activity
</Button>
<Typography>Planned Activities</Typography>
{/* {showTasks("postseed")} */}
</Box>
);
};
const harvest = () => {
return (
<Box textAlign="center">
<Button
color="primary"
onClick={() => {
setType("harvest");
setNewTaskDialog(true);
}}>
Add Crop Activity
</Button>
<Typography>Planned Activities</Typography>
{/* {showTasks("harvest")} */}
</Box>
);
};
const fall = () => {
return (
<Box textAlign="center">
<Button
color="primary"
onClick={() => {
setType("fall");
setNewTaskDialog(true);
}}>
Add Crop Activity
</Button>
<Typography>Planned Activities</Typography>
{/* {showTasks("fall")} */}
</Box>
);
};
const stepperContent = (step: number) => {
switch (step) {
case 1:
return preSeed();
case 2:
return seed();
case 3:
return postSeed();
case 4:
return harvest();
case 5:
return fall();
default:
return general();
}
};
const deleteDialog = () => {
return (
<ResponsiveDialog open={deleteOpen}>
<DialogContent>
Are you sure you wish to delete plan? This action is irreversible.
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteOpen(false)}>Cancel</Button>
<Button onClick={deletePlan}>
<Typography color="error">Delete</Typography>
</Button>
</DialogActions>
</ResponsiveDialog>
);
};
return (
<React.Fragment>
<ResponsiveDialog open={open} onClose={() => closeDialog()} maxWidth="lg">
<DialogTitle>Create New Crop Plan</DialogTitle>
<DialogContent>
{stepper()}
{stepperContent(activeStep)}
</DialogContent>
<DialogActions>
<Grid container direction="row">
<Grid>
{planKey && plan && field.permissions.includes(pond.Permission.PERMISSION_WRITE) && (
<Button
variant="contained"
style={{ color: "black", backgroundColor: "red" }}
onClick={() => setDeleteOpen(true)}>
Delete
</Button>
)}
</Grid>
<Grid>
<Button
className={classes.buttonSpacing}
variant="contained"
color="primary"
onClick={() => closeDialog()}>
Cancel
</Button>
{activeStep === 0 && !planKey && (
<React.Fragment>
<Button
className={classes.buttonSpacing}
variant="contained"
onClick={() => {
savePlan();
}}
color="primary">
Create Plan
</Button>
</React.Fragment>
)}
{activeStep > 0 && (
<Button
className={classes.buttonSpacing}
variant="contained"
onClick={handleBack}
color="primary">
Back
</Button>
)}
{activeStep < 5 && (activeStep > 0 || planKey) && (
<Button
className={classes.buttonSpacing}
variant="contained"
onClick={handleNextStep}
color="primary">
Next
</Button>
)}
{(planKey || activeStep > 0) && (
<Tooltip title="Finish Harvest Plan">
<Button
className={classes.buttonSpacing}
variant="contained"
color="primary"
onClick={updatePlan}>
Confirm
</Button>
</Tooltip>
)}
</Grid>
</Grid>
</DialogActions>
</ResponsiveDialog>
{deleteDialog()}
{/* <TaskSettings
task={taskToEdit}
hasCost
costTitle="Material Cost(acres)"
secondaryCostTitle="Equipment Cost(acre)"
objectKey={planKey}
type={type}
open={newTaskDialog}
onClose={r => {
setType("");
setTaskToEdit(undefined);
setNewTaskDialog(false);
if (r) {
loadTasks();
}
}}
/> */}
</React.Fragment>
);
}

View file

@ -6,7 +6,7 @@ import {
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
Grid, Grid2 as Grid,
Icon, Icon,
IconButton, IconButton,
LinearProgress, LinearProgress,
@ -355,7 +355,7 @@ export default function MapBase(props: Props) {
)} )}
</Box> </Box>
<Grid container direction={isMobile ? "column" : "row"}> <Grid container direction={isMobile ? "column" : "row"}>
<Grid item> <Grid>
<IconButton <IconButton
className={classes.iconButtons} className={classes.iconButtons}
style={ style={
@ -371,7 +371,7 @@ export default function MapBase(props: Props) {
<MarkerMove /> <MarkerMove />
</IconButton> </IconButton>
</Grid> </Grid>
<Grid item> <Grid>
<IconButton <IconButton
className={classes.iconButtons} className={classes.iconButtons}
style={msHovered ? { background: "grey" } : {}} style={msHovered ? { background: "grey" } : {}}
@ -382,7 +382,7 @@ export default function MapBase(props: Props) {
<Layers /> <Layers />
</IconButton> </IconButton>
</Grid> </Grid>
<Grid item> <Grid>
<IconButton <IconButton
className={classes.iconButtons} className={classes.iconButtons}
style={hcHovered ? { background: "grey" } : {}} style={hcHovered ? { background: "grey" } : {}}
@ -393,7 +393,7 @@ export default function MapBase(props: Props) {
<HomeIcon /> <HomeIcon />
</IconButton> </IconButton>
</Grid> </Grid>
<Grid item> <Grid>
<IconButton <IconButton
className={classes.iconButtons} className={classes.iconButtons}
style={ style={
@ -542,7 +542,7 @@ export default function MapBase(props: Props) {
const onCreate = (event: any, objectToCut?: string, measurement?: boolean) => { const onCreate = (event: any, objectToCut?: string, measurement?: boolean) => {
let feature = event.features[0]; let feature = event.features[0];
console.log(feature) //console.log(feature)
//if the string exist we are adding a hole to an existing object //if the string exist we are adding a hole to an existing object
if (objectToCut) { if (objectToCut) {
if (props.cutHoleInPolygon) { if (props.cutHoleInPolygon) {
@ -554,7 +554,7 @@ export default function MapBase(props: Props) {
} }
} else { } else {
if (props.addNewShape) { if (props.addNewShape) {
console.log("add new shape") //console.log("add new shape")
props.addNewShape(feature.geometry.coordinates, feature.geometry.type); props.addNewShape(feature.geometry.coordinates, feature.geometry.type);
} }
} }
@ -665,7 +665,7 @@ export default function MapBase(props: Props) {
latitude={homePin.latitude} latitude={homePin.latitude}
draggable draggable
onDragEnd={homeDragEnd} onDragEnd={homeDragEnd}
offset={[0, -25]}> offset={[0, -30]}>
<Box <Box
className={classes.pin} className={classes.pin}
style={{ pointerEvents: "none", width: 50, height: 50, background: "red" }}> style={{ pointerEvents: "none", width: 50, height: 50, background: "red" }}>
@ -674,11 +674,11 @@ export default function MapBase(props: Props) {
transform: "rotate(-45deg)", transform: "rotate(-45deg)",
width: 30, width: 30,
height: 30, height: 30,
marginTop: -10, marginTop: 10,
marginLeft: -10, marginLeft: 7,
pointerEvents: "none" pointerEvents: "none"
}}> }}>
<HomeIcon type="light" /> <HomeIcon type="light" height={30} width={30} />
</Box> </Box>
</Box> </Box>
</Marker> </Marker>

View file

@ -21,7 +21,7 @@ export default function DrawController(props: Props) {
const cutRef = useRef(); //the object id for what shape to cut a hole const cutRef = useRef(); //the object id for what shape to cut a hole
const mRef = useRef<boolean>(); const mRef = useRef<boolean>();
const lineLimitRef = useRef<number | undefined>(); const lineLimitRef = useRef<number | undefined>();
const pointsRef = useRef<number[][]>([]); //const pointsRef = useRef<number[][]>([]);
const { current: map } = useMap(); const { current: map } = useMap();
//use effect is just for adding and removing the controller from the map //use effect is just for adding and removing the controller from the map
@ -71,18 +71,18 @@ export default function DrawController(props: Props) {
} }
} }
}); });
map.on("click", e => { // map.on("click", e => {
if (map.hasControl(draw) && draw.getMode() === "draw_line_string") { // if (map.hasControl(draw) && draw.getMode() === "draw_line_string") {
if (lineLimitRef.current) { // if (lineLimitRef.current) {
pointsRef.current.push([e.lngLat.lng, e.lngLat.lat]); // pointsRef.current.push([e.lngLat.lng, e.lngLat.lat]);
if (pointsRef.current.length === lineLimitRef.current) { // if (pointsRef.current.length === lineLimitRef.current) {
draw.changeMode("simple_select"); //changing the mode causes the create event to fire // draw.changeMode("simple_select"); //changing the mode causes the create event to fire
pointsRef.current = []; // pointsRef.current = [];
draw.changeMode("draw_line_string"); // draw.changeMode("draw_line_string");
} // }
} // }
} // }
}); // });
} }
}, [map]); // eslint-disable-line react-hooks/exhaustive-deps }, [map]); // eslint-disable-line react-hooks/exhaustive-deps

View file

@ -11,8 +11,8 @@ import {
} from "@mui/material"; } from "@mui/material";
import { Notes } from "@mui/icons-material"; import { Notes } from "@mui/icons-material";
import DisplayDrawer from "common/DisplayDrawer"; import DisplayDrawer from "common/DisplayDrawer";
//import HarvestPlanDisplay from "harvestPlan/HarvestPlanDisplay"; import HarvestPlanDisplay from "harvestPlan/HarvestPlanDisplay";
import { Field, fieldScope, /*HarvestPlan,*/ teamScope } from "models"; import { Field, fieldScope, HarvestPlan, teamScope } from "models";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
//import TaskViewer from "tasks/TaskViewer"; //import TaskViewer from "tasks/TaskViewer";
import Weather from "weather/weather"; import Weather from "weather/weather";
@ -21,9 +21,9 @@ import GrainDescriber from "grain/GrainDescriber";
import Chat from "chat/Chat"; import Chat from "chat/Chat";
import { pond } from "protobuf-ts/pond"; import { pond } from "protobuf-ts/pond";
import { useMobile } from "hooks"; import { useMobile } from "hooks";
import { useGlobalState, /*useHarvestPlanAPI,*/ useUserAPI } from "providers"; import { useGlobalState, useHarvestPlanAPI, useUserAPI } from "providers";
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
//import FieldActions from "field/FieldActions"; import FieldActions from "field/FieldActions";
interface TabPanelProps { interface TabPanelProps {
children?: React.ReactNode; children?: React.ReactNode;
@ -77,13 +77,13 @@ const useStyles = makeStyles((theme: Theme) => ({
export default function FieldDrawer(props: Props) { export default function FieldDrawer(props: Props) {
const { open, closeDrawer, selectedFieldKey, fields, moveMap } = props; const { open, closeDrawer, selectedFieldKey, fields, moveMap } = props;
const [field, setField] = useState<Field>(Field.create()); const [field, setField] = useState<Field>(Field.create());
//const [hPlan, setHPlan] = useState<HarvestPlan>(HarvestPlan.create()); const [hPlan, setHPlan] = useState<HarvestPlan>(HarvestPlan.create());
const [value, setValue] = useState(0); const [value, setValue] = useState(0);
const classes = useStyles(); const classes = useStyles();
const [openNote, setOpenNote] = useState(false); const [openNote, setOpenNote] = useState(false);
const isMobile = useMobile(); const isMobile = useMobile();
const [{ as, user }] = useGlobalState(); const [{ as, user }] = useGlobalState();
//const hPlanAPI = useHarvestPlanAPI(); const hPlanAPI = useHarvestPlanAPI();
const [planLoading, setPlanLoading] = useState(false); const [planLoading, setPlanLoading] = useState(false);
const userAPI = useUserAPI(); const userAPI = useUserAPI();
const [permissions, setPermissions] = useState<pond.Permission[]>([]); const [permissions, setPermissions] = useState<pond.Permission[]>([]);
@ -129,24 +129,24 @@ export default function FieldDrawer(props: Props) {
} }
}, [as, user, userAPI, field]); //eslint-disable-line react-hooks/exhaustive-deps }, [as, user, userAPI, field]); //eslint-disable-line react-hooks/exhaustive-deps
// useEffect(() => { useEffect(() => {
// if (field.key() !== "") { if (field.key() !== "") {
// hPlanAPI hPlanAPI
// .listHarvestPlans(1, 0, "desc", "createDate", field.key(), as) .listHarvestPlans(1, 0, "desc", "createDate", field.key(), as)
// .then(resp => { .then(resp => {
// if (resp.data.harvestPlan.length > 0) { if (resp.data.harvestPlan.length > 0) {
// let plan = resp.data.harvestPlan[0]; let plan = resp.data.harvestPlan[0];
// setHPlan(HarvestPlan.any(plan)); setHPlan(HarvestPlan.any(plan));
// } else { } else {
// setHPlan(HarvestPlan.create()); setHPlan(HarvestPlan.create());
// } }
// setPlanLoading(false); setPlanLoading(false);
// }) })
// .catch(err => { .catch(err => {
// //openSnack("Failed to load plan"); //openSnack("Failed to load plan");
// }); });
// } }
// }, [field, as, hPlanAPI]); }, [field, as, hPlanAPI]);
const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => { const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
setValue(newValue); setValue(newValue);
@ -206,7 +206,7 @@ export default function FieldDrawer(props: Props) {
let taskLoadKeys: string[] = []; let taskLoadKeys: string[] = [];
if (!planLoading) { if (!planLoading) {
field.key() !== "" && taskLoadKeys.push(field.key()); field.key() !== "" && taskLoadKeys.push(field.key());
//hPlan.key() !== "" && taskLoadKeys.push(hPlan.key()); hPlan.key() !== "" && taskLoadKeys.push(hPlan.key());
} }
return ( return (
<React.Fragment> <React.Fragment>
@ -255,7 +255,7 @@ export default function FieldDrawer(props: Props) {
<Box padding={3}> <Box padding={3}>
<Divider style={{ padding: 2 }} /> <Divider style={{ padding: 2 }} />
</Box> </Box>
{/* <HarvestPlanDisplay <HarvestPlanDisplay
plan={hPlan} plan={hPlan}
permissions={permissions} permissions={permissions}
planField={field} planField={field}
@ -266,7 +266,7 @@ export default function FieldDrawer(props: Props) {
setHPlan(updatedPlan); setHPlan(updatedPlan);
} }
}} }}
/> */} />
</Box> </Box>
</TabPanelMine> </TabPanelMine>
<TabPanelMine value={value} index={1}> <TabPanelMine value={value} index={1}>
@ -311,14 +311,14 @@ export default function FieldDrawer(props: Props) {
} }
: undefined : undefined
} }
// objectActions={ objectActions={
// field.settings.fieldGeoData && field.settings.fieldGeoData &&
// field.settings.fieldGeoData.origin === pond.DataOrigin.DATA_ORIGIN_ADAPTIVE ? ( field.settings.fieldGeoData.origin === pond.DataOrigin.DATA_ORIGIN_ADAPTIVE ? (
// <FieldActions field={field} permissions={permissions} refreshCallback={() => {}} /> <FieldActions field={field} permissions={permissions} refreshCallback={() => {}} />
// ) : ( ) : (
// undefined undefined
// ) )
// } }
/> />
{noteDrawer()} {noteDrawer()}
</React.Fragment> </React.Fragment>

View file

@ -2,7 +2,7 @@ import { Box } from "@mui/material";
import DisplayDrawer from "common/DisplayDrawer"; import DisplayDrawer from "common/DisplayDrawer";
import MapMarkerSettings from "maps/MapMarkerSettings"; import MapMarkerSettings from "maps/MapMarkerSettings";
import { GrainBag as BagModel } from "models/GrainBag"; import { GrainBag as BagModel } from "models/GrainBag";
//import GrainBag from "pages/grainBag"; import GrainBag from "pages/grainBag";
import { pond } from "protobuf-ts/pond"; import { pond } from "protobuf-ts/pond";
import { useGrainBagAPI, useSnackbar } from "providers"; import { useGrainBagAPI, useSnackbar } from "providers";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
@ -86,8 +86,8 @@ export default function GrainBagDrawer(props: Props) {
}; };
const drawerBody = () => { const drawerBody = () => {
//return <Box>{bag.key() !== "" && <GrainBag bagKey={bag.key()} mobileView />}</Box>; return <Box>{bag.key() !== "" && <GrainBag bagKey={bag.key()} mobileView />}</Box>;
return <Box></Box> //return <Box></Box>
}; };
const update = () => { const update = () => {

View file

@ -50,7 +50,7 @@ import {
import AgMapTools from "../mapObjectTools/agMapTools"; import AgMapTools from "../mapObjectTools/agMapTools";
import FieldDrawer from "../mapDrawers/FieldDrawer"; import FieldDrawer from "../mapDrawers/FieldDrawer";
import { coordDistance, shapeFromCoords } from "models/GeometryMapping"; import { coordDistance, shapeFromCoords } from "models/GeometryMapping";
//import FieldSettings from "field/FieldSettings"; import FieldSettings from "field/FieldSettings";
import GrainBagSettings from "grainBag/grainBagSettings"; import GrainBagSettings from "grainBag/grainBagSettings";
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
import { Result } from "@mapbox/mapbox-gl-geocoder"; import { Result } from "@mapbox/mapbox-gl-geocoder";
@ -1327,7 +1327,7 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
getOptionLabel={(option: BagModel) => option.name()} getOptionLabel={(option: BagModel) => option.name()}
fullWidth fullWidth
onChange={(e, val) => { onChange={(e, val) => {
val && setSelectKey(val.key); val && setSelectKey(val.key());
}} }}
renderInput={params => ( renderInput={params => (
<TextField {...params} label="Bags (leave blank to create a new one)" /> <TextField {...params} label="Bags (leave blank to create a new one)" />
@ -1518,9 +1518,6 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
} }
}} }}
addNewShape={(coords, type) => { addNewShape={(coords, type) => {
console.log("controller add shape")
console.log(coords)
console.log(type)
if (type === "Polygon") { if (type === "Polygon") {
//if it is a polygon it is a field (for now until we add more objects that use polygons) //if it is a polygon it is a field (for now until we add more objects that use polygons)
let newShapes: pond.Shape[] = []; let newShapes: pond.Shape[] = [];
@ -1842,7 +1839,7 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
longitude={newMarkerLong} longitude={newMarkerLong}
latitude={newMarkerLat} latitude={newMarkerLat}
/> />
{/* <FieldSettings <FieldSettings
selectedField={fieldsRef.current.get(objectKey)} selectedField={fieldsRef.current.get(objectKey)}
updateFields={(key, settings) => { updateFields={(key, settings) => {
let data = clone(geoData); let data = clone(geoData);
@ -1880,7 +1877,7 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
setOpenFieldSettings(false); setOpenFieldSettings(false);
}} }}
borders={fieldBorders} borders={fieldBorders}
/> */} />
<GrainBagSettings <GrainBagSettings
open={openBagSettings} open={openBagSettings}
coordinates={bagCoordinates.current} coordinates={bagCoordinates.current}

160
src/models/HarvestPlan.ts Normal file
View file

@ -0,0 +1,160 @@
import { cloneDeep } from "lodash";
import { pond } from "protobuf-ts/pond";
import { or } from "utils/types";
export class HarvestPlan {
public settings: pond.HarvestPlanSettings = pond.HarvestPlanSettings.create();
public status: pond.HarvestPlanStatus = pond.HarvestPlanStatus.create();
public permissions: pond.Permission[] = [];
public static create(pb?: pond.HarvestPlan): HarvestPlan {
let my = new HarvestPlan();
if (pb) {
my.settings = pond.HarvestPlanSettings.fromObject(cloneDeep(or(pb.settings, {})));
my.status = pond.HarvestPlanStatus.fromObject(cloneDeep(or(pb.status, {})));
my.permissions = pb.planPermissions;
}
return my;
}
public static clone(other?: HarvestPlan): HarvestPlan {
if (other) {
return HarvestPlan.create(
pond.HarvestPlan.fromObject({
settings: cloneDeep(other.settings),
status: cloneDeep(other.status)
})
);
}
return HarvestPlan.create();
}
public static any(data: any): HarvestPlan {
return HarvestPlan.create(pond.HarvestPlan.fromObject(cloneDeep(data)));
}
public key(): string {
return this.settings.key;
}
public totalMaterialCost(): number {
return (
this.settings.seedMaterials +
this.settings.preSeedMaterials +
this.settings.postSeedMaterials +
this.settings.harvestMaterials +
this.settings.fallMaterials
);
}
public totalEquipmentCost(): number {
return (
this.settings.seedEquipment +
this.settings.preSeedEquipment +
this.settings.postSeedEquipment +
this.settings.harvestEquipment +
this.settings.fallEquipment
);
}
public field(): string {
return this.settings.field;
}
public createDate(): number {
return this.settings.createDate;
}
public harvestYear(): number {
return this.settings.harvestYear;
}
public cropType(): pond.Grain {
return this.settings.cropType;
}
public customCropType(): string {
return this.settings.customGrain;
}
public grainType(): string {
return this.settings.grainType;
}
public yieldTarget(): number {
return this.settings.yieldTarget;
}
public actualYield(): number {
return this.settings.actualYield;
}
//NOTE all cost are set as per acre
public seedMaterialsCost(): number {
return this.settings.seedMaterials;
}
public seedEquipmentCost(): number {
return this.settings.seedEquipment;
}
public seedPerAcre(): number {
return this.settings.seedMaterials + this.settings.seedEquipment;
}
public preSeedMaterialsCost(): number {
return this.settings.preSeedMaterials;
}
public preSeedEquipmentCost(): number {
return this.settings.preSeedEquipment;
}
public preSeedPerAcre(): number {
return this.settings.preSeedMaterials + this.settings.preSeedEquipment;
}
public postSeedMaterialCost(): number {
return this.settings.postSeedMaterials;
}
public postSeedEquipmentCost(): number {
return this.settings.postSeedEquipment;
}
public postSeedPerAcre(): number {
return this.settings.postSeedMaterials + this.settings.postSeedEquipment;
}
public harvestMaterialCost(): number {
return this.settings.harvestMaterials;
}
public harvestEquipmentCost(): number {
return this.settings.harvestEquipment;
}
public harvestPerAcre(): number {
return this.settings.harvestMaterials + this.settings.harvestEquipment;
}
public fallMaterialCost(): number {
return this.settings.fallMaterials;
}
public fallEquipmentCost(): number {
return this.settings.fallEquipment;
}
public fallPerAcre(): number {
return this.settings.fallMaterials + this.settings.fallEquipment;
}
public bushelPrice(): number {
return this.settings.bushelPrice;
}
public taskIds(): string[] {
return this.settings.taskIds;
}
}

View file

@ -17,7 +17,7 @@ export * from "./user";
export * from "./Field"; export * from "./Field";
// export * from "./Team"; // export * from "./Team";
export * from "./team"; export * from "./team";
// export * from "./HarvestPlan"; export * from "./HarvestPlan";
// export * from "./HarvestYear"; // export * from "./HarvestYear";
export * from "./Note"; export * from "./Note";
// export * from "./FieldMapping"; // export * from "./FieldMapping";

View file

@ -8,6 +8,7 @@ import { ErrorBoundary } from "react-error-boundary";
import { getWhitelabel } from "services/whiteLabel"; import { getWhitelabel } from "services/whiteLabel";
import Ventilation from "pages/VentEditor"; import Ventilation from "pages/VentEditor";
import FieldMap from "pages/FieldMap"; import FieldMap from "pages/FieldMap";
import GrainBag from "pages/grainBag";
const DeviceHistory = lazy(() => import("pages/DeviceHistory")); const DeviceHistory = lazy(() => import("pages/DeviceHistory"));
const DevicePage = lazy(() => import("pages/Device")); const DevicePage = lazy(() => import("pages/Device"));
@ -222,6 +223,7 @@ export default function Router(props: Props) {
<Route path="users" element={<Users/>} /> <Route path="users" element={<Users/>} />
<Route path="visualFarm" element={<FieldMap />} /> <Route path="visualFarm" element={<FieldMap />} />
<Route path="/logout" element={<Logout />} /> <Route path="/logout" element={<Logout />} />
<Route path="grainbags/:bagID" element={<GrainBag />} />
{/* {/*
<ErrorBoundary FallbackComponent={ErrorFallback}> <ErrorBoundary FallbackComponent={ErrorFallback}>
<Route path="*" element={<RelativeRoutes/>} /> <Route path="*" element={<RelativeRoutes/>} />

133
src/pages/grainBag.tsx Normal file
View file

@ -0,0 +1,133 @@
import { Box, Card, Grid2 as Grid, Typography } from "@mui/material";
import ObjectControls from "common/ObjectControls";
import { GetDefaultDateRange } from "common/time/DateRange";
import TimeBar from "common/time/TimeBar";
import GrainBagActions from "grainBag/grainBagActions";
import GrainBagInventoryGraph from "grainBag/grainBagInventoryGraph";
import GrainBagVisualizer from "grainBag/grainBagVisualizer";
import { Scope } from "models";
import { GrainBag as IGrainBag } from "models/GrainBag";
import { Moment } from "moment";
//import { MatchParams } from "navigation/Routes";
import { pond } from "protobuf-ts/pond";
import { useGlobalState, useGrainBagAPI, useSnackbar, useUserAPI } from "providers";
import { useEffect, useState } from "react";
//import { useRouteMatch } from "react-router";
import { useParams } from "react-router-dom";
import PageContainer from "./PageContainer";
interface Props {
bagKey?: string;
mobileView?: boolean;
}
export default function GrainBag(props: Props) {
const { bagKey, mobileView } = props;
//const match = useRouteMatch<MatchParams>();
//const bagID = bagKey ?? match.params.bagID;
const bagID = bagKey ?? useParams<{ bagID: string }>()?.bagID ?? "";
const grainBagAPI = useGrainBagAPI();
const [{ user, as }] = useGlobalState();
const userAPI = useUserAPI();
const [permissions, setPermissions] = useState<pond.Permission[]>([]);
const [grainBag, setGrainBag] = useState<IGrainBag>(IGrainBag.create());
const defaultDateRange = GetDefaultDateRange();
const [startDate, setStartDate] = useState<Moment>(defaultDateRange.start);
const [endDate, setEndDate] = useState<Moment>(defaultDateRange.end);
const { openSnack } = useSnackbar();
useEffect(() => {
grainBagAPI
.getGrainBag(bagID)
.then(resp => {
if (resp.data.grainBag) {
setGrainBag(IGrainBag.create(resp.data.grainBag));
}
})
.catch(err => {
openSnack("There was a problem loading the Grain Bag");
});
}, [bagID, grainBagAPI, openSnack]);
useEffect(() => {
let key = bagID;
let kind = "grainbag";
if (as) {
key = as;
kind = "team";
}
console.log
userAPI.getUser(user.id(), { key: key, kind: kind } as Scope).then(resp => {
console.log(resp)
setPermissions(resp.permissions);
});
}, [as, bagID, userAPI, user]);
const updateDateRange = (newStartDate: any, newEndDate: any) => {
let range = GetDefaultDateRange();
range.start = newStartDate;
range.end = newEndDate;
setStartDate(newStartDate);
setEndDate(newEndDate);
};
return (
<PageContainer>
<ObjectControls
actions={
<GrainBagActions
grainBag={grainBag}
refreshCallback={updatedBag => {
if (updatedBag) {
setGrainBag(updatedBag);
}
}}
permissions={permissions}
/>
}
/>
<Card style={{ padding: 15, margin: 15 }}>
<Grid container width={"100%"} direction="row" spacing={3} style={{ padding: 5 }}>
<Grid size={{
xs: 12,
sm: 12,
md: mobileView ? 12 : 6,
lg: mobileView ? 12 : 4
}}>
<Box
paddingLeft={1}
display="flex"
justifyContent="space-between"
alignItems="left"
marginBottom={2}>
<Typography style={{ fontWeight: 650, fontSize: 25 }}>{grainBag.name()}</Typography>
</Box>
<GrainBagVisualizer grainBag={grainBag} />
</Grid>
<Grid size={{
xs: 12,
sm: 12,
md: 12,
lg: mobileView ? 12 : 8
}}>
<Box
paddingLeft={1}
display="flex"
justifyContent="space-between"
alignItems="left"
marginBottom={2}>
<TimeBar startDate={startDate} endDate={endDate} updateDateRange={updateDateRange} />
</Box>
<GrainBagInventoryGraph
grainBag={grainBag}
startDate={startDate}
endDate={endDate}
customHeight={500}
/>
</Grid>
</Grid>
</Card>
</PageContainer>
);
}

View file

@ -6,11 +6,13 @@ import React from "react";
interface Props { interface Props {
type?: "light" | "dark"; type?: "light" | "dark";
height?: number | string
width?: number | string
} }
export default function HomeIcon(props: Props) { export default function HomeIcon(props: Props) {
const themeType = useThemeType(); const themeType = useThemeType();
const { type } = props; const { type, height, width } = props;
const src = () => { const src = () => {
if (type) { if (type) {
@ -20,5 +22,5 @@ export default function HomeIcon(props: Props) {
return themeType === "light" ? HomeIconDark : HomeIconLight; return themeType === "light" ? HomeIconDark : HomeIconLight;
}; };
return <ImgIcon alt="fields" src={src()} />; return <ImgIcon alt="fields" src={src()} iconHeight={height} iconWidth={width} />;
} }

View file

@ -17,7 +17,7 @@ export {
useFieldMarkerAPI, useFieldMarkerAPI,
useFirmwareAPI, useFirmwareAPI,
useGroupAPI, useGroupAPI,
// useHarvestPlanAPI, useHarvestPlanAPI,
// useHarvestYearAPI, // useHarvestYearAPI,
useHomeMarkerAPI, useHomeMarkerAPI,
useInteractionsAPI, useInteractionsAPI,

View file

@ -127,19 +127,20 @@ export default function GrainBagProvider(props: PropsWithChildren<Props>) {
}; };
const listHistory = (id: string, limit: number, offset: number, start?: string, end?: string) => { const listHistory = (id: string, limit: number, offset: number, start?: string, end?: string) => {
return get<pond.ListGrainBagHistoryResponse>( let url = pondURL(
pondURL( "/grainbags/" +
"/grainbags/" + id +
id + "/history?limit=" +
"/history?limit=" + limit +
limit + "&offset=" +
"&offset=" + offset +
offset + (start && "&start=" + start) +
(start && "&start=" + start) + (end && "&end=" + end)
(end && "&end=" + end) + )
(as && "&as=" + as) if (as) {
) url = url + "?as=" + as
); }
return get<pond.ListGrainBagHistoryResponse>(url);
}; };
return ( return (

View file

@ -0,0 +1,130 @@
import { AxiosResponse } from "axios";
import { useHTTP } from "hooks";
import { pond } from "protobuf-ts/pond";
import { useGlobalState } from "providers/StateContainer";
import { createContext, PropsWithChildren, useContext } from "react";
import { or } from "utils";
import { pondURL } from "./pond";
export interface IHarvestPlanAPIContext {
addHarvestPlan: (harvestPlan: pond.HarvestPlanSettings) => Promise<any>;
getHarvestPlan: (harvestPlanId: string) => Promise<any>;
listHarvestPlans: (
limit: number,
offset: number,
order?: "asc" | "desc",
orderBy?: string,
search?: string,
as?: string,
asRoot?: boolean
) => Promise<AxiosResponse<pond.ListHarvestPlansResponse>>;
listHistory: (
id: string,
limit: number,
offset: number
) => Promise<AxiosResponse<pond.ListHarvestPlanHistoryResponse>>;
removeHarvestPlan: (
harvestPlanId: string
) => Promise<AxiosResponse<pond.RemoveHarvestPlanResponse>>;
updateHarvestPlan: (
key: string,
harvestPlan: pond.HarvestPlanSettings,
asRoot?: true
) => Promise<AxiosResponse<pond.UpdateHarvestPlanResponse>>;
}
export const HarvestPlanAPIContext = createContext<IHarvestPlanAPIContext>(
{} as IHarvestPlanAPIContext
);
interface Props {}
export default function HarvestPlanProvider(props: PropsWithChildren<Props>) {
const { children } = props;
const { get, del, post, put } = useHTTP();
const [{ as }] = useGlobalState();
const addHarvestPlan = (harvestPlan: pond.HarvestPlanSettings) => {
if (as) return post(pondURL("/harvestPlans?as=" + as), harvestPlan);
return post(pondURL("/harvestPlans"), harvestPlan);
};
const getHarvestPlan = (harvestPlanId: string) => {
if (as) return get(pondURL("/harvestPlans/" + harvestPlanId + "?as=" + as));
return get(pondURL("/harvestPlans/" + harvestPlanId));
};
const removeHarvestPlan = (key: string) => {
if (as)
return del<pond.RemoveHarvestPlanResponse>(pondURL("/harvestPlans/" + key + "?as=" + as));
return del<pond.RemoveHarvestPlanResponse>(pondURL("/harvestPlans/" + key));
};
const listHarvestPlans = (
limit: number,
offset: number,
order?: "asc" | "desc",
orderBy?: string,
search?: string,
as?: string,
asRoot?: boolean
) => {
return get<pond.ListHarvestPlansResponse>(
pondURL(
"/harvestPlans" +
"?limit=" +
limit +
"&offset=" +
offset +
("&order=" + or(order, "asc")) +
("&by=" + or(orderBy, "key")) +
(asRoot ? "&asRoot=" + asRoot.toString() : "") +
(search ? "&search=" + search : "") +
(as ? "&as=" + as : "")
)
);
};
const listHistory = (id: string, limit: number, offset: number) => {
return get<pond.ListHarvestPlanHistoryResponse>(
pondURL("/harvestPlans/" + id + "/history?limit=" + limit + "&offset=" + offset)
);
};
const updateHarvestPlan = (
key: string,
harvestPlan: pond.HarvestPlanSettings,
asRoot?: boolean
) => {
if (as)
return put<pond.UpdateHarvestPlanResponse>(
pondURL(
"/harvestPlans/" +
key +
(as ? "?as=" + as : "") +
(asRoot ? "&asRoot=" + asRoot.toString() : "")
),
harvestPlan
);
return put<pond.UpdateHarvestPlanResponse>(
pondURL("/harvestPlans/" + key + (asRoot ? "?asRoot=" + asRoot.toString() : "")),
harvestPlan
);
};
return (
<HarvestPlanAPIContext.Provider
value={{
addHarvestPlan,
getHarvestPlan,
removeHarvestPlan,
listHarvestPlans,
listHistory,
updateHarvestPlan
}}>
{children}
</HarvestPlanAPIContext.Provider>
);
}
export const useHarvestPlanAPI = () => useContext(HarvestPlanAPIContext);

View file

@ -23,6 +23,7 @@ import FileControllerProvider, { useFileControllerAPI } from "./fileControllerAP
import MineProvider, { useMineAPI } from "./mineAPI"; import MineProvider, { useMineAPI } from "./mineAPI";
import FieldMarkerProvider, {useFieldMarkerAPI} from "./fieldMarkerAPI"; import FieldMarkerProvider, {useFieldMarkerAPI} from "./fieldMarkerAPI";
import HomeMarkerProvider, {useHomeMarkerAPI} from "./homeMarkerAPI"; import HomeMarkerProvider, {useHomeMarkerAPI} from "./homeMarkerAPI";
import HarvestPlanProvider, {useHarvestPlanAPI} from "./harvestPlanAPI";
// import NoteProvider from "providers/noteAPI"; // import NoteProvider from "providers/noteAPI";
export const pondURL = (partial: string, demo: boolean = false): string => { export const pondURL = (partial: string, demo: boolean = false): string => {
@ -62,7 +63,9 @@ export default function PondProvider(props: PropsWithChildren<any>) {
<MineProvider> <MineProvider>
<FieldMarkerProvider> <FieldMarkerProvider>
<HomeMarkerProvider> <HomeMarkerProvider>
{children} <HarvestPlanProvider>
{children}
</HarvestPlanProvider>
</HomeMarkerProvider> </HomeMarkerProvider>
</FieldMarkerProvider> </FieldMarkerProvider>
</MineProvider> </MineProvider>
@ -111,5 +114,6 @@ export {
useFileControllerAPI, useFileControllerAPI,
useMineAPI, useMineAPI,
useFieldMarkerAPI, useFieldMarkerAPI,
useHomeMarkerAPI useHomeMarkerAPI,
useHarvestPlanAPI
}; };

View file

@ -18,7 +18,7 @@ import { makeStyles } from "@mui/styles";
const openWeather = new OpenWeatherMap({ const openWeather = new OpenWeatherMap({
//apiKey: process.env.REACT_APP_OPEN_WEATHERMAP ? process.env.REACT_APP_OPEN_WEATHERMAP : "" //apiKey: process.env.REACT_APP_OPEN_WEATHERMAP ? process.env.REACT_APP_OPEN_WEATHERMAP : ""
apiKey: import.meta.env.REACT_APP_OPEN_WEATHERMAP ? import.meta.env.REACT_APP_OPEN_WEATHERMAP : "" apiKey: import.meta.env.VITE_OPEN_WEATHERMAP ? import.meta.env.VITE_OPEN_WEATHERMAP : ""
}); });
interface Props { interface Props {