added grain bag stuff and file selector/uploader stuff

This commit is contained in:
Carter 2025-03-11 16:36:32 -06:00
parent ae30251035
commit 2d78947d2e
18 changed files with 1391 additions and 36 deletions

70
package-lock.json generated
View file

@ -18,10 +18,12 @@
"@mui/styles": "^6.1.6",
"@mui/x-date-pickers": "^7.26.0",
"@sentry/react": "^8.38.0",
"@turf/area": "^7.2.0",
"@types/classnames": "^2.3.0",
"axios": "^1.7.7",
"dayjs": "^1.11.13",
"deep-object-diff": "^1.1.9",
"geojson": "^0.5.0",
"json-2-csv": "^5.5.8",
"lodash": "^4.17.21",
"moment": "^2.30.1",
@ -3422,6 +3424,47 @@
"sourcemap-codec": "^1.4.8"
}
},
"node_modules/@turf/area": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@turf/area/-/area-7.2.0.tgz",
"integrity": "sha512-zuTTdQ4eoTI9nSSjerIy4QwgvxqwJVciQJ8tOPuMHbXJ9N/dNjI7bU8tasjhxas/Cx3NE9NxVHtNpYHL0FSzoA==",
"license": "MIT",
"dependencies": {
"@turf/helpers": "^7.2.0",
"@turf/meta": "^7.2.0",
"@types/geojson": "^7946.0.10",
"tslib": "^2.8.1"
},
"funding": {
"url": "https://opencollective.com/turf"
}
},
"node_modules/@turf/helpers": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-7.2.0.tgz",
"integrity": "sha512-cXo7bKNZoa7aC7ydLmUR02oB3IgDe7MxiPuRz3cCtYQHn+BJ6h1tihmamYDWWUlPHgSNF0i3ATc4WmDECZafKw==",
"license": "MIT",
"dependencies": {
"@types/geojson": "^7946.0.10",
"tslib": "^2.8.1"
},
"funding": {
"url": "https://opencollective.com/turf"
}
},
"node_modules/@turf/meta": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@turf/meta/-/meta-7.2.0.tgz",
"integrity": "sha512-igzTdHsQc8TV1RhPuOLVo74Px/hyPrVgVOTgjWQZzt3J9BVseCdpfY/0cJBdlSRI4S/yTmmHl7gAqjhpYH5Yaw==",
"license": "MIT",
"dependencies": {
"@turf/helpers": "^7.2.0",
"@types/geojson": "^7946.0.10"
},
"funding": {
"url": "https://opencollective.com/turf"
}
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@ -3539,6 +3582,12 @@
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true
},
"node_modules/@types/geojson": {
"version": "7946.0.16",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
"license": "MIT"
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@ -4174,8 +4223,10 @@
}
},
"node_modules/axios": {
"version": "1.7.7",
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz",
"integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
@ -5672,6 +5723,15 @@
"node": ">=6.9.0"
}
},
"node_modules/geojson": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/geojson/-/geojson-0.5.0.tgz",
"integrity": "sha512-/Bx5lEn+qRF4TfQ5aLu6NH+UKtvIv7Lhc487y/c8BdludrCTpiWf9wyI0RTyqg49MFefIAvFDuEi5Dfd/zgNxQ==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@ -8566,6 +8626,12 @@
}
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",

View file

@ -28,10 +28,12 @@
"@mui/styles": "^6.1.6",
"@mui/x-date-pickers": "^7.26.0",
"@sentry/react": "^8.38.0",
"@turf/area": "^7.2.0",
"@types/classnames": "^2.3.0",
"axios": "^1.7.7",
"dayjs": "^1.11.13",
"deep-object-diff": "^1.1.9",
"geojson": "^0.5.0",
"json-2-csv": "^5.5.8",
"lodash": "^4.17.21",
"moment": "^2.30.1",

View file

@ -0,0 +1,202 @@
import {
Box,
Button,
CircularProgress,
darken,
Grid2 as Grid,
IconButton,
Theme,
Typography
} from "@mui/material";
import { useFileControllerAPI, useGlobalState } from "providers";
import { useState } from "react";
import ErrorIcon from "@mui/icons-material/Error";
import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline";
import { getThemeType } from "theme";
import { toAttach } from "providers/pond/fileControllerAPI";
import DeleteIcon from "products/AgIcons/Delete";
import { pond } from "protobuf-ts/pond";
import { makeStyles } from "@mui/styles";
interface Props {
toAttach?: toAttach;
uploadStart: () => void;
uploadEnd: (fileID?: string, fileName?: string) => void;
uniqueID?: string;
keys?: string[];
types?: string[];
}
const useStyles = makeStyles((theme: Theme) => {
return ({
fileSelect: {
cursor: "pointer",
border: "1px solid white",
padding: 7,
borderRadius: 5,
backgroundColor: darken(
theme.palette.background.paper,
getThemeType() === "light" ? 0.05 : 0.25
),
"&:hover": {
backgroundColor: theme.palette.background.paper
}
},
fileDisabled: {
border: "1px solid gray",
color: "gray",
padding: 7,
borderRadius: 5,
backgroundColor: theme.palette.background.paper
}
});
});
export default function FileSelector(props: Props) {
const { uploadEnd, uploadStart, uniqueID, toAttach, keys, types } = props;
const [{ userTeamPermissions }] = useGlobalState();
const [fileList, setFileList] = useState<FileList | undefined>();
const classes = useStyles();
const fileAPI = useFileControllerAPI();
const [uploadFailed, setUploadFailed] = useState(false);
const [fileID, setFileID] = useState("");
const [uploading, setUploading] = useState(false);
const [fileName, setFileName] = useState("");
const [errorMsg, setErrorMsg] = useState("File failed to upload");
const [fileInvalid, setFileInvalid] = useState(false);
const maxFileSizeMB = 10;
const bytesConversionFactor = 1048576;
const uploadFile = () => {
if (fileList) {
let image = fileID;
uploadStart();
setUploading(true);
fileAPI
.uploadFiles(fileList, toAttach, keys, types)
.then(resp => {
image = resp.data.uuid;
setUploadFailed(false);
})
.catch(err => {
//file failed to upload
if (err.response) {
setErrorMsg(err.response.data.error);
}
setUploadFailed(true);
})
.finally(() => {
setFileID(image);
uploadEnd(image, fileName);
setUploading(false);
});
}
};
const deleteFile = () => {
fileAPI
.deleteFile(fileID)
.then(resp => {
console.log("no errors");
})
.catch(err => {
console.log("error found");
});
};
const checkFile = (file: File) => {
let invalid = false;
let largeFile = file.size / bytesConversionFactor > maxFileSizeMB;
if (largeFile || !file.type.includes("image/")) {
invalid = true;
let errMsg = "File type not allowed";
if (largeFile) errMsg = "File too large";
setErrorMsg(errMsg);
}
setFileInvalid(invalid);
};
const uploadStatus = () => {
if (uploading) {
return <CircularProgress />;
}
if (uploadFailed) {
return <ErrorIcon style={{ height: 40, width: 40, color: "red" }} />;
}
if (fileID !== "") {
return <CheckCircleOutlineIcon style={{ height: 40, width: 40, color: "green" }} />;
}
return;
};
return (
<Box width={"100%"}>
<Grid
container
direction="row"
alignContent="center"
alignItems="center"
justifyContent="space-between">
<Grid >
<input
disabled={
uploading ||
(!uploadFailed && fileID !== "") ||
!userTeamPermissions.includes(pond.Permission.PERMISSION_FILE_MANAGEMENT)
}
multiple={false}
name={uniqueID || "fileInput"}
id={uniqueID || "fileInput"}
hidden
type="file"
onChange={event => {
if (event.target.files) {
setFileList(event.target.files);
let file = event.target.files[0];
if (file) {
setFileName(file.name);
checkFile(file);
}
}
}}
/>
<label
htmlFor={uniqueID || "fileInput"}
className={
userTeamPermissions.includes(pond.Permission.PERMISSION_FILE_MANAGEMENT)
? classes.fileSelect
: classes.fileDisabled
}>
{fileName === "" ? "Choose a file..." : fileName}
</label>
</Grid>
<Grid >
<Grid container spacing={2} alignContent="center" alignItems="center">
<Grid >{uploadStatus()}</Grid>
{!uploadFailed && fileID !== "" && (
<Grid>
<IconButton
onClick={() => {
deleteFile();
}}>
<DeleteIcon />
</IconButton>
</Grid>
)}
<Grid >
{fileList && (
<Button
onClick={uploadFile}
variant="contained"
disabled={uploading || (!uploadFailed && fileID !== "") || fileInvalid}>
Upload
</Button>
)}
</Grid>
</Grid>
</Grid>
</Grid>
{(uploadFailed || fileInvalid) && <Typography color="error">{errorMsg}</Typography>}
</Box>
);
}

View file

@ -0,0 +1,43 @@
import { Box } from "@mui/material";
import { toAttach } from "providers/pond/fileControllerAPI";
import React, { useEffect, useState } from "react";
import FileSelector from "./FileSelector";
interface Props {
startingSelectorCount?: number;
toAttach?: toAttach;
uploadStart: () => void;
uploadEnd: (fileID?: string, fileName?: string) => void;
keys?: string[];
types?: string[];
}
export default function FileUploader(props: Props) {
const { uploadStart, uploadEnd, startingSelectorCount, toAttach, keys, types } = props;
const [numSelectors, setNumSelectors] = useState(startingSelectorCount || 1);
const [selectors, setSelectors] = useState<JSX.Element[]>([]);
useEffect(() => {
let currentSelectors: JSX.Element[] = [];
for (let i = 0; i < numSelectors; i++) {
currentSelectors.push(
<Box key={i} margin={2}>
<FileSelector
toAttach={toAttach}
keys={keys}
types={types}
uniqueID={"fileSelector" + i}
uploadStart={uploadStart}
uploadEnd={(fileID, fileName) => {
setNumSelectors(numSelectors + 1);
uploadEnd(fileID, fileName);
}}
/>
</Box>
);
}
setSelectors([...currentSelectors]);
}, [numSelectors, uploadEnd, uploadStart, toAttach, keys, types]);
return <React.Fragment>{selectors}</React.Fragment>;
}

View file

@ -15,9 +15,9 @@ import FileUploader from "common/FileUploads/FileUploader";
import ResponsiveDialog from "common/ResponsiveDialog";
import SearchSelect, { Option } from "common/SearchSelect";
import { useMobile } from "hooks";
import { Bin, Field } from "models";
import { Contract } from "models/Contract";
import { GrainBag } from "models/GrainBag";
import { Bin, Field, Contract, GrainBag } from "models";
// import { Contract } from "models/Contract";
// import { GrainBag } from "models/GrainBag";
import moment from "moment";
import { pond } from "protobuf-ts/pond";
import {
@ -28,7 +28,7 @@ import {
useSnackbar,
useTransactionAPI
} from "providers";
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import { getGrainUnit } from "utils";
interface Props {

View file

@ -1,17 +1,16 @@
import {
Button,
createStyles,
DialogActions,
DialogContent,
DialogTitle,
Grid,
Grid2 as Grid,
InputAdornment,
makeStyles,
Switch,
TextField,
Theme,
useTheme
} from "@material-ui/core";
} from "@mui/material";
import { makeStyles } from "@mui/styles";
import ResponsiveDialog from "common/ResponsiveDialog";
import SearchSelect, { Option } from "common/SearchSelect";
import GrainDescriber, { GrainOptions, ToGrainOption } from "grain/GrainDescriber";
@ -22,16 +21,16 @@ 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 { useNavigate } from "react-router-dom";
import { getGrainUnit } from "utils";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
const useStyles = makeStyles((theme: Theme) => {
return ({
bottomSpacing: {
marginBottom: theme.spacing(1)
}
})
);
});
interface Props {
open: boolean;
@ -61,7 +60,8 @@ export default function GrainBagSettings(props: Props) {
const [grainBushels, setGrainBushels] = useState(0);
const [fillDate, setFillDate] = useState(moment().format("YYYY-MM-DD"));
const [initialMoisture, setInitialMoisture] = useState(0);
const history = useHistory();
// const history = useHistory();
const navigate = useNavigate()
const { openSnack } = useSnackbar();
const theme = useTheme();
const [grainUpdate, setGrainUpdate] = useState(false);
@ -108,7 +108,7 @@ export default function GrainBagSettings(props: Props) {
.then(resp => {
openSnack("Removed Grain Bag");
close();
history.push("/bins");
navigate("/bins");
})
.catch(err => {
openSnack("Failed to remove Grain Bag");
@ -336,8 +336,8 @@ export default function GrainBagSettings(props: Props) {
onChange={e => setFillDate(e.target.value)}
/>
<Grid container alignItems="center">
<Grid item>Grain</Grid>
<Grid item>
<Grid>Grain</Grid>
<Grid>
<Switch
color="default"
value={isCustom}
@ -353,7 +353,7 @@ export default function GrainBagSettings(props: Props) {
name="storage"
/>
</Grid>
<Grid item>Custom</Grid>
<Grid>Custom</Grid>
</Grid>
{grainBox()}
<TextField
@ -424,8 +424,8 @@ export default function GrainBagSettings(props: Props) {
/>
</DialogContent>
<DialogActions>
<Grid container direction="row" justify="space-between">
<Grid item>
<Grid container direction="row" justifyContent="space-between">
<Grid >
{grainBag && (
<Button
onClick={() => {
@ -437,7 +437,7 @@ export default function GrainBagSettings(props: Props) {
</Button>
)}
</Grid>
<Grid item>
<Grid >
<Button
onClick={() => {
close();

237
src/models/Contract.ts Normal file
View file

@ -0,0 +1,237 @@
import { Option } from "common/SearchSelect";
import { GrainOptions, ToGrainOption } from "grain";
import GrainDescriber from "grain/GrainDescriber";
import { cloneDeep } from "lodash";
import { pond } from "protobuf-ts/pond";
import { getGrainUnit, stringToMaterialColour } from "utils";
import { or } from "utils/types";
export class Contract {
public settings: pond.ContractSettings = pond.ContractSettings.create();
public title: string = "Contract";
public colour: string = "";
public unit: string = "";
public label: string = "";
private objKey: string = "";
public static create(pb?: pond.Contract): Contract {
let my = new Contract();
if (pb) {
my.settings = pond.ContractSettings.fromObject(cloneDeep(or(pb.settings, {})));
my.title = pb.name;
my.objKey = pb.key;
my.unit = my.measurementUnit();
my.colour = my.commodityColour();
my.label = my.commodityLabel();
}
return my;
}
private measurementUnit(): string {
if (this.settings.type === pond.ContractType.CONTRACT_TYPE_GRAIN) {
return getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT ? "mT" : "bu";
}
return "";
}
private commodityColour(): string {
if (this.settings.type === pond.ContractType.CONTRACT_TYPE_GRAIN) {
if (this.isCustom()) {
return stringToMaterialColour(this.settings.customCommodity);
} else {
return GrainDescriber(this.settings.commodity).colour;
}
}
return "blue";
}
public commodityLabel(): string {
let label = "";
if (this.settings.type === pond.ContractType.CONTRACT_TYPE_GRAIN) {
if (this.isCustom()) {
label = this.settings.customCommodity;
} else {
label = GrainDescriber(this.settings.commodity).name;
}
}
return label;
}
public static clone(other?: Contract): Contract {
if (other) {
return Contract.create(
pond.Contract.fromObject({
title: other.title,
key: other.objKey,
settings: cloneDeep(other.settings)
})
);
}
return Contract.create();
}
public static any(data: any): Contract {
return Contract.create(pond.Contract.fromObject(cloneDeep(data)));
}
public key(): string {
if (this) {
return this.objKey;
} else {
return "";
}
}
public name(): string {
if (this) {
return this.title;
} else {
return "";
}
}
public objectType(): pond.ObjectType {
return pond.ObjectType.OBJECT_TYPE_CONTRACT;
}
public objectTypeString(): string {
return "Contract";
}
//possibly add another field to the contract for the subtype
public subtype(): string {
return this.settings.customCommodity;
}
//this is for grain transaction to compare if the source type matches
public storage(): pond.BinStorage {
let storage = pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN;
if (this.settings.commodity === pond.Grain.GRAIN_CUSTOM) {
storage = pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN;
}
return storage;
}
public grain(): pond.Grain {
return this.settings.commodity;
}
public customType(): string {
return this.settings.customCommodity;
}
public isCustom(): boolean {
let custom = false;
switch (this.settings.type) {
case pond.ContractType.CONTRACT_TYPE_GRAIN:
return this.settings.commodity === pond.Grain.GRAIN_CUSTOM;
}
return custom;
}
public static customValue(type: pond.ContractType): number {
let v = 0;
switch (type) {
case pond.ContractType.CONTRACT_TYPE_GRAIN:
return pond.Grain.GRAIN_CUSTOM;
}
return v;
}
public static commodityOptionsForType(type: pond.ContractType): Option[] {
let ops: Option[] = [];
if (type === pond.ContractType.CONTRACT_TYPE_GRAIN) {
ops = GrainOptions();
}
return ops;
}
public static typeOptions(): Option[] {
return [
{
label: "Grain",
value: pond.ContractType.CONTRACT_TYPE_GRAIN
}
];
}
public static toTypeOption(type: pond.ContractType): Option {
switch (type) {
case pond.ContractType.CONTRACT_TYPE_GRAIN:
return {
label: "Grain",
value: pond.ContractType.CONTRACT_TYPE_GRAIN
};
default:
return {
label: "Unknown",
value: pond.ContractType.CONTRACT_TYPE_UNKNOWN
};
}
}
public static toCommodityOption(type: pond.ContractType, commodity: number): Option {
switch (type) {
case pond.ContractType.CONTRACT_TYPE_GRAIN:
return ToGrainOption(commodity);
default:
return { label: "None", value: 0 };
}
}
/**
* returns the conversion value in the settings, if not set will return 1
* @returns 1 or bushels per tonne
*/
public conversionValue(): number {
//trying to avoid a divide by 0 error by only returning a value greater than 0
//since to get the secondary unit you divide the current unit by the conversion value
if (this.settings.conversionValue > 0) {
return this.settings.conversionValue;
} else {
return 1;
}
}
//used for grain transactions to compare the source and destinations since different bushels per tonne for custom grain should be treated like seperate grain types
public bushelsPerTonne(): number {
return this.conversionValue();
}
public sizeInPreferredUnit(): number {
let size = this.settings.size;
switch (this.settings.type) {
case pond.ContractType.CONTRACT_TYPE_GRAIN:
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && this.conversionValue() > 1) {
size = size / this.conversionValue();
}
}
return Math.round(size * 100) / 100;
}
public deliveredInPreferredUnit(): number {
let del = this.settings.delivered;
switch (this.settings.type) {
case pond.ContractType.CONTRACT_TYPE_GRAIN:
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && this.conversionValue() > 1) {
del = del / this.conversionValue();
}
}
return Math.round(del * 100) / 100;
}
public static toStoredUnit(
secondaryUnitVal: number,
contractType: pond.ContractType,
conversionValue: number
): number {
let storedunitVal = secondaryUnitVal;
switch (contractType) {
case pond.ContractType.CONTRACT_TYPE_GRAIN:
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && conversionValue > 1) {
storedunitVal = secondaryUnitVal * conversionValue;
}
}
return storedunitVal;
}
}

153
src/models/Field.ts Normal file
View file

@ -0,0 +1,153 @@
import { cloneDeep } from "lodash";
import { pond } from "protobuf-ts/pond";
import { or } from "utils/types";
import area from "@turf/area";
import { Feature } from "geojson";
import { GeometryMapping } from "./GeometryMapping";
export class Field {
public settings: pond.FieldSettings = pond.FieldSettings.create();
public status: pond.FieldStatus = pond.FieldStatus.create();
public permissions: pond.Permission[] = [];
public static create(pf?: pond.Field): Field {
let my = new Field();
if (pf) {
my.settings = pond.FieldSettings.fromObject(cloneDeep(or(pf.settings, {})));
my.status = pond.FieldStatus.fromObject(cloneDeep(or(pf.status, {})));
my.permissions = pf.fieldPermissions;
}
return my;
}
public static clone(other?: Field): Field {
if (other) {
return Field.create(
pond.Field.fromObject({
settings: cloneDeep(other.settings),
status: cloneDeep(other.status),
permissions: cloneDeep(other.permissions)
})
);
}
return Field.create();
}
public calculateAcres(): number {
let totalAcres = 0;
const metersToAcresConversionValue = 0.00024710538146717;
let geoData = this.settings.fieldGeoData;
if (geoData) {
//round to 2 decimal places
totalAcres =
Math.round(
area(
GeometryMapping.geoJSON(geoData.geoShape, geoData.shapes, geoData.holes) as Feature
) *
metersToAcresConversionValue *
100
) / 100;
}
return totalAcres;
}
public static any(data: any): Field {
return Field.create(pond.Field.fromObject(cloneDeep(data)));
}
public key(): string {
return this.settings.key;
}
public fieldName(): string {
return this.settings.fieldName;
}
public landLoc(): string {
return this.settings.landLocation;
}
public crop(): pond.Grain {
return this.settings.crop;
}
public name(): string {
return this.settings.fieldName;
}
public objectType(): pond.ObjectType {
return pond.ObjectType.OBJECT_TYPE_FIELD;
}
public objectTypeString(): string {
return "Field";
}
public grain(): pond.Grain {
return this.settings.crop;
}
public subtype(): string {
return this.settings.grainSubtype;
}
public customType(): string {
let c = "";
if (this.settings) {
c = this.settings.customGrain;
}
return c;
}
public acres(): number {
if (this.settings.acres > 0) {
return this.settings.acres;
} else {
return this.calculateAcres();
}
}
//these are for when using the field as a source for a grain transaction to compare if the destination matches
public storage(): pond.BinStorage {
let storage = pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN;
if (this.settings.crop === pond.Grain.GRAIN_CUSTOM) {
storage = pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN;
}
return storage;
}
/**
* 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.bushelsPerTonne > 0) {
bpt = this.settings.bushelsPerTonne;
}
return bpt;
}
public center(): pond.Coordinates {
let coords = pond.Coordinates.create();
if (this.settings.fieldGeoData) {
let minLong = 0;
let minLat = 0;
let maxLong = 0;
let maxLat = 0;
this.settings.fieldGeoData.shapes.forEach(shape => {
shape.points.forEach(pair => {
if (pair.longitude < minLong || minLong === 0) minLong = pair.longitude;
if (pair.longitude > maxLong || maxLong === 0) maxLong = pair.longitude;
if (pair.latitude < minLat || minLat === 0) minLat = pair.latitude;
if (pair.latitude > maxLat || maxLat === 0) maxLat = pair.latitude;
});
});
coords.longitude = (maxLong + minLong) / 2;
coords.latitude = (maxLat + minLat) / 2;
}
return coords;
}
}

View file

@ -0,0 +1,117 @@
import { pond } from "protobuf-ts/pond";
// interface properties {
// exclusion: boolean;
// field: string;
// main: boolean;
// color: string;
// }
interface geoJSONGeometry {
coordinates: any[];
type: string;
}
/**
* GeometryMapping is a geoJSON structure that React-map-gl's map layers needs in order to draw the shape on the map
*/
export class GeometryMapping {
public type: "Feature" = "Feature";
public geometry: geoJSONGeometry = {
coordinates: [],
type: ""
};
public static geoJSON(shape: string, shapes: pond.Shape[], holes?: pond.Shape[]) {
let gm = new GeometryMapping();
gm.geometry = getGeometry(shape, shapes, holes);
return gm;
}
}
//helper function to build the correct geometry for the desired shape
export function getGeometry(
shape: string,
shapes: pond.Shape[],
holes?: pond.Shape[]
): { coordinates: any[]; type: string } {
let geo: geoJSONGeometry = {
coordinates: [],
type: shape
};
switch (shape) {
case "LineString":
if (shapes[0]) {
shapes[0].points.forEach(point => {
geo.coordinates.push([point.longitude, point.latitude]);
});
}
break;
case "Polygon":
if (shapes[0]) {
let shapeArray: any[] = [];
shapes[0].points.forEach(point => {
shapeArray.push([point.longitude, point.latitude]);
});
geo.coordinates.push(shapeArray);
}
if (holes) {
holes.forEach(hole => {
let holeArray: any[] = [];
hole.points.forEach(point => {
holeArray.push([point.longitude, point.latitude]);
});
geo.coordinates.push(holeArray);
});
}
break;
case "MultiPolygon":
//this is an array of polygons (one polygon is an array of shapes, the first being the border, and all subsequent shapes being holes in that border)
shapes.forEach(shape => {
let polygon: any[] = [];
let shapeArray: any[] = [];
shape.points.forEach(point => {
shapeArray.push([point.longitude, point.latitude]);
});
polygon.push(shapeArray);
//just note JD and CNH dont do holes in their fields so this part will be UNTESTED until handling drawing multi polygons for our fields
if (holes) {
let holeArray: any[] = [];
holes.forEach(hole => {
if (hole.key === shape.key) {
hole.points.forEach(point => {
holeArray.push([point.longitude, point.latitude]);
});
}
polygon.push(holeArray);
});
}
geo.coordinates.push(polygon);
});
break;
default:
break;
}
return geo;
}
/**
* takes in a 2D array of numbers an builds then into a shape object that can be added to either the holes or shapes of geoData
* @param coordinates an array of number pairs
* @returns pond.shape object using that array
*/
export function shapeFromCoords(coordinates: number[][]): pond.Shape {
let shape = pond.Shape.create();
coordinates.forEach(pair => {
shape.points.push(
pond.Coordinates.create({
longitude: pair[0],
latitude: pair[1]
})
);
});
return shape;
}

View file

@ -14,7 +14,7 @@ export * from "./Tag";
// export * from "./Upgrade";
// export * from "./User";
export * from "./user";
// export * from "./Field";
export * from "./Field";
// export * from "./Team";
export * from "./team";
// export * from "./HarvestPlan";
@ -22,4 +22,5 @@ export * from "./team";
export * from "./Note";
// export * from "./FieldMapping";
// export * from "./FieldMarker";
// export * from "./GrainBag";
export * from "./GrainBag";
export * from "./Contract";

View file

@ -84,6 +84,7 @@ import BinSettings from "bin/BinSettings";
import AddBinFab from "bin/AddBinFab";
import { GrainBag } from "models/GrainBag";
import { getThemeType } from "theme/themeType";
import GrainBagSettings from "grainBag/grainBagSettings";
// import { useHistory } from "react-router";
const useStyles = makeStyles((theme: Theme) => {
@ -1311,7 +1312,7 @@ export default function Bins(props: Props) {
</Grid>
<Grid item>
<Box display="flex" paddingY={1} alignContent="center" alignItems="center">
{/* <StyledToggleButtonGroup
<StyledToggleButtonGroup
id="tour-graph-tabs"
value={bagView}
exclusive
@ -1325,7 +1326,7 @@ export default function Bins(props: Props) {
sessionStorage.setItem("bagsView", "grid");
}}>
<ViewComfy />
</StyledToggle> */}
</StyledToggle>
{/* hidden at dustins request so that grid view and list are the only two */}
{/* <StyledToggle
value={"scroll"}
@ -1336,7 +1337,7 @@ export default function Bins(props: Props) {
}}>
<ViewColumn />
</StyledToggle> */}
{/* <StyledToggle
<StyledToggle
value={"list"}
aria-label="list view"
onClick={() => {
@ -1345,7 +1346,7 @@ export default function Bins(props: Props) {
}}>
<ViewList />
</StyledToggle>
</StyledToggleButtonGroup> */}
</StyledToggleButtonGroup>
<IconButton
className={classes.icon}
onClick={() => {
@ -1434,13 +1435,13 @@ export default function Bins(props: Props) {
}
}}
/>
{/* <GrainBagSettings
<GrainBagSettings
open={addBagOpen}
close={() => {
setAddBagOpen(false);
}}
/>
<QrCodeGenerator
{/* <QrCodeGenerator
open={openQrGenerator}
close={() => {
setOpenQrGenerator(false);

View file

@ -13,7 +13,7 @@ export {
// useComponentWebsocket,
useDeviceAPI,
// useDeviceWebsocket,
// useFieldAPI,
useFieldAPI,
// useFieldMarkerAPI,
useFirmwareAPI,
useGroupAPI,
@ -40,9 +40,9 @@ export {
useGateAPI,
// useObjectHeaterAPI,
// useMutationAPI,
// useGrainBagAPI,
// useTransactionAPI,
// useFileControllerAPI
useGrainBagAPI,
useTransactionAPI,
useFileControllerAPI
} from "./pond/pond";
// export { SecurityContext, useSecurity } from "./security";
export { SnackbarContext, useSnackbar } from "./Snackbar";

View file

@ -0,0 +1,104 @@
import { AxiosResponse } from "axios";
import { useHTTP } from "hooks";
import { pond } from "protobuf-ts/pond";
import { useGlobalState } from "providers";
import React, { createContext, PropsWithChildren, useContext } from "react";
import { or } from "utils";
import { pondURL } from "./pond";
export interface IFieldAPIContext {
addField: (field: pond.FieldSettings) => Promise<any>;
getField: (fieldId: string) => Promise<any>;
listFields: (
limit: number,
offset: number,
order?: "asc" | "desc",
orderBy?: string,
search?: string,
as?: string,
asRoot?: boolean
) => Promise<AxiosResponse<pond.ListFieldsResponse>>;
removeField: (key: string) => Promise<AxiosResponse<pond.RemoveFieldResponse>>;
updateField: (
key: string,
field: pond.FieldSettings,
asRoot?: true
) => Promise<AxiosResponse<pond.UpdateSiteResponse>>;
}
export const FieldAPIContext = createContext<IFieldAPIContext>({} as IFieldAPIContext);
interface Props {}
export default function FieldProvider(props: PropsWithChildren<Props>) {
const { children } = props;
const { get, del, post, put } = useHTTP();
const [{ as }] = useGlobalState();
const addField = (field: pond.FieldSettings) => {
if (as) return post<pond.AddFieldResponse>(pondURL("/fields?as=" + as), field);
return post(pondURL("/fields"), field);
};
const getField = (fieldId: string) => {
if (as) return get(pondURL("/field/" + fieldId + "?as=" + as));
return get(pondURL("/field/" + fieldId));
};
const removeField = (key: string) => {
if (as) return del<pond.RemoveFieldResponse>(pondURL("/fields/" + key + "?as=" + as));
return del<pond.RemoveFieldResponse>(pondURL("/fields/" + key));
};
const listFields = (
limit: number,
offset: number,
order?: "asc" | "desc",
orderBy?: string,
search?: string,
as?: string,
asRoot?: boolean
) => {
return get<pond.ListFieldsResponse>(
pondURL(
"/fields" +
"?limit=" +
limit +
"&offset=" +
offset +
("&order=" + or(order, "asc")) +
("&by=" + or(orderBy, "key")) +
(as ? "&as=" + as : "") +
(asRoot ? "&asRoot=" + asRoot.toString() : "") +
(search ? "&search=" + search : "")
)
);
};
const updateField = (key: string, field: pond.FieldSettings, asRoot?: boolean) => {
if (as)
return put<pond.UpdateFieldResponse>(
pondURL("/fields/" + key + "?as=" + as + (asRoot ? "&asRoot=" + asRoot.toString() : "")),
field
);
return put<pond.UpdateFieldResponse>(
pondURL("/fields/" + key + (asRoot ? "?asRoot=" + asRoot.toString() : "")),
field
);
};
return (
<FieldAPIContext.Provider
value={{
addField,
getField,
listFields,
removeField,
updateField
}}>
{children}
</FieldAPIContext.Provider>
);
}
export const useFieldAPI = () => useContext(FieldAPIContext);

View file

@ -0,0 +1,122 @@
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 { pondURL } from "./pond";
export interface toAttach {
key: string;
type: pond.ObjectType;
}
export interface IFileControllerAPI {
uploadFiles: (
files: FileList,
toAttach?: toAttach,
keys?: string[],
types?: string[]
) => Promise<AxiosResponse<pond.AddFileResponse>>;
listFileRefs: (
objectKey: string,
keys: string[],
types: string[]
) => Promise<AxiosResponse<pond.ListFileRefsResponse>>;
retrieveFile: (path: string) => Promise<any>;
deleteFile: (fileID: string) => Promise<any>;
}
export const FileControllerAPIContext = createContext<IFileControllerAPI>({} as IFileControllerAPI);
export default function FileControllerProvider(props: PropsWithChildren<any>) {
const { children } = props;
const [{ as }] = useGlobalState();
const { get, post, del } = useHTTP();
const uploadFiles = (files: FileList, toAttach?: toAttach, keys?: string[], types?: string[]) => {
let formData = new FormData();
Array.from(files).forEach((f, i) => {
formData.append("file" + i, f);
});
let url = pondURL(
"/fileControl/upload" +
(toAttach ? "?toAttach=" + toAttach.key + "&objectType=" + toAttach.type : "") +
(keys ? "&keys=" + keys.join(",") : "") +
(types ? "&types=" + types.join(",") : "")
);
if (as) {
url = pondURL(
"/fileControl/upload?as=" +
as +
(toAttach ? "&toAttach=" + toAttach.key + "&objectType=" + toAttach.type : "") +
(keys ? "&keys=" + keys.join(",") : "") +
(types ? "&types=" + types.join(",") : "")
);
}
return post<pond.AddFileResponse>(url, formData);
};
/**
* Function to get the file references from the backend.
* Note: the object key is what the files are attached to and not used for the permissions,
* For example on a contract the files are linked to the transaction
* however you would check the users permission to the contract
* @param objectKey what the files are attached to
* @param keys the key of the object you want to check permissions for
* @param types the type of the object you want to check permissions for
* @returns promise that contains a list of file references in the data
*/
const listFileRefs = (objectKey: string, keys: string[], types: string[]) => {
let url = pondURL(
"/fileControl/list?objectKey=" +
objectKey +
"&keys=" +
keys.join(",") +
"&types=" +
types.join(",")
);
if (as) {
url = pondURL(
"/fileControl/list?as=" +
as +
"&objectKey=" +
objectKey +
"&keys=" +
keys.join(",") +
"&types=" +
types.join(",")
);
}
return get<pond.ListFileRefsResponse>(url);
};
const retrieveFile = (path: string) => {
let url = pondURL("/fileControl/retrieve?path=" + path);
if (as) {
url = pondURL("/fileControl/retrieve?as=" + as + "&path=" + path);
}
return get(url, { responseType: "blob" });
};
const deleteFile = (fileID: string) => {
let url = pondURL("/fileControl/" + fileID + "/delete");
if (as) {
url = pondURL("/fileControl/" + fileID + "/delete?as=" + as);
}
return del(url, { responseType: "blob" });
};
return (
<FileControllerAPIContext.Provider
value={{
uploadFiles,
listFileRefs,
retrieveFile,
deleteFile
}}>
{children}
</FileControllerAPIContext.Provider>
);
}
export const useFileControllerAPI = () => useContext(FileControllerAPIContext);

View file

@ -0,0 +1,161 @@
import { useHTTP } from "hooks";
import React, { createContext, PropsWithChildren, useContext } from "react";
import { pond } from "protobuf-ts/pond";
import { pondURL } from "./pond";
import { AxiosResponse } from "axios";
import { useGlobalState } from "providers";
import { GrainBag } from "models/GrainBag";
export interface IGrainBagInterface {
addGrainBag: (
name: string,
settings: pond.GrainBagSettings
) => Promise<AxiosResponse<pond.AddGrainBagResponse>>;
listGrainBags: (
limit: number,
offset: number,
order?: "asc" | "desc",
orderBy?: string,
search?: string,
keys?: string[],
types?: string[],
numerical?: boolean,
specificUser?: string
) => Promise<AxiosResponse<pond.ListGrainBagsResponse>>;
updateGrainBag: (
key: string,
name: string,
settings: pond.GrainBagSettings
) => Promise<AxiosResponse<pond.UpdateGrainBagResponse>>;
bulkUpdateGrainBags: (
bags: pond.GrainBag[]
) => Promise<AxiosResponse<pond.BulkGrainBagUpdateResponse>>;
getGrainBag: (key: string) => Promise<AxiosResponse<pond.GetGrainBagResponse>>;
removeGrainBag: (key: string) => Promise<AxiosResponse<pond.RemoveGrainBagResponse>>;
listHistory: (
id: string,
limit: number,
offset: number,
start?: string,
end?: string
) => Promise<AxiosResponse<pond.ListGrainBagHistoryResponse>>;
}
export const GrainBagAPIcontext = createContext<IGrainBagInterface>({} as IGrainBagInterface);
interface Props {}
export default function GrainBagProvider(props: PropsWithChildren<Props>) {
const { children } = props;
const { get, del, post, put } = useHTTP();
const [{ as }] = useGlobalState();
const addGrainBag = (name: string, settings: pond.GrainBagSettings) => {
if (as) {
return post<pond.AddGrainBagResponse>(
pondURL("/grainbags?name=" + name + "&as=" + as),
settings
);
}
return post<pond.AddGrainBagResponse>(pondURL("/grainbags?name=" + name), settings);
};
const listGrainBags = (
limit: number,
offset: number,
order?: "asc" | "desc",
orderBy?: string,
search?: string,
keys?: string[],
types?: string[],
numerical?: boolean,
specificUser?: string
) => {
let asText = "";
if (as) asText = "&as=" + as;
if (specificUser) asText = "&as=" + specificUser;
return get<pond.ListGrainBagsResponse>(
pondURL(
"/grainbags?limit=" +
limit +
"&offset=" +
offset +
("&order=" + (order ? order : "asc")) +
("&by=" + (orderBy ? orderBy : "key")) +
(numerical ? "&numerical=true" : "") +
(search ? "&search=" + search : "") +
(keys ? "&keys=" + keys.toString() : "") +
(types ? "&types=" + types.toString() : "") +
asText
)
);
};
const updateGrainBag = (key: string, name: string, settings: pond.GrainBagSettings) => {
if (as) {
return put<pond.UpdateGrainBagResponse>(
pondURL("/grainbags/" + key + "?as=" + as + "&name=" + name),
settings
);
}
return put<pond.UpdateGrainBagResponse>(
pondURL("/grainbags/" + key + "?name=" + name),
settings
);
};
const bulkUpdateGrainBags = (bags: pond.GrainBag[]) => {
if (as)
return put<pond.BulkGrainBagUpdateResponse>(pondURL("/bulkGrainBags/update?as=" + as), {
bags: bags
});
return put<pond.BulkGrainBagUpdateResponse>(pondURL("/bulkGrainBags/update"), { bags: bags });
};
const removeGrainBag = (key: string) => {
if (as) {
return del<pond.RemoveGrainBagResponse>(pondURL("/grainbags/" + key + "?as=" + as));
}
return del<pond.RemoveGrainBagResponse>(pondURL("/grainbags/" + key + "?as=" + as));
};
const getGrainBag = (key: string) => {
if (as) {
return get<pond.GetGrainBagResponse>(pondURL("/grainbags/" + key + "?as=" + as));
}
return get<pond.GetGrainBagResponse>(pondURL("/grainbags/" + key));
};
const listHistory = (id: string, limit: number, offset: number, start?: string, end?: string) => {
return get<pond.ListGrainBagHistoryResponse>(
pondURL(
"/grainbags/" +
id +
"/history?limit=" +
limit +
"&offset=" +
offset +
(start && "&start=" + start) +
(end && "&end=" + end) +
(as && "&as=" + as)
)
);
};
return (
<GrainBagAPIcontext.Provider
value={{
addGrainBag,
listGrainBags,
updateGrainBag,
bulkUpdateGrainBags,
removeGrainBag,
getGrainBag,
listHistory
}}>
{children}
</GrainBagAPIcontext.Provider>
);
}
export const useGrainBagAPI = () => useContext(GrainBagAPIcontext);

View file

@ -16,6 +16,10 @@ import FirmwareProvider, { useFirmwareAPI } from "./firmwareAPI";
import ComponentProvider, { useComponentAPI } from "./componentAPI";
import InteractionProvider, { useInteractionsAPI } from "./interactionsAPI";
import BinYardProvider, { useBinYardAPI } from "./binYardAPI";
import GrainBagProvider, { useGrainBagAPI } from "./grainBagAPI";
import FieldProvider, { useFieldAPI } from "./fieldAPI";
import TransactionProvider, { useTransactionAPI } from "./transactionAPI";
import FileControllerProvider, { useFileControllerAPI } from "./fileControllerAPI";
// import NoteProvider from "providers/noteAPI";
export const pondURL = (partial: string, demo: boolean = false): string => {
@ -48,7 +52,15 @@ export default function PondProvider(props: PropsWithChildren<any>) {
<ComponentProvider>
<InteractionProvider>
<BinYardProvider>
{children}
<GrainBagProvider>
<FieldProvider>
<TransactionProvider>
<FileControllerProvider>
{children}
</FileControllerProvider>
</TransactionProvider>
</FieldProvider>
</GrainBagProvider>
</BinYardProvider>
</InteractionProvider>
</ComponentProvider>
@ -82,6 +94,10 @@ export {
useBinYardAPI,
useGateAPI,
useGroupAPI,
useGrainBagAPI,
useNoteAPI,
useNotificationAPI
useNotificationAPI,
useFieldAPI,
useTransactionAPI,
useFileControllerAPI,
};

View file

@ -0,0 +1,127 @@
import { useHTTP } from "hooks";
import React, { createContext, PropsWithChildren, useContext } from "react";
import { pond } from "protobuf-ts/pond";
import { pondURL } from "./pond";
import { AxiosResponse } from "axios";
import { useGlobalState } from "providers";
export interface ITransactionAPIContext {
addTransaction: (
transaction: pond.Transaction,
fileIDs?: string[]
) => Promise<AxiosResponse<pond.AddTransactionResponse>>;
transactionPageData: (
start: string,
end: string
) => Promise<AxiosResponse<pond.TransactionPageDataResponse>>;
listTransactions: (
start: string,
end: string,
fromObject?: pond.ObjectType,
toObject?: pond.ObjectType,
fromKey?: string,
toKey?: string
) => Promise<AxiosResponse<pond.ListTransactionsResponse>>;
revokeTransaction: (key: string) => Promise<AxiosResponse<pond.RevokeTransactionResponse>>;
updateTransaction: (
key: string,
data: pond.TransactionData
) => Promise<AxiosResponse<pond.UpdateTransactionResponse>>;
}
export const TransactionAPIContext = createContext<ITransactionAPIContext>(
{} as ITransactionAPIContext
);
interface Props {}
export default function TransactionProvider(props: PropsWithChildren<Props>) {
const { children } = props;
const { get, post, put } = useHTTP();
const [{ as }] = useGlobalState();
const addTransaction = (transaction: pond.Transaction, fileIDs?: string[]) => {
if (as)
return post<pond.AddTransactionResponse>(
pondURL(
"/transactions?as=" + as + (fileIDs ? "&fileAttachments=" + fileIDs.toString() : "")
),
transaction
);
return post<pond.AddTransactionResponse>(
pondURL("/transactions" + (fileIDs ? "?fileAttachments=" + fileIDs.toString() : "")),
transaction
);
};
const listTransactions = (
start: string,
end: string,
fromObject?: pond.ObjectType,
toObject?: pond.ObjectType,
fromKey?: string,
toKey?: string
) => {
if (as) {
return get<pond.ListTransactionsResponse>(
pondURL("/transactions?as=" + as + "&start=" + start + "&end=" + end)
);
}
return get<pond.ListTransactionsResponse>(
pondURL(
"/transactions?start=" +
start +
"&end=" +
end +
(fromObject ? "&fromObject=" + fromObject : "") +
(toObject ? "&toObject=" + toObject : "") +
(fromKey ? "&fromKey=" + fromKey : "") +
(toKey ? "&toKey=" + toKey : "")
)
);
};
const updateTransaction = (key: string, data: pond.TransactionData) => {
if (as)
return post<pond.UpdateTransactionResponse>(
pondURL("/transactions/" + key + "/update?as=" + as),
data
);
return post<pond.UpdateTransactionResponse>(pondURL("/transactions/" + key + "/update"), data);
};
const transactionPageData = (start: string, end: string) => {
if (as) {
return get<pond.TransactionPageDataResponse>(
pondURL("/transactionsPage?as=" + as + "&start=" + start + "&end=" + end)
);
}
return get<pond.TransactionPageDataResponse>(
pondURL("/transactionsPage?start=" + start + "&end=" + end)
);
};
const revokeTransaction = (key: string) => {
if (as) {
return put<pond.RevokeTransactionResponse>(
pondURL("/transactions/" + key + "/revoke?as=" + as)
);
}
return put<pond.TransactionPageDataResponse>(pondURL("/transactions/" + key + "/revoke"));
};
return (
<TransactionAPIContext.Provider
value={{
addTransaction,
listTransactions,
updateTransaction,
transactionPageData,
revokeTransaction
}}>
{children}
</TransactionAPIContext.Provider>
);
}
export const useTransactionAPI = () => useContext(TransactionAPIContext);

3
src/theme/index.ts Normal file
View file

@ -0,0 +1,3 @@
export * from "./text";
export { CreateTheme } from "./theme";
export * from "./themeType";