From 781b52afd8b834c7c4dce6c5e9838cb37f5be0d4 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Tue, 5 Aug 2025 16:50:58 -0600 Subject: [PATCH 01/36] new table for the fields dashboard, map rendering in row --- src/field/FieldList.tsx | 175 ++++++++++++++++++++++++++----------- src/field/Fieldminimap.tsx | 24 +++++ 2 files changed, 148 insertions(+), 51 deletions(-) create mode 100644 src/field/Fieldminimap.tsx diff --git a/src/field/FieldList.tsx b/src/field/FieldList.tsx index 6243ce3..6f1a7e7 100644 --- a/src/field/FieldList.tsx +++ b/src/field/FieldList.tsx @@ -9,7 +9,8 @@ import { TableContainer, TableHead, TableRow, - Tabs + Tabs, + Typography } from "@mui/material"; import React, { useCallback, useEffect, useState } from "react"; import { useMobile, useSnackbar } from "hooks"; @@ -19,6 +20,8 @@ import { Field } from "models"; import GrainDescriber from "grain/GrainDescriber"; import { pond } from "protobuf-ts/pond"; import HarvestSettings from "harvestPlan/HarvestSettings"; +import ResponsiveTable, { Column } from "common/ResponsiveTable"; +import FieldMinimap from "./Fieldminimap"; interface TabPanelProps { children?: React.ReactNode; @@ -46,10 +49,13 @@ export default function FieldList() { const isMobile = useMobile(); const fieldAPI = useFieldAPI(); const { openSnack } = useSnackbar(); - const [fields, setFields] = useState([]); const [value, setValue] = React.useState(0); const [openHarvestSettings, setOpenHarvestSettings] = useState(false); const [fieldForPlan, setFieldForPlan] = useState(Field.create()); + const [fields, setFields] = useState([]); //all fields + const [adaptiveFields, setAdaptiveFields] = useState([]) + const [jdFields, setJDFields] = useState([]) + const [cnhFields, setCNHFields] = useState([]) const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => { setValue(newValue); @@ -59,7 +65,7 @@ export default function FieldList() { fieldAPI .listFields(500, 0, "asc", "fieldName", undefined, as) .then(resp => { - setFields(resp.data.fields.map(f => Field.any(f))); + setAdaptiveFields(resp.data.fields.map(f => Field.any(f))); // resp.data.fields.forEach(f => { // if (f.settings) { // fieldAPI.updateField(f.settings.key, f.settings); @@ -75,7 +81,11 @@ export default function FieldList() { loadFields(); }, [loadFields]); - const calcTotalAcres = () => { + useEffect(()=>{ + setFields(adaptiveFields.concat(jdFields, cnhFields)) + },[adaptiveFields,jdFields,cnhFields]) + + const calcTotalAcres = (fields: Field[]) => { let totalAcres = 0; fields.forEach(field => { totalAcres += field.acres(); @@ -84,31 +94,104 @@ export default function FieldList() { return totalAcres; }; - const listFieldsInfo = fields.map((field, index) => ( - - {field.fieldName()} - {field.landLoc()} - - {field.crop() === pond.Grain.GRAIN_CUSTOM - ? field.customType() - : GrainDescriber(field.crop()).name} - - {field.calculateAcres()} - - {field.permissions.includes(pond.Permission.PERMISSION_WRITE) && ( - - )} - - - )); + // const fieldList = (fields: Field[]) => { + // return fields.map((field, index) => ( + // + // {field.fieldName()} + // {field.landLoc()} + // + // {field.crop() === pond.Grain.GRAIN_CUSTOM + // ? field.customType() + // : GrainDescriber(field.crop()).name} + // + // {field.calculateAcres()} + // + // {field.permissions.includes(pond.Permission.PERMISSION_WRITE) && ( + // + // )} + // + // + // )); + // } + + // const fieldTable = (fields: Field[]) => { + // return ( + // + // + // + // + // Field Name + // Land Location + // Main Crop Type + // Acres ({calcTotalAcres(fields)} Total) + // Create New Plan + // + // + // {fieldList(fields)} + //
+ //
+ // ) + // } + +const columns: Column[] = [ + { + title: "View", + render: row => { + return ( + + + + ) + } + }, + { + title: "Field Name", + render: row => { + return {row.name()} + } + }, + { + title: "Current Crop", + render: row => { + return {row.crop()} + } + }, + { + title: "Acres", + render: row => { + return {row.acres()} + } + }, + { + title: "Actions", + render: row => { + return Field Actions Here + } + } +] + + const fieldTable = (fields: Field[]) => { + return ( + + columns={columns} + rows={fields} + setPage={()=>{}} + hidePagination + handleRowsPerPageChange={()=>{}} + page={0} + pageSize={10} + total={fields.length} + /> + ) + } return ( @@ -116,33 +199,23 @@ export default function FieldList() { value={value} onChange={handleChange} TabIndicatorProps={{ style: { background: "rgba(255,255,0,255)" } }}> - - {/* {!isMobile && } - {!isMobile && } */} + + + + - - - - - Field Name - Land Location - Main Crop Type - Acres ({calcTotalAcres()} Total) - Create New Plan - - - {listFieldsInfo} -
-
+ {fieldTable(fields)}
- {/* need to re-factor the harvest table file before restoring these functions */} - {/* - + + {fieldTable(adaptiveFields)} - - */} + {fieldTable(jdFields)} + + + {fieldTable(cnhFields)} + setOpenHarvestSettings(false)} diff --git a/src/field/Fieldminimap.tsx b/src/field/Fieldminimap.tsx new file mode 100644 index 0000000..489877b --- /dev/null +++ b/src/field/Fieldminimap.tsx @@ -0,0 +1,24 @@ +import { Box } from "@mui/material"; +import { Field } from "models"; +import { useRef } from "react"; +import Map, { MapRef } from "react-map-gl/mapbox"; +import 'mapbox-gl/dist/mapbox-gl.css'; + +interface Props { + field?: Field +} + +export default function FieldMinimap(props: Props) { + const { field } = props + const MAPBOX_TOKEN = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN; + + + return ( + + + ) +} \ No newline at end of file From 399f449ca4a19c751aa46deabbc16b52cdb6e787 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Wed, 20 Aug 2025 10:55:34 -0600 Subject: [PATCH 02/36] proto change --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ee0e9b7..3abca39 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "mui-tel-input": "^7.0.0", "notistack": "^3.0.1", "openweathermap-ts": "^1.2.10", - "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#master", + "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#field_dashboard", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", From 96958c12a0f10b13025e77bcccb61b4606d099f1 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Wed, 20 Aug 2025 16:02:43 -0600 Subject: [PATCH 03/36] got the field on the map and the setting the initial bounding box --- src/field/FieldList.tsx | 52 ++------------------------------------ src/field/Fieldminimap.tsx | 48 ++++++++++++++++++++++++++++++----- src/models/Field.ts | 37 +++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 57 deletions(-) diff --git a/src/field/FieldList.tsx b/src/field/FieldList.tsx index 6f1a7e7..f30d6dd 100644 --- a/src/field/FieldList.tsx +++ b/src/field/FieldList.tsx @@ -94,59 +94,12 @@ export default function FieldList() { return totalAcres; }; - // const fieldList = (fields: Field[]) => { - // return fields.map((field, index) => ( - // - // {field.fieldName()} - // {field.landLoc()} - // - // {field.crop() === pond.Grain.GRAIN_CUSTOM - // ? field.customType() - // : GrainDescriber(field.crop()).name} - // - // {field.calculateAcres()} - // - // {field.permissions.includes(pond.Permission.PERMISSION_WRITE) && ( - // - // )} - // - // - // )); - // } - - // const fieldTable = (fields: Field[]) => { - // return ( - // - // - // - // - // Field Name - // Land Location - // Main Crop Type - // Acres ({calcTotalAcres(fields)} Total) - // Create New Plan - // - // - // {fieldList(fields)} - //
- //
- // ) - // } - const columns: Column[] = [ { title: "View", render: row => { return ( - + ) @@ -184,10 +137,9 @@ const columns: Column[] = [ columns={columns} rows={fields} setPage={()=>{}} - hidePagination handleRowsPerPageChange={()=>{}} page={0} - pageSize={10} + pageSize={1} total={fields.length} /> ) diff --git a/src/field/Fieldminimap.tsx b/src/field/Fieldminimap.tsx index 489877b..762a622 100644 --- a/src/field/Fieldminimap.tsx +++ b/src/field/Fieldminimap.tsx @@ -1,24 +1,58 @@ -import { Box } from "@mui/material"; +import { CircularProgress } from "@mui/material"; import { Field } from "models"; -import { useRef } from "react"; -import Map, { MapRef } from "react-map-gl/mapbox"; +import { useEffect, useState } from "react"; +import Map from "react-map-gl/mapbox"; import 'mapbox-gl/dist/mapbox-gl.css'; +import GeoMapLayer from "maps/mapLayers/geoMapLayer"; +import { Feature, FeatureCollection } from "geojson"; +import { GeometryMapping } from "models/GeometryMapping"; interface Props { - field?: Field + field: Field } export default function FieldMinimap(props: Props) { const { field } = props const MAPBOX_TOKEN = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN; + const [geoCollection, setGeoCollection] = useState(); + + useEffect(()=>{ + let fieldData = field.settings.fieldGeoData + if(fieldData){ + let fieldFeature = GeometryMapping.geoJSON(fieldData.geoShape, fieldData.shapes, fieldData.holes) as Feature; + fieldFeature.id = fieldData.objectKey; + fieldFeature.properties = { + title: fieldData.title, + objectKey: fieldData.objectKey, + fill: fieldData.colour, + lineWidth: fieldData.geoShape === "LineString" ? 5 : 2, + origin: fieldData.origin + }; + let collection: FeatureCollection = { + type: "FeatureCollection", + features: [fieldFeature] + }; + setGeoCollection(collection); + } + },[field]) - return ( + return geoCollection ? + ( + {}} /> - - ) + + ) + : + () } \ No newline at end of file diff --git a/src/models/Field.ts b/src/models/Field.ts index 25b3263..3816fce 100644 --- a/src/models/Field.ts +++ b/src/models/Field.ts @@ -5,6 +5,7 @@ import area from "@turf/area"; import { Feature } from "geojson"; import { GeometryMapping } from "./GeometryMapping"; import GrainDescriber from "grain/GrainDescriber"; +import { LngLat, LngLatBounds } from "mapbox-gl"; export class Field { public settings: pond.FieldSettings = pond.FieldSettings.create(); @@ -152,6 +153,42 @@ export class Field { return coords; } + /** + * Returns a LngLatBounds object containing the southwest and northeast corners of a box to contain the field, spacing can also be provided + * to pad the sides of the bounding area + * @param spacing - an optional paramater that will expand the bounding area by the given long lat amount + * @returns LngLatBounds - an object containing the southwest and northeast coordinates of a bounding box + */ + public fieldBounds(spacing?: number): LngLatBounds { + let minLong = 0; + let minLat = 0; + let maxLong = 0; + let maxLat = 0; + if (this.settings.fieldGeoData) { + 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; + }); + }); + } + // let southWest = [minLong,minLat] + // let northEast = [maxLong,maxLat] + if(spacing){ + minLong = minLong - spacing + minLat = minLat - spacing + maxLong = maxLong + spacing + maxLat = maxLat + spacing + } + return new LngLatBounds( + new LngLat(minLong, minLat), + new LngLat(maxLong, maxLat) + ); + // return[southWest, northEast] + } + public grainName(): string { if (this.grain() !== pond.Grain.GRAIN_INVALID) { if (this.grain() === pond.Grain.GRAIN_CUSTOM) { From 3567190c3a020896206b77368fd4c128fabc9084 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Fri, 22 Aug 2025 09:39:20 -0600 Subject: [PATCH 04/36] added fields page data api call --- package-lock.json | 2 +- src/bin/BinsList.tsx | 2 +- src/providers/pond/fieldAPI.tsx | 37 +++++++++++++++++++++++++++++++-- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index ecde9fc..6518d2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10911,7 +10911,7 @@ }, "node_modules/protobuf-ts": { "version": "1.0.0", - "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#37e19bde4218b369e028606c009729112668ccc6", + "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#1b6076608722fff5290457b077604bb0b38d5499", "dependencies": { "protobufjs": "^6.8.8" } diff --git a/src/bin/BinsList.tsx b/src/bin/BinsList.tsx index 3ae7428..2b0ae56 100644 --- a/src/bin/BinsList.tsx +++ b/src/bin/BinsList.tsx @@ -118,7 +118,7 @@ export default function BinsList(props: Props) { xs: 6, sm: 4, md: 3, - lg: 2, + lg: 1.5, }} key={i} className={classes.gridListTile} diff --git a/src/providers/pond/fieldAPI.tsx b/src/providers/pond/fieldAPI.tsx index 37be160..17d188e 100644 --- a/src/providers/pond/fieldAPI.tsx +++ b/src/providers/pond/fieldAPI.tsx @@ -18,13 +18,21 @@ export interface IFieldAPIContext { otherTeam?: string, asRoot?: boolean ) => Promise>; + fieldsPageData: ( + limit: number, + offset: number, + order?: "asc" | "desc", + orderBy?: string, + search?: string, + otherTeam?: string, + ) => Promise> removeField: (key: string, otherTeam?: string) => Promise>; updateField: ( key: string, field: pond.FieldSettings, asRoot?: true, otherTeam?: string - ) => Promise>; + ) => Promise>; } export const FieldAPIContext = createContext({} as IFieldAPIContext); @@ -80,6 +88,30 @@ export default function FieldProvider(props: PropsWithChildren) { ); }; + const fieldsPageData = ( + limit: number, + offset: number, + order?: "asc" | "desc", + orderBy?: string, + search?: string, + otherTeam?: string, + ) => { +const view = otherTeam ? otherTeam : as + return get( + pondURL( + "/fieldsPage" + + "?limit=" + + limit + + "&offset=" + + offset + + ("&order=" + or(order, "asc")) + + ("&by=" + or(orderBy, "key")) + + (view ? "&as=" + view : "") + + (search ? "&search=" + search : "") + ) + ); + } + const updateField = (key: string, field: pond.FieldSettings, asRoot?: boolean, otherTeam?: string) => { const view = otherTeam ? otherTeam : as if (view) @@ -100,7 +132,8 @@ export default function FieldProvider(props: PropsWithChildren) { getField, listFields, removeField, - updateField + updateField, + fieldsPageData }}> {children} From 4db21a3d0c9d0da2a5a3f00ce3aa977dad14adf8 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Tue, 26 Aug 2025 14:33:57 -0600 Subject: [PATCH 05/36] finished designing the list and mobile view of the field list --- package-lock.json | 2 +- src/component/ComponentCard.tsx | 1 - src/field/FieldList.tsx | 252 ++++++++++++++++++++------- src/field/Fieldminimap.tsx | 9 +- src/field/ShareAllFields.tsx | 43 +++++ src/maps/mapLayers/geoMapLayer.tsx | 8 +- src/models/Field.ts | 10 +- src/providers/pond/fieldAPI.tsx | 74 ++++---- src/providers/pond/permissionAPI.tsx | 1 - 9 files changed, 287 insertions(+), 113 deletions(-) create mode 100644 src/field/ShareAllFields.tsx diff --git a/package-lock.json b/package-lock.json index 6518d2b..6292c47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10911,7 +10911,7 @@ }, "node_modules/protobuf-ts": { "version": "1.0.0", - "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#1b6076608722fff5290457b077604bb0b38d5499", + "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#70351a87aa4a83bd6274adf8e3b7937090ab4cc2", "dependencies": { "protobufjs": "^6.8.8" } diff --git a/src/component/ComponentCard.tsx b/src/component/ComponentCard.tsx index bf83696..6d430d5 100644 --- a/src/component/ComponentCard.tsx +++ b/src/component/ComponentCard.tsx @@ -285,7 +285,6 @@ export default function ComponentCard(props: Props) { // ) : ( diff --git a/src/field/FieldList.tsx b/src/field/FieldList.tsx index f30d6dd..7362fd8 100644 --- a/src/field/FieldList.tsx +++ b/src/field/FieldList.tsx @@ -1,7 +1,13 @@ import { + Accordion, + AccordionDetails, + AccordionSummary, Box, Button, + Grid2, + IconButton, Paper, + Stack, Tab, Table, TableBody, @@ -13,15 +19,20 @@ import { Typography } from "@mui/material"; import React, { useCallback, useEffect, useState } from "react"; -import { useMobile, useSnackbar } from "hooks"; -import { useGlobalState, useFieldAPI } from "providers"; +import { useMobile, useSnackbar, useUserAPI } from "hooks"; +import { useGlobalState, useFieldAPI, useJohnDeereProxyAPI, useCNHiProxyAPI } from "providers"; //import HarvestTable from "harvestPlan/HarvestTable"; -import { Field } from "models"; +import { Field, fieldScope, teamScope } from "models"; import GrainDescriber from "grain/GrainDescriber"; import { pond } from "protobuf-ts/pond"; import HarvestSettings from "harvestPlan/HarvestSettings"; import ResponsiveTable, { Column } from "common/ResponsiveTable"; import FieldMinimap from "./Fieldminimap"; +import FieldActions from "./FieldActions"; +import FieldSettings from "./FieldSettings"; +import { ExpandMore, Settings } from "@mui/icons-material"; +import { cloneDeep } from "lodash"; +import ShareAllFields from "./ShareAllFields"; interface TabPanelProps { children?: React.ReactNode; @@ -29,61 +40,73 @@ interface TabPanelProps { value: any; } -function TabPanelMine(props: TabPanelProps) { - const { children, value, index, ...other } = props; - - return ( - - ); -} - export default function FieldList() { const [{ as }] = useGlobalState(); const isMobile = useMobile(); const fieldAPI = useFieldAPI(); + const jdAPI = useJohnDeereProxyAPI(); + const cnhAPI = useCNHiProxyAPI(); const { openSnack } = useSnackbar(); - const [value, setValue] = React.useState(0); + const [tabValue, setTabValue] = React.useState(0); const [openHarvestSettings, setOpenHarvestSettings] = useState(false); - const [fieldForPlan, setFieldForPlan] = useState(Field.create()); - const [fields, setFields] = useState([]); //all fields - const [adaptiveFields, setAdaptiveFields] = useState([]) - const [jdFields, setJDFields] = useState([]) - const [cnhFields, setCNHFields] = useState([]) + // const [fieldForPlan, setFieldForPlan] = useState(Field.create()); + const [fields, setFields] = useState([]); + const [total, setTotal] = useState(0) + const [rowsPerPage, setRowsPerPage] = useState(5) + const [page, setPage] = useState(0) + const [selectedField, setSelectedField] = useState() + const [openFieldSettings, setOpenFieldSettings] = useState(false) + const [shareOpen, setShareOpen] = useState(false) const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => { - setValue(newValue); + setTabValue(newValue); + setPage(0) }; - const loadFields = useCallback(() => { + const loadAdaptiveFields = useCallback(() => { fieldAPI - .listFields(500, 0, "asc", "fieldName", undefined, as) + .listFields(rowsPerPage, page*rowsPerPage, "asc", "fieldName", undefined, as) .then(resp => { - setAdaptiveFields(resp.data.fields.map(f => Field.any(f))); - // resp.data.fields.forEach(f => { - // if (f.settings) { - // fieldAPI.updateField(f.settings.key, f.settings); - // } - // }); + setFields(resp.data.fields.map(f => Field.create(f))); + setTotal(resp.data.total) }) .catch(err => { - openSnack("Failed to load Field Mapping"); + openSnack("Failed to load Fields"); }); - }, [fieldAPI, as, openSnack]); + }, [fieldAPI, as, openSnack, page, rowsPerPage]); + + const loadJDFields = useCallback(() => { + jdAPI.listFields(rowsPerPage, page*rowsPerPage, as) + .then(resp => { + resp.data.fields ? setFields(resp.data.fields.map(f => Field.create(f))) : setFields([]) + }).catch(err => { + openSnack("Failed to load John Deere Fields"); + }) + },[jdAPI, as, openSnack, page, rowsPerPage]) + + const loadCNHFields = useCallback(() => { + cnhAPI.listFields(rowsPerPage, page*rowsPerPage, as) + .then(resp => { + resp.data.fields ? setFields(resp.data.fields.map(f => Field.create(f))) : setFields([]) + }).catch(err => { + openSnack("Failed to load John Deere Fields"); + }) + },[cnhAPI, as, openSnack, page, rowsPerPage]) useEffect(() => { - loadFields(); - }, [loadFields]); - - useEffect(()=>{ - setFields(adaptiveFields.concat(jdFields, cnhFields)) - },[adaptiveFields,jdFields,cnhFields]) + //based on the current tab load the correct fields + switch(tabValue){ + case 0: + loadAdaptiveFields(); + break; + case 1: + loadJDFields(); + break; + case 2: + loadCNHFields(); + break; + } + }, [loadAdaptiveFields, loadJDFields, loadCNHFields, tabValue]); const calcTotalAcres = (fields: Field[]) => { let totalAcres = 0; @@ -94,6 +117,19 @@ export default function FieldList() { return totalAcres; }; + const fieldActions = (field: Field) => { + return ( + + { + setSelectedField(field) + setOpenFieldSettings(true) + }}> + {}}/> + + ) + } + const columns: Column[] = [ { title: "View", @@ -108,26 +144,25 @@ const columns: Column[] = [ { title: "Field Name", render: row => { - return {row.name()} + return {row.name()} } }, { title: "Current Crop", render: row => { - return {row.crop()} + return {GrainDescriber(row.crop()).name} } }, { title: "Acres", render: row => { - return {row.acres()} + return {row.acres()} } }, { title: "Actions", - render: row => { - return Field Actions Here - } + hidden: tabValue > 0, + render: row => fieldActions(row) } ] @@ -136,43 +171,126 @@ const columns: Column[] = [ columns={columns} rows={fields} - setPage={()=>{}} - handleRowsPerPageChange={()=>{}} - page={0} - pageSize={1} - total={fields.length} + setPage={(newPage)=>{ + setPage(newPage) + }} + handleRowsPerPageChange={(e)=>{setRowsPerPage(e.target.value)}} + page={page} + pageSize={rowsPerPage} + total={total} + resizeable + loadMore={() => { + let currentFields = cloneDeep(fields) + switch(tabValue){ + case 0: + fieldAPI.listFields(rowsPerPage, currentFields.length, "asc", "fieldName", undefined, as).then(resp => { + setFields(currentFields.concat(resp.data.fields.map(f => Field.create(f)))) + }).catch(err => { + openSnack("Failed to load more fields") + }) + break; + case 1: + jdAPI.listFields(rowsPerPage, currentFields.length, as).then(resp => { + resp.data.fields ? setFields(currentFields.concat(resp.data.fields.map(f => Field.create(f)))) : setFields([]) + }).catch(err => { + openSnack("Failed to load more fields") + }) + break; + case 2: + cnhAPI.listFields(rowsPerPage, currentFields.length, as).then(resp => { + resp.data.fields ? setFields(currentFields.concat(resp.data.fields.map(f => Field.create(f)))) : setFields([]) + }).catch(err => { + openSnack("Failed to load more fields") + }) + break; + } + }} + renderMobile={(row, index) => { + return ( + + }> + + + + {row.name()} + + + + {fieldActions(row)} + + + + + + + + + + + + + {row.grainName()} + {row.acres()} Acres + + + + + + ) + }} /> ) } return ( + {isMobile && + + } - + {!isMobile && + + } - - {fieldTable(fields)} - - - {fieldTable(adaptiveFields)} - - - {fieldTable(jdFields)} - - - {fieldTable(cnhFields)} - - setOpenHarvestSettings(false)} field={fieldForPlan} + /> */} + { + //will need to re-load the fields after one is deleted + loadAdaptiveFields() + }} + open={openFieldSettings} + onClose={() => { + setOpenFieldSettings(false); + }} /> + {setShareOpen(false)}}/> ); } \ No newline at end of file diff --git a/src/field/Fieldminimap.tsx b/src/field/Fieldminimap.tsx index 762a622..0752610 100644 --- a/src/field/Fieldminimap.tsx +++ b/src/field/Fieldminimap.tsx @@ -40,16 +40,17 @@ export default function FieldMinimap(props: Props) { return geoCollection ? ( {}} /> ) diff --git a/src/field/ShareAllFields.tsx b/src/field/ShareAllFields.tsx new file mode 100644 index 0000000..366ee16 --- /dev/null +++ b/src/field/ShareAllFields.tsx @@ -0,0 +1,43 @@ +import { Dialog, DialogActions, DialogContent, DialogTitle } from "@mui/material"; +import ResponsiveDialog from "common/ResponsiveDialog"; + +interface Props { + open: boolean + close: () => void +} + +export default function ShareAllFields(props: Props) { + const {open, close} = props + + const share = () => { + //if sharing to team use shareAllByKey + + //if sharing to user use shareAll + } + + const target = () => { + // toggle for whether sharing to user or team + // if sharing to user have a text field + // if sharing to team have the team selector + } + + const permissions = () => { + // the checkboxes of permissions + } + + const actions = () => { + //the buttons to shaare or cancel + } + + return ( + + Share All Fields + + Content for selecting target and permissions + + + Buttons for cancel and confirm + + + ) +} \ No newline at end of file diff --git a/src/maps/mapLayers/geoMapLayer.tsx b/src/maps/mapLayers/geoMapLayer.tsx index 5b90854..1923b64 100644 --- a/src/maps/mapLayers/geoMapLayer.tsx +++ b/src/maps/mapLayers/geoMapLayer.tsx @@ -6,7 +6,7 @@ interface Props { objectCollection: FeatureCollection; measurementCollection?: FeatureCollection; showTitle?: boolean; - setInteractiveLayers: (layers: string[]) => void; + setInteractiveLayers?: (layers: string[]) => void; } export default function GeoMapLayer(props: Props) { @@ -18,7 +18,9 @@ export default function GeoMapLayer(props: Props) { // }, [measurementCollection, objectCollection]); useEffect(() => { - setInteractiveLayers(["shapefill", "shapeborder", "measurementBorder"]); + if(setInteractiveLayers){ + setInteractiveLayers(["shapefill", "shapeborder", "measurementBorder"]); + } }, []); // eslint-disable-line react-hooks/exhaustive-deps return ( @@ -60,6 +62,7 @@ export default function GeoMapLayer(props: Props) { /> )} + {measurementCollection && + } ); } diff --git a/src/models/Field.ts b/src/models/Field.ts index 3816fce..d2061d1 100644 --- a/src/models/Field.ts +++ b/src/models/Field.ts @@ -10,14 +10,16 @@ import { LngLat, LngLatBounds } from "mapbox-gl"; export class Field { public settings: pond.FieldSettings = pond.FieldSettings.create(); public status: pond.FieldStatus = pond.FieldStatus.create(); - public permissions: pond.Permission[] = []; + 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; + //the from object is required here to deal with a protobuf quirk where enums would be seen as strings at runtime, but the code sees them as the actual value + let field = pond.Field.fromObject(cloneDeep(or(pf, pond.Field.create()))) + my.settings = field.settings ? field.settings : pond.FieldSettings.create() + my.status = field.status ? field.status : pond.FieldStatus.create() + my.permissions = field.fieldPermissions } return my; } diff --git a/src/providers/pond/fieldAPI.tsx b/src/providers/pond/fieldAPI.tsx index 17d188e..93309df 100644 --- a/src/providers/pond/fieldAPI.tsx +++ b/src/providers/pond/fieldAPI.tsx @@ -5,6 +5,7 @@ import { useGlobalState } from "providers"; import React, { createContext, PropsWithChildren, useContext } from "react"; import { or } from "utils"; import { pondURL } from "./pond"; +import { permissionToString } from "pbHelpers/Permission"; export interface IFieldAPIContext { addField: (field: pond.FieldSettings, otherTeam?: string) => Promise; @@ -18,14 +19,6 @@ export interface IFieldAPIContext { otherTeam?: string, asRoot?: boolean ) => Promise>; - fieldsPageData: ( - limit: number, - offset: number, - order?: "asc" | "desc", - orderBy?: string, - search?: string, - otherTeam?: string, - ) => Promise> removeField: (key: string, otherTeam?: string) => Promise>; updateField: ( key: string, @@ -33,6 +26,14 @@ export interface IFieldAPIContext { asRoot?: true, otherTeam?: string ) => Promise>; + shareAll: ( + email: string, + permissions: pond.Permission[], + ) => Promise> + shareAllByKey: ( + key: string, + permissions: pond.Permission[] + ) => Promise> } export const FieldAPIContext = createContext({} as IFieldAPIContext); @@ -88,30 +89,6 @@ export default function FieldProvider(props: PropsWithChildren) { ); }; - const fieldsPageData = ( - limit: number, - offset: number, - order?: "asc" | "desc", - orderBy?: string, - search?: string, - otherTeam?: string, - ) => { -const view = otherTeam ? otherTeam : as - return get( - pondURL( - "/fieldsPage" + - "?limit=" + - limit + - "&offset=" + - offset + - ("&order=" + or(order, "asc")) + - ("&by=" + or(orderBy, "key")) + - (view ? "&as=" + view : "") + - (search ? "&search=" + search : "") - ) - ); - } - const updateField = (key: string, field: pond.FieldSettings, asRoot?: boolean, otherTeam?: string) => { const view = otherTeam ? otherTeam : as if (view) @@ -125,6 +102,36 @@ const view = otherTeam ? otherTeam : as ); }; + const shareAll = (email: string, permissions: pond.Permission[]) => { + let url = pondURL("/fields/shareAll") + if (as) url = url + "?as=" + as + return new Promise((resolve, reject) => { + post(url, { + email: email, + permissions: permissions.map(permission => permissionToString(permission)) + }).then(resp => { + return resolve(resp) + }).catch(err => { + return reject(err) + }) + }) + } + + const shareAllByKey = (key: string, permissions: pond.Permission[]) => { + let url = pondURL("/fields/shareAllByKey") + if (as) url = url + "?as=" + as + return new Promise((resolve, reject) => { + post(url, { + key: key, + permissions: permissions.map(permission => permissionToString(permission)) + }).then(resp => { + return resolve(resp) + }).catch(err => { + return reject(err) + }) + }) + } + return ( {children} diff --git a/src/providers/pond/permissionAPI.tsx b/src/providers/pond/permissionAPI.tsx index 2311afd..91b7d6a 100644 --- a/src/providers/pond/permissionAPI.tsx +++ b/src/providers/pond/permissionAPI.tsx @@ -135,7 +135,6 @@ export default function PermissionProvider(props: PropsWithChildren) { let url = pondURL("/" + scope.kind + "s/" + scope.key + "/shareByKey") if (as) url = pondURL(`/${scope.kind}s/${scope.key}/shareByKey?as=${as}`) return new Promise((resolve, reject) => { - post(url, { key: key, permissions: permissions.map(permission => permissionToString(permission)) From 3802691977cc533e7ff49284b342544539dc7b21 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Wed, 27 Aug 2025 16:06:59 -0600 Subject: [PATCH 06/36] added way for users to share all fields --- src/field/FieldList.tsx | 15 --- src/field/ShareAllFields.tsx | 170 ++++++++++++++++++++++++++++++-- src/models/Field.ts | 8 +- src/providers/pond/fieldAPI.tsx | 4 +- 4 files changed, 173 insertions(+), 24 deletions(-) diff --git a/src/field/FieldList.tsx b/src/field/FieldList.tsx index 7362fd8..8b03843 100644 --- a/src/field/FieldList.tsx +++ b/src/field/FieldList.tsx @@ -244,14 +244,12 @@ const columns: Column[] = [ return ( - {isMobile && - } [] = [ - {!isMobile && - - } {fieldTable(fields)} {/* ("") + const [user, setUser] = useState("") + const [sharedPermissions, setSharedPermissions] = useState([ + pond.Permission.PERMISSION_READ + ]) const share = () => { - //if sharing to team use shareAllByKey + if(teamShare){ + //if sharing to team use shareAllByKey + fieldAPI.shareAllByKey(team, sharedPermissions) + .then(resp => { + openSnack("Shared all fields to team") + }).catch(err => { + openSnack("There was a problem sharing the fields") + }) + }else{ + //if sharing to user use shareAll + fieldAPI.shareAll(user, sharedPermissions) + .then(resp => { + openSnack("Shared all fields to user") + }).catch(err => { + openSnack("There was a problem sharing the fields") + }) + } - //if sharing to user use shareAll } const target = () => { // toggle for whether sharing to user or team + return ( + + + User + + { + setTeamShare(checked) + }} + name="storage" + /> + + Team + + {teamShare ? + + {setTeam(teamKey)}}/> + : + + { + setUser(e.target.value) + }} + /> + } + + ) // if sharing to user have a text field - // if sharing to team have the team selector + } + + const changePermissions = (checked: boolean, permission: pond.Permission) => { + let currentPerms = cloneDeep(sharedPermissions) + if(checked){ + //if the permissions does not include the permission add it + if(!sharedPermissions.includes(permission)) currentPerms.push(permission) + }else{ + //if the permissions includes the permission remove it + if(sharedPermissions.includes(permission)) currentPerms.splice(currentPerms.indexOf(permission), 1) + } + setSharedPermissions(currentPerms) } const permissions = () => { // the checkboxes of permissions + return ( + + + Permissions + + {changePermissions(checked, pond.Permission.PERMISSION_READ)}} + value={pond.Permission.PERMISSION_READ as pond.Permission} + /> + } + label="View" + labelPlacement="end" + /> + {changePermissions(checked, pond.Permission.PERMISSION_WRITE)}} + value={pond.Permission.PERMISSION_WRITE as pond.Permission} + /> + } + label="Edit" + labelPlacement="end" + /> + {changePermissions(checked, pond.Permission.PERMISSION_SHARE)}} + value={pond.Permission.PERMISSION_SHARE as pond.Permission} + /> + } + label="Share" + labelPlacement="end" + /> + {changePermissions(checked, pond.Permission.PERMISSION_USERS)}} + value={pond.Permission.PERMISSION_USERS as pond.Permission} + /> + } + label="Users" + labelPlacement="end" + /> + {changePermissions(checked, pond.Permission.PERMISSION_FILE_MANAGEMENT)}} + value={pond.Permission.PERMISSION_FILE_MANAGEMENT as pond.Permission} + /> + } + label="Files" + labelPlacement="end" + /> + {/* billing */} + {changePermissions(checked, pond.Permission.PERMISSION_BILLING)}} + value={pond.Permission.PERMISSION_BILLING as pond.Permission} + /> + } + label="Billing" + labelPlacement="end" + /> + + + + ) } const actions = () => { //the buttons to shaare or cancel + return ( + + + + + ) } return ( Share All Fields - Content for selecting target and permissions + + {target()} + {permissions()} + - Buttons for cancel and confirm + {actions()} ) diff --git a/src/models/Field.ts b/src/models/Field.ts index d2061d1..d85b294 100644 --- a/src/models/Field.ts +++ b/src/models/Field.ts @@ -161,7 +161,7 @@ export class Field { * @param spacing - an optional paramater that will expand the bounding area by the given long lat amount * @returns LngLatBounds - an object containing the southwest and northeast coordinates of a bounding box */ - public fieldBounds(spacing?: number): LngLatBounds { + public fieldBounds(spacing?: number, squared?: boolean): LngLatBounds { let minLong = 0; let minLat = 0; let maxLong = 0; @@ -178,6 +178,12 @@ export class Field { } // let southWest = [minLong,minLat] // let northEast = [maxLong,maxLat] + if (squared){ + //make sure the bounds are squared so as to fit the entire field in the map + //get the diff between the longs and the lats + //find which is narrower + //take half of the difference between the differences + } if(spacing){ minLong = minLong - spacing minLat = minLat - spacing diff --git a/src/providers/pond/fieldAPI.tsx b/src/providers/pond/fieldAPI.tsx index 93309df..803be77 100644 --- a/src/providers/pond/fieldAPI.tsx +++ b/src/providers/pond/fieldAPI.tsx @@ -103,7 +103,7 @@ export default function FieldProvider(props: PropsWithChildren) { }; const shareAll = (email: string, permissions: pond.Permission[]) => { - let url = pondURL("/fields/shareAll") + let url = pondURL("/shareAllFields") if (as) url = url + "?as=" + as return new Promise((resolve, reject) => { post(url, { @@ -118,7 +118,7 @@ export default function FieldProvider(props: PropsWithChildren) { } const shareAllByKey = (key: string, permissions: pond.Permission[]) => { - let url = pondURL("/fields/shareAllByKey") + let url = pondURL("/shareAllFieldsByKey") if (as) url = url + "?as=" + as return new Promise((resolve, reject) => { post(url, { From b3a8a3b25f2591200033705342c7f95f1db0f14d Mon Sep 17 00:00:00 2001 From: csawatzky Date: Thu, 28 Aug 2025 10:50:05 -0600 Subject: [PATCH 07/36] added option to the field minimap and the field bounds for the options to adjust the bounds to make them squared for longer/taller fields --- src/field/FieldList.tsx | 2 +- src/field/Fieldminimap.tsx | 6 ++++-- src/models/Field.ts | 15 +++++++++++++-- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/field/FieldList.tsx b/src/field/FieldList.tsx index 8b03843..6b074f8 100644 --- a/src/field/FieldList.tsx +++ b/src/field/FieldList.tsx @@ -136,7 +136,7 @@ const columns: Column[] = [ render: row => { return ( - + ) } diff --git a/src/field/Fieldminimap.tsx b/src/field/Fieldminimap.tsx index 0752610..d657ae5 100644 --- a/src/field/Fieldminimap.tsx +++ b/src/field/Fieldminimap.tsx @@ -9,10 +9,12 @@ import { GeometryMapping } from "models/GeometryMapping"; interface Props { field: Field + squared?: boolean + borderSpacing?: number } export default function FieldMinimap(props: Props) { - const { field } = props + const { field, squared, borderSpacing } = props const MAPBOX_TOKEN = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN; const [geoCollection, setGeoCollection] = useState(); @@ -42,7 +44,7 @@ export default function FieldMinimap(props: Props) { latDiff){ + //if the longitude is the narrow bound apply half of the diff between the diffs to the min and max lats + let halfDiff = (longDiff - latDiff)/2 + minLat = minLat - halfDiff + maxLat = maxLat + halfDiff + } else { + //if the longitude is not the narrow bound apply half of the diff between the diffs to the min and max longs + let halfDiff = (latDiff - longDiff)/2 + minLong = minLong - halfDiff + maxLong = maxLong + halfDiff + } } if(spacing){ minLong = minLong - spacing @@ -194,7 +206,6 @@ export class Field { new LngLat(minLong, minLat), new LngLat(maxLong, maxLat) ); - // return[southWest, northEast] } public grainName(): string { From 1588905a0449a98c0285768e187b864c3353b11a Mon Sep 17 00:00:00 2001 From: csawatzky Date: Thu, 28 Aug 2025 10:54:26 -0600 Subject: [PATCH 08/36] added explanation of squared parameter to fieldBounds function in the field model --- src/models/Field.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/models/Field.ts b/src/models/Field.ts index ce46db4..9ccdb4e 100644 --- a/src/models/Field.ts +++ b/src/models/Field.ts @@ -158,7 +158,8 @@ export class Field { /** * Returns a LngLatBounds object containing the southwest and northeast corners of a box to contain the field, spacing can also be provided * to pad the sides of the bounding area - * @param spacing - an optional paramater that will expand the bounding area by the given long lat amount + * @param spacing number(optional) - an optional paramater that will expand the bounding area by the given long lat amount + * @param squared boolean(optional) - when true will adjust the bounds by extending the narrower edge to match the wider edge * @returns LngLatBounds - an object containing the southwest and northeast coordinates of a bounding box */ public fieldBounds(spacing?: number, squared?: boolean): LngLatBounds { From 1fbc817abe599d99e3b499e9d76ce2f5e53b78ae Mon Sep 17 00:00:00 2001 From: csawatzky Date: Fri, 29 Aug 2025 12:36:45 -0600 Subject: [PATCH 09/36] put the first components into the field page --- src/field/FieldActions.tsx | 2 + src/field/FieldList.tsx | 21 +++++-- src/navigation/Router.tsx | 18 +++++- src/pages/Field.tsx | 123 +++++++++++++++++++++++++++++++++++++ 4 files changed, 159 insertions(+), 5 deletions(-) create mode 100644 src/pages/Field.tsx diff --git a/src/field/FieldActions.tsx b/src/field/FieldActions.tsx index fbaf090..7e0cb44 100644 --- a/src/field/FieldActions.tsx +++ b/src/field/FieldActions.tsx @@ -63,6 +63,8 @@ export default function FieldActions(props: Props) { removeSelf: false }); + console.log(permissions) + const groupMenu = () => { const canShare = permissions.includes(pond.Permission.PERMISSION_SHARE); const canManageUsers = permissions.includes(pond.Permission.PERMISSION_USERS); diff --git a/src/field/FieldList.tsx b/src/field/FieldList.tsx index 6b074f8..36d0de7 100644 --- a/src/field/FieldList.tsx +++ b/src/field/FieldList.tsx @@ -30,9 +30,11 @@ import ResponsiveTable, { Column } from "common/ResponsiveTable"; import FieldMinimap from "./Fieldminimap"; import FieldActions from "./FieldActions"; import FieldSettings from "./FieldSettings"; -import { ExpandMore, Settings } from "@mui/icons-material"; +import { ArrowForward, ExpandMore, Settings } from "@mui/icons-material"; import { cloneDeep } from "lodash"; import ShareAllFields from "./ShareAllFields"; +import EventBlocker from "common/EventBlocker"; +import { useNavigate } from "react-router-dom"; interface TabPanelProps { children?: React.ReactNode; @@ -57,6 +59,7 @@ export default function FieldList() { const [selectedField, setSelectedField] = useState() const [openFieldSettings, setOpenFieldSettings] = useState(false) const [shareOpen, setShareOpen] = useState(false) + const navigate = useNavigate(); const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => { setTabValue(newValue); @@ -117,16 +120,22 @@ export default function FieldList() { return totalAcres; }; + const goTo = (field: Field) => { + let path = field.key(); + navigate(path, { state: {field: field, permissions: field.permissions} }); + } + const fieldActions = (field: Field) => { return ( - + { setSelectedField(field) setOpenFieldSettings(true) }}> {}}/> - + {isMobile && {goTo(field)}}>} + ) } @@ -171,6 +180,9 @@ const columns: Column[] = [ columns={columns} rows={fields} + onRowClick={(row) => { + !isMobile && goTo(row) + }} setPage={(newPage)=>{ setPage(newPage) }} @@ -224,7 +236,7 @@ const columns: Column[] = [ - + @@ -232,6 +244,7 @@ const columns: Column[] = [ {row.grainName()} {row.acres()} Acres + diff --git a/src/navigation/Router.tsx b/src/navigation/Router.tsx index afe12c5..c54c291 100644 --- a/src/navigation/Router.tsx +++ b/src/navigation/Router.tsx @@ -47,6 +47,7 @@ const Contract = lazy(() => import("pages/Contract")); const JohnDeere = lazy(() => import("pages/JohnDeere")); const CNHi = lazy(() => import("pages/CNHi")); const LibraCart = lazy(() => import("pages/LibraCart")); +const FieldPage = lazy(()=>import("pages/Field")); export const appendToUrl = (appendage: number | string) => { const basePath = location.pathname.replace(/\/$/, ""); @@ -70,6 +71,7 @@ export default function Router() { } /> } /> } /> + } /> ) } @@ -284,6 +286,21 @@ export default function Router() { ); }; + const FieldsRoute = () => { + return ( + + } + /> + } + /> + + ); + } + if (isLoading) return null; // if (!isAuthenticated) { // loginWithRedirect() @@ -325,7 +342,6 @@ export default function Router() { {user.hasFeature("admin") && } /> } - } /> } /> {user.hasFeature("admin") && ( } /> diff --git a/src/pages/Field.tsx b/src/pages/Field.tsx new file mode 100644 index 0000000..9302a3b --- /dev/null +++ b/src/pages/Field.tsx @@ -0,0 +1,123 @@ +import { Field, HarvestPlan } from "models"; +import { useGlobalState } from "providers"; +import { useEffect, useState } from "react"; +import { useLocation } from "react-router-dom"; +import PageContainer from "./PageContainer"; +import { useMobile } from "hooks"; +import { Box, Grid2, IconButton } from "@mui/material"; +import FieldActions from "field/FieldActions"; +import { Settings } from "@mui/icons-material"; +import { pond } from "protobuf-ts/pond"; +import FieldMinimap from "field/Fieldminimap"; +import Weather from "weather/weather"; +import TaskViewer from "tasks/TaskViewer"; +import HarvestPlanDisplay from "harvestPlan/HarvestPlanDisplay"; + +export default function FieldPage() { + const { state } = useLocation(); + //const [{ as }] = useGlobalState() + const [field, setField] = useState(state?.field ? Field.create(state.field) : Field.create()) + const [permissions, setPermissions] = useState(state?.permissions ? state.permissions : []) + const isMobile = useMobile() + const [openFieldSettings, setOpenFieldSettings] = useState(false) + const [planLoading, setPlanLoading] = useState(false) + const [hPlan, setHPlan] = useState(HarvestPlan.create()); + + const loadField = () => { + + } + + useEffect(()=>{ + if(state.field){ + let field = Field.create(state.field) + setField(field) + setPermissions(state.permissions) + } + },[state.field]) + + const fieldActions = () => { + return ( + + { + setOpenFieldSettings(true) + }}> + {}}/> + + ) + } + + const map = () => { + return ( + + + + ) + } + + const weather = () => { + return () + } + + const harvestPlans = () => { + console.log(permissions) + return ( + { + if (updatedPlan) { + setHPlan(updatedPlan); + } + }} + />) + } + + const tasks = () => { + let taskLoadKeys: string[] = []; + if (!planLoading) { + field.key() !== "" && taskLoadKeys.push(field.key()); + //hPlan.key() !== "" && taskLoadKeys.push(hPlan.key()); + } + return () + } + + const desktopView = () => { + return ( + + + {map()} + + + {weather()} + + + {tasks()} + + + {harvestPlans()} + + + ) + } + + const mobileView = () => { + return ( + Mobile will just be the drawer view + ) + } + + const fieldSettings = () => { + + } + + return ( + + {fieldActions()} + {isMobile ? mobileView() : desktopView()} + + ) +} \ No newline at end of file From 84885dc16364fa5422cb0c707a4aae04dae442a4 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Tue, 2 Sep 2025 16:19:07 -0600 Subject: [PATCH 10/36] fixed some bugs with the viewer that were causing console errors, even though is functioned properly --- src/tasks/TaskViewer.tsx | 71 +++++++++------------------------------- 1 file changed, 15 insertions(+), 56 deletions(-) diff --git a/src/tasks/TaskViewer.tsx b/src/tasks/TaskViewer.tsx index 68ebf43..a5c02a6 100644 --- a/src/tasks/TaskViewer.tsx +++ b/src/tasks/TaskViewer.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; import { EventClickArg } from "@fullcalendar/core"; import { DateClickArg } from "@fullcalendar/interaction"; import { useGlobalState } from "providers"; -import { useMobile, useSnackbar, useThemeType } from "hooks"; +import { useMobile, useSnackbar } from "hooks"; import { useTaskAPI } from "providers"; import { useEffect, useCallback } from "react"; import TaskCalendar from "./TaskCalendar"; @@ -12,7 +12,6 @@ import { Button, Fab, Grid2 as Grid, - IconButton, LinearProgress, Theme, Typography @@ -21,8 +20,6 @@ import { Task } from "models"; import TaskSettings from "./TaskSettings"; import { DayPicker } from "react-day-picker"; import "react-day-picker/dist/style.css"; -import { CalendarToday } from "@mui/icons-material"; -import NotesIcon from "@mui/icons-material/Notes"; import AddIcon from "@mui/icons-material/Add"; import TaskDrawer from "./TaskDrawer"; import moment from "moment"; @@ -42,17 +39,7 @@ interface TabPanelProps { } const useStyles = makeStyles((theme: Theme) => { - const themeType = useThemeType(); - return ({ - avatar: { - color: themeType === "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 - }, active: { color: theme.palette.getContrastText(theme.palette.secondary.main), backgroundColor: theme.palette.secondary.main, @@ -84,18 +71,18 @@ const useStyles = makeStyles((theme: Theme) => { export default function TaskViewer(props: ViewProps) { const { objectKey, label, drawerView, loadKeys } = props; - const [{ user }] = useGlobalState(); - const [{ as }] = useGlobalState(); + const [{ user, as }] = useGlobalState(); + //const [{ as }] = useGlobalState(); const taskAPI = useTaskAPI(); const { openSnack } = useSnackbar(); - const [tasks, setTasks] = useState>(new Map()); - const [viewTask, setViewTask] = useState(Task.create()); + const [tasks, setTasks] = useState>(new Map([])); + const [viewTask, setViewTask] = useState(); const [newTaskDialog, setNewTaskDialog] = useState(false); const [editTask, setEditTask] = useState(); const [loaded, setLoaded] = useState(false); const [currentDate, setCurrentDate] = useState(new Date()); const isMobile = useMobile(); - const [value, setValue] = React.useState(0); + const [value, setValue] = useState(0); const classes = useStyles(); const [expand, setExpand] = useState(false); const [hideCal, setHideCal] = useState(false); @@ -216,33 +203,6 @@ export default function TaskViewer(props: ViewProps) { }); }; - const tabIcons = () => { - return ( - - setValue(0)}> - - - setValue(1)}> - - - - ); - }; - function TabPanelMine(props: TabPanelProps) { const { children, value, index, ...other } = props; @@ -274,7 +234,6 @@ export default function TaskViewer(props: ViewProps) { size={isMobile ? "medium" : "large"}> - {!drawerView && tabIcons()} - { - setOpenDrawer(false)} - completeTask={markComplete} - deleteTask={deleteTask} - editTask={setTaskToEdit} - /> + {viewTask && + setOpenDrawer(false)} + completeTask={markComplete} + deleteTask={deleteTask} + editTask={setTaskToEdit} + /> } ); From c69636482a6e7235a21d79fde5c6eadb7433b79f Mon Sep 17 00:00:00 2001 From: csawatzky Date: Thu, 4 Sep 2025 14:38:53 -0600 Subject: [PATCH 11/36] adding the harvest plan stuff to the field page --- src/bin/BinSettings.tsx | 8 +- src/field/FieldActions.tsx | 2 - src/harvestPlan/DuplicateHarvestPlan.tsx | 31 ++++++-- src/harvestPlan/HarvestPlanActions.tsx | 6 +- src/harvestPlan/HarvestPlanDisplay.tsx | 6 +- src/harvestPlan/HarvestPlanTable.tsx | 26 +++++++ src/pages/Field.tsx | 97 ++++++++++++++++++++---- src/providers/pond/fieldAPI.tsx | 6 +- 8 files changed, 146 insertions(+), 36 deletions(-) create mode 100644 src/harvestPlan/HarvestPlanTable.tsx diff --git a/src/bin/BinSettings.tsx b/src/bin/BinSettings.tsx index f970b2b..237a540 100644 --- a/src/bin/BinSettings.tsx +++ b/src/bin/BinSettings.tsx @@ -741,7 +741,7 @@ export default function BinSettings(props: Props) { case pond.BinInventoryControl.BIN_INVENTORY_CONTROL_HYBRID_LIDAR: return "Hybrid (lidar)" case pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART: - return "Auto (LibraCart)" + return "Auto (Libra Cart)" default: return "Manual" } @@ -905,7 +905,7 @@ export default function BinSettings(props: Props) { } - label={"Auto (LibraCart)"} + label={"Auto (Libra Cart)"} /> } @@ -951,7 +951,7 @@ export default function BinSettings(props: Props) { {inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART && { let newForm = form; @@ -963,7 +963,7 @@ export default function BinSettings(props: Props) { options={lcDestinationOptions} /> - The linked bins inventory will be adjusted When the LibraCart Destination data is synced every 6 hours + The linked bins inventory will be adjusted When the Libra Cart Destination data is synced every 6 hours } diff --git a/src/field/FieldActions.tsx b/src/field/FieldActions.tsx index 7e0cb44..fbaf090 100644 --- a/src/field/FieldActions.tsx +++ b/src/field/FieldActions.tsx @@ -63,8 +63,6 @@ export default function FieldActions(props: Props) { removeSelf: false }); - console.log(permissions) - const groupMenu = () => { const canShare = permissions.includes(pond.Permission.PERMISSION_SHARE); const canManageUsers = permissions.includes(pond.Permission.PERMISSION_USERS); diff --git a/src/harvestPlan/DuplicateHarvestPlan.tsx b/src/harvestPlan/DuplicateHarvestPlan.tsx index 5f083af..6e910c0 100644 --- a/src/harvestPlan/DuplicateHarvestPlan.tsx +++ b/src/harvestPlan/DuplicateHarvestPlan.tsx @@ -7,16 +7,17 @@ import { TextField } from "@mui/material"; import ResponsiveDialog from "common/ResponsiveDialog"; +import { Option } from "common/SearchSelect"; import { Field, HarvestPlan, Task } from "models"; import moment from "moment"; import { pond } from "protobuf-ts/pond"; -import { useGlobalState, useHarvestPlanAPI, useTaskAPI } from "providers"; +import { useFieldAPI, useGlobalState, useHarvestPlanAPI, useTaskAPI } from "providers"; import { useCallback, useEffect, useState } from "react"; interface Props { open: boolean; close: () => void; - fields: Field[]; + fields?: Field[]; plan: HarvestPlan; } @@ -29,6 +30,8 @@ export default function DuplicateHarvestPlan(props: Props) { const harvestPlanAPI = useHarvestPlanAPI(); const taskAPI = useTaskAPI(); const [ready, setReady] = useState(false); + const [fieldOptions, setFieldOptions] = useState([]) + const fieldAPI = useFieldAPI(); const loadTasks = useCallback(() => { taskAPI.listTasks(50, 0, "asc", "start", plan.key(), undefined).then(resp => { @@ -41,9 +44,25 @@ export default function DuplicateHarvestPlan(props: Props) { loadTasks(); }, [loadTasks]); + useEffect(()=>{ + if(fields){ + //if fields were passed in use those + setFieldOptions(fields) + }else{ + //otherwise load them + fieldAPI.listFields(50, 0, "asc", "fieldName").then(resp => { + setFieldOptions(resp.data.fields.map(f => Field.create(f))) + }).catch(err => { + + }) + } + },[fields]) + + //this should load the fields here + const duplicate = () => { let planSettings = HarvestPlan.clone(plan).settings; - planSettings.field = fields[newFieldIndex].key(); + planSettings.field = fieldOptions[newFieldIndex].key(); planSettings.createDate = moment.now(); if (title !== "") { planSettings.title = title; @@ -69,7 +88,7 @@ export default function DuplicateHarvestPlan(props: Props) { value={newFieldIndex} onChange={e => setNewFieldIndex(+e.target.value)} color="primary"> - {fields.map((option, i) => ( + {fieldOptions.map((option, i) => ( {option.fieldName()} @@ -98,8 +117,8 @@ export default function DuplicateHarvestPlan(props: Props) { color="primary" disabled={ !ready || - (fields[newFieldIndex] && - !fields[newFieldIndex].permissions.includes(pond.Permission.PERMISSION_WRITE)) + (fieldOptions[newFieldIndex] && + !fieldOptions[newFieldIndex].permissions.includes(pond.Permission.PERMISSION_WRITE)) }> Duplicate diff --git a/src/harvestPlan/HarvestPlanActions.tsx b/src/harvestPlan/HarvestPlanActions.tsx index 441446a..f29c1e4 100644 --- a/src/harvestPlan/HarvestPlanActions.tsx +++ b/src/harvestPlan/HarvestPlanActions.tsx @@ -13,7 +13,7 @@ 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 React, { useEffect, useState } from "react"; import ObjectUsers from "user/ObjectUsers"; import ShareObject from "user/ShareObject"; import { isOffline } from "utils/environment"; @@ -43,7 +43,7 @@ const useStyles = makeStyles((theme: Theme) => ({ interface Props { plan: HarvestPlan; planField: Field; - fieldList: Field[]; + fieldList?: Field[]; permissions: pond.Permission[]; refreshCallback: (updatedPlan?: HarvestPlan) => void; hideNew?: boolean; @@ -215,7 +215,7 @@ export default function HarvestPlanActions(props: Props) { setUpdatePlan(true); setOpenState({ ...openState, settings: true }); }}> - Edit or Add Activity + Activities )} void; } @@ -175,7 +175,7 @@ export default function HarvestPlanDisplay(props: Props) { - Crop Plan{plan.key() !== "" ? " - " + plan.settings.title : " - None Found"} + {plan.key() !== "" ? plan.settings.title : "None Found"} {!isMobile && permissions.includes(pond.Permission.PERMISSION_WRITE) && ( { + harvestAPI.listHarvestPlans(pageSize, page*pageSize, "asc", "name", field).then(resp => { + console.log(resp) + }) + },[page, pageSize, field]) + + return ( + + + + ) +} \ No newline at end of file diff --git a/src/pages/Field.tsx b/src/pages/Field.tsx index 9302a3b..b204f4a 100644 --- a/src/pages/Field.tsx +++ b/src/pages/Field.tsx @@ -1,10 +1,10 @@ -import { Field, HarvestPlan } from "models"; -import { useGlobalState } from "providers"; -import { useEffect, useState } from "react"; -import { useLocation } from "react-router-dom"; +import { Field, fieldScope, HarvestPlan, teamScope } from "models"; +import { useFieldAPI, useGlobalState, useHarvestPlanAPI } from "providers"; +import { useCallback, useEffect, useState } from "react"; +import { useLocation, useParams } from "react-router-dom"; import PageContainer from "./PageContainer"; -import { useMobile } from "hooks"; -import { Box, Grid2, IconButton } from "@mui/material"; +import { useMobile, useSnackbar, useUserAPI } from "hooks"; +import { Box, Card, Grid2, IconButton } from "@mui/material"; import FieldActions from "field/FieldActions"; import { Settings } from "@mui/icons-material"; import { pond } from "protobuf-ts/pond"; @@ -12,28 +12,82 @@ import FieldMinimap from "field/Fieldminimap"; import Weather from "weather/weather"; import TaskViewer from "tasks/TaskViewer"; import HarvestPlanDisplay from "harvestPlan/HarvestPlanDisplay"; +import { cloneDeep } from "lodash"; +import HarvestPlanTable from "harvestPlan/HarvestPlanTable"; export default function FieldPage() { const { state } = useLocation(); //const [{ as }] = useGlobalState() + const fieldAPI = useFieldAPI(); + const userAPI = useUserAPI(); + const [{as, user}] = useGlobalState(); + const {openSnack} = useSnackbar(); + const fieldID = useParams<{ fieldID: string }>()?.fieldID ?? ""; const [field, setField] = useState(state?.field ? Field.create(state.field) : Field.create()) const [permissions, setPermissions] = useState(state?.permissions ? state.permissions : []) const isMobile = useMobile() const [openFieldSettings, setOpenFieldSettings] = useState(false) const [planLoading, setPlanLoading] = useState(false) const [hPlan, setHPlan] = useState(HarvestPlan.create()); + const [loading, setLoading] = useState(false) + const hPlanAPI = useHarvestPlanAPI(); - const loadField = () => { + const loadField = useCallback(() => { - } + },[as]) useEffect(()=>{ - if(state.field){ + // if already loading do nothing + if(loading) return + //if the field is passed through in the state just use it + if(state?.field){ let field = Field.create(state.field) + field.permissions = state.permissions setField(field) setPermissions(state.permissions) + return } - },[state.field]) + //otherwise load the field + setLoading(true) + fieldAPI.getField(fieldID, as).then(resp => { + setField(Field.create(resp.data)) + + }).catch(err => { + openSnack("Failed to load Field") + }).finally(() => { + setLoading(false) + }) + //also get the permissions + let scope = fieldScope(fieldID); + if (as) { + scope = teamScope(as) + } + userAPI.getUser(user.id(), scope).then(resp => { + let f = cloneDeep(field) + f.permissions = resp.permissions + setField(f) + setPermissions(resp.permissions); + }); + },[loadField, fieldID, as]) + + useEffect(() => { + if (field.key() !== "") { + hPlanAPI + .listHarvestPlans(1, 0, "desc", "createDate", field.key(), as) + .then(resp => { + if (resp.data.harvestPlan.length > 0) { + let plan = resp.data.harvestPlan[0]; + setHPlan(HarvestPlan.any(plan)); + } else { + setHPlan(HarvestPlan.create()); + } + setPlanLoading(false); + }) + .catch(err => { + //openSnack("Failed to load plan"); + }); + } + }, [field, as, hPlanAPI]); const fieldActions = () => { return ( @@ -60,14 +114,12 @@ export default function FieldPage() { } const harvestPlans = () => { - console.log(permissions) return ( { if (updatedPlan) { setHPlan(updatedPlan); @@ -87,18 +139,31 @@ export default function FieldPage() { const desktopView = () => { return ( - + - {map()} + + {map()} + + {weather()} + + {tasks()} + - + + {harvestPlans()} + + + + + + ) @@ -116,8 +181,10 @@ export default function FieldPage() { return ( + {fieldActions()} {isMobile ? mobileView() : desktopView()} + ) } \ No newline at end of file diff --git a/src/providers/pond/fieldAPI.tsx b/src/providers/pond/fieldAPI.tsx index 803be77..be980eb 100644 --- a/src/providers/pond/fieldAPI.tsx +++ b/src/providers/pond/fieldAPI.tsx @@ -9,7 +9,7 @@ import { permissionToString } from "pbHelpers/Permission"; export interface IFieldAPIContext { addField: (field: pond.FieldSettings, otherTeam?: string) => Promise; - getField: (fieldId: string, otherTeam?: string) => Promise; + getField: (fieldId: string, otherTeam?: string) => Promise>; listFields: ( limit: number, offset: number, @@ -53,8 +53,8 @@ export default function FieldProvider(props: PropsWithChildren) { const getField = (fieldId: string, otherTeam?: string) => { const view = otherTeam ? otherTeam : as - if (view) return get(pondURL("/field/" + fieldId + "?as=" + view)); - return get(pondURL("/field/" + fieldId)); + if (view) return get(pondURL("/field/" + fieldId + "?as=" + view)); + return get(pondURL("/field/" + fieldId)); }; const removeField = (key: string, otherTeam?: string) => { From 3448a24266e6fb575e83377323d257387a653ef5 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Thu, 4 Sep 2025 14:43:05 -0600 Subject: [PATCH 12/36] adding spaces to some places since libra cart is two words --- src/integrations/LibraCart/LibraCartAccess.tsx | 4 ++-- src/navigation/SideNavigator.tsx | 4 ++-- src/pages/LibraCart.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/integrations/LibraCart/LibraCartAccess.tsx b/src/integrations/LibraCart/LibraCartAccess.tsx index 5d31b66..1c97811 100644 --- a/src/integrations/LibraCart/LibraCartAccess.tsx +++ b/src/integrations/LibraCart/LibraCartAccess.tsx @@ -198,7 +198,7 @@ export default function LibraCartAccess() { - +