imported in the field settings and actions stuff for drawing fields and the field drawer

This commit is contained in:
csawatzky 2025-03-27 15:48:00 -06:00
parent 6755764c75
commit a8e7ae1041
5 changed files with 487 additions and 13 deletions

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

@ -23,7 +23,7 @@ import { pond } from "protobuf-ts/pond";
import { useMobile } from "hooks";
import { useGlobalState, /*useHarvestPlanAPI,*/ useUserAPI } from "providers";
import { makeStyles } from "@mui/styles";
//import FieldActions from "field/FieldActions";
import FieldActions from "field/FieldActions";
interface TabPanelProps {
children?: React.ReactNode;
@ -311,14 +311,14 @@ export default function FieldDrawer(props: Props) {
}
: undefined
}
// objectActions={
// field.settings.fieldGeoData &&
// field.settings.fieldGeoData.origin === pond.DataOrigin.DATA_ORIGIN_ADAPTIVE ? (
// <FieldActions field={field} permissions={permissions} refreshCallback={() => {}} />
// ) : (
// undefined
// )
// }
objectActions={
field.settings.fieldGeoData &&
field.settings.fieldGeoData.origin === pond.DataOrigin.DATA_ORIGIN_ADAPTIVE ? (
<FieldActions field={field} permissions={permissions} refreshCallback={() => {}} />
) : (
undefined
)
}
/>
{noteDrawer()}
</React.Fragment>

View file

@ -50,7 +50,7 @@ import {
import AgMapTools from "../mapObjectTools/agMapTools";
import FieldDrawer from "../mapDrawers/FieldDrawer";
import { coordDistance, shapeFromCoords } from "models/GeometryMapping";
//import FieldSettings from "field/FieldSettings";
import FieldSettings from "field/FieldSettings";
import GrainBagSettings from "grainBag/grainBagSettings";
import { makeStyles } from "@mui/styles";
import { Result } from "@mapbox/mapbox-gl-geocoder";
@ -1839,7 +1839,7 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
longitude={newMarkerLong}
latitude={newMarkerLat}
/>
{/* <FieldSettings
<FieldSettings
selectedField={fieldsRef.current.get(objectKey)}
updateFields={(key, settings) => {
let data = clone(geoData);
@ -1877,7 +1877,7 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
setOpenFieldSettings(false);
}}
borders={fieldBorders}
/> */}
/>
<GrainBagSettings
open={openBagSettings}
coordinates={bagCoordinates.current}

View file

@ -18,7 +18,7 @@ import { makeStyles } from "@mui/styles";
const openWeather = new OpenWeatherMap({
//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 {