Merge branch 'field_dashboard' into staging_environment
This commit is contained in:
commit
943a6e4132
23 changed files with 1243 additions and 217 deletions
2
package-lock.json
generated
2
package-lock.json
generated
|
|
@ -42,7 +42,7 @@
|
||||||
"mui-tel-input": "^7.0.0",
|
"mui-tel-input": "^7.0.0",
|
||||||
"notistack": "^3.0.1",
|
"notistack": "^3.0.1",
|
||||||
"openweathermap-ts": "^1.2.10",
|
"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",
|
"query-string": "^9.2.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@
|
||||||
"mui-tel-input": "^7.0.0",
|
"mui-tel-input": "^7.0.0",
|
||||||
"notistack": "^3.0.1",
|
"notistack": "^3.0.1",
|
||||||
"openweathermap-ts": "^1.2.10",
|
"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",
|
"query-string": "^9.2.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ export default function BinsList(props: Props) {
|
||||||
xs: 6,
|
xs: 6,
|
||||||
sm: 4,
|
sm: 4,
|
||||||
md: 3,
|
md: 3,
|
||||||
lg: 2,
|
lg: 1.5,
|
||||||
}}
|
}}
|
||||||
key={i}
|
key={i}
|
||||||
className={classes.gridListTile}
|
className={classes.gridListTile}
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ export default function DisplayDrawer(props: Props) {
|
||||||
classes={{ paper: classes.drawerPaper }}>
|
classes={{ paper: classes.drawerPaper }}>
|
||||||
<Box
|
<Box
|
||||||
className={isMobile ? classes.drawerMobile : classes.drawerDesktop}
|
className={isMobile ? classes.drawerMobile : classes.drawerDesktop}
|
||||||
style={{ width: isMobile ? "100%" : width }}>
|
style={{ width: isMobile ? "100%" : width, height: isMobile ? drawerHeight + "vh" : 0}}>
|
||||||
<Box className={classes.header} style={{ width: isMobile ? "100%" : width }}>
|
<Box className={classes.header} style={{ width: isMobile ? "100%" : width }}>
|
||||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||||
<Box>
|
<Box>
|
||||||
|
|
|
||||||
|
|
@ -285,7 +285,6 @@ export default function ComponentCard(props: Props) {
|
||||||
// ) : (
|
// ) : (
|
||||||
<UnitMeasurementSummary
|
<UnitMeasurementSummary
|
||||||
component={component}
|
component={component}
|
||||||
excludedNodes={component.settings.excludedNodes}
|
|
||||||
reading={UnitMeasurement.convertLastMeasurement(measurements)}
|
reading={UnitMeasurement.convertLastMeasurement(measurements)}
|
||||||
//dense
|
//dense
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
import {
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionSummary,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
Grid2,
|
||||||
|
IconButton,
|
||||||
Paper,
|
Paper,
|
||||||
|
Stack,
|
||||||
Tab,
|
Tab,
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
|
|
@ -9,16 +15,26 @@ import {
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableRow,
|
TableRow,
|
||||||
Tabs
|
Tabs,
|
||||||
|
Typography
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
import { useMobile, useSnackbar } from "hooks";
|
import { useMobile, useSnackbar, useUserAPI } from "hooks";
|
||||||
import { useGlobalState, useFieldAPI } from "providers";
|
import { useGlobalState, useFieldAPI, useJohnDeereProxyAPI, useCNHiProxyAPI } from "providers";
|
||||||
//import HarvestTable from "harvestPlan/HarvestTable";
|
//import HarvestTable from "harvestPlan/HarvestTable";
|
||||||
import { Field } from "models";
|
import { Field, fieldScope, teamScope } from "models";
|
||||||
import GrainDescriber from "grain/GrainDescriber";
|
import GrainDescriber from "grain/GrainDescriber";
|
||||||
import { pond } from "protobuf-ts/pond";
|
import { pond } from "protobuf-ts/pond";
|
||||||
import HarvestSettings from "harvestPlan/HarvestSettings";
|
import HarvestSettings from "harvestPlan/HarvestSettings";
|
||||||
|
import ResponsiveTable, { Column } from "common/ResponsiveTable";
|
||||||
|
import FieldMinimap from "./Fieldminimap";
|
||||||
|
import FieldActions from "./FieldActions";
|
||||||
|
import FieldSettings from "./FieldSettings";
|
||||||
|
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 {
|
interface TabPanelProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
|
@ -26,128 +42,244 @@ interface TabPanelProps {
|
||||||
value: any;
|
value: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabPanelMine(props: TabPanelProps) {
|
|
||||||
const { children, value, index, ...other } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="tabpanel"
|
|
||||||
hidden={value !== index}
|
|
||||||
aria-labelledby={`simple-tab-${index}`}
|
|
||||||
style={{ height: "94%" }}
|
|
||||||
{...other}>
|
|
||||||
{value === index && <React.Fragment>{children}</React.Fragment>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function FieldList() {
|
export default function FieldList() {
|
||||||
const [{ as }] = useGlobalState();
|
const [{ as }] = useGlobalState();
|
||||||
const isMobile = useMobile();
|
const isMobile = useMobile();
|
||||||
const fieldAPI = useFieldAPI();
|
const fieldAPI = useFieldAPI();
|
||||||
|
const jdAPI = useJohnDeereProxyAPI();
|
||||||
|
const cnhAPI = useCNHiProxyAPI();
|
||||||
const { openSnack } = useSnackbar();
|
const { openSnack } = useSnackbar();
|
||||||
const [fields, setFields] = useState<Field[]>([]);
|
const [tabValue, setTabValue] = React.useState(0);
|
||||||
const [value, setValue] = React.useState(0);
|
|
||||||
const [openHarvestSettings, setOpenHarvestSettings] = useState(false);
|
const [openHarvestSettings, setOpenHarvestSettings] = useState(false);
|
||||||
const [fieldForPlan, setFieldForPlan] = useState(Field.create());
|
// const [fieldForPlan, setFieldForPlan] = useState(Field.create());
|
||||||
|
const [fields, setFields] = useState<Field[]>([]);
|
||||||
|
const [total, setTotal] = useState(0)
|
||||||
|
const [rowsPerPage, setRowsPerPage] = useState(5)
|
||||||
|
const [page, setPage] = useState(0)
|
||||||
|
const [selectedField, setSelectedField] = useState<Field>()
|
||||||
|
const [openFieldSettings, setOpenFieldSettings] = useState(false)
|
||||||
|
const [shareOpen, setShareOpen] = useState(false)
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
|
const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
|
||||||
setValue(newValue);
|
setTabValue(newValue);
|
||||||
|
setPage(0)
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadFields = useCallback(() => {
|
const loadAdaptiveFields = useCallback(() => {
|
||||||
fieldAPI
|
fieldAPI
|
||||||
.listFields(500, 0, "asc", "fieldName", undefined, as)
|
.listFields(rowsPerPage, page*rowsPerPage, "asc", "fieldName", undefined, as)
|
||||||
.then(resp => {
|
.then(resp => {
|
||||||
setFields(resp.data.fields.map(f => Field.any(f)));
|
setFields(resp.data.fields.map(f => Field.create(f)));
|
||||||
// resp.data.fields.forEach(f => {
|
setTotal(resp.data.total)
|
||||||
// if (f.settings) {
|
|
||||||
// fieldAPI.updateField(f.settings.key, f.settings);
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.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(() => {
|
useEffect(() => {
|
||||||
loadFields();
|
//based on the current tab load the correct fields
|
||||||
}, [loadFields]);
|
switch(tabValue){
|
||||||
|
case 0:
|
||||||
|
loadAdaptiveFields();
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
loadJDFields();
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
loadCNHFields();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}, [loadAdaptiveFields, loadJDFields, loadCNHFields, tabValue]);
|
||||||
|
|
||||||
const calcTotalAcres = () => {
|
const goTo = (field: Field) => {
|
||||||
let totalAcres = 0;
|
let path = field.key();
|
||||||
fields.forEach(field => {
|
navigate(path, { state: {field: field, permissions: field.permissions} });
|
||||||
totalAcres += field.acres();
|
}
|
||||||
});
|
|
||||||
totalAcres = Math.round(totalAcres);
|
|
||||||
return totalAcres;
|
|
||||||
};
|
|
||||||
|
|
||||||
const listFieldsInfo = fields.map((field, index) => (
|
const fieldActions = (field: Field) => {
|
||||||
<TableRow key={index}>
|
return (
|
||||||
<TableCell>{field.fieldName()}</TableCell>
|
<EventBlocker>
|
||||||
<TableCell>{field.landLoc()}</TableCell>
|
<IconButton
|
||||||
<TableCell>
|
|
||||||
{field.crop() === pond.Grain.GRAIN_CUSTOM
|
|
||||||
? field.customType()
|
|
||||||
: GrainDescriber(field.crop()).name}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{field.calculateAcres()}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{field.permissions.includes(pond.Permission.PERMISSION_WRITE) && (
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setFieldForPlan(field);
|
setSelectedField(field)
|
||||||
setOpenHarvestSettings(true);
|
setOpenFieldSettings(true)
|
||||||
}}>
|
}}><Settings /></IconButton>
|
||||||
New Crop Plan
|
<FieldActions field={field} permissions={field.permissions} refreshCallback={()=>{}}/>
|
||||||
</Button>
|
{isMobile && <IconButton onClick={() => {goTo(field)}}><ArrowForward /></IconButton>}
|
||||||
)}
|
</EventBlocker>
|
||||||
</TableCell>
|
)
|
||||||
</TableRow>
|
}
|
||||||
));
|
|
||||||
|
const columns: Column<Field>[] = [
|
||||||
|
{
|
||||||
|
title: "View",
|
||||||
|
render: row => {
|
||||||
|
return (
|
||||||
|
<Box margin={1} height={200} width={200}>
|
||||||
|
<FieldMinimap field={row} squared borderSpacing={0.001}/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Field Name",
|
||||||
|
render: row => {
|
||||||
|
return <Typography paddingLeft={2}>{row.name()}</Typography>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Current Crop",
|
||||||
|
render: row => {
|
||||||
|
return <Typography paddingLeft={2}>{GrainDescriber(row.crop()).name}</Typography>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Acres",
|
||||||
|
render: row => {
|
||||||
|
return <Typography paddingLeft={2}>{row.acres()}</Typography>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Actions",
|
||||||
|
hidden: tabValue > 0,
|
||||||
|
render: row => fieldActions(row)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const fieldTable = (fields: Field[]) => {
|
||||||
|
return (
|
||||||
|
<ResponsiveTable<Field>
|
||||||
|
columns={columns}
|
||||||
|
rows={fields}
|
||||||
|
onRowClick={(row) => {
|
||||||
|
!isMobile && goTo(row)
|
||||||
|
}}
|
||||||
|
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 (
|
||||||
|
<Accordion>
|
||||||
|
<AccordionSummary expandIcon={<ExpandMore />}>
|
||||||
|
<Grid2 alignItems="center" width="100%" container direction={"row"} justifyContent="space-between" wrap="nowrap">
|
||||||
|
<Grid2>
|
||||||
|
<Typography textAlign="center">
|
||||||
|
{row.name()}
|
||||||
|
</Typography>
|
||||||
|
</Grid2>
|
||||||
|
<Grid2>
|
||||||
|
{fieldActions(row)}
|
||||||
|
</Grid2>
|
||||||
|
</Grid2>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Grid2 alignItems="center" width="100%" container direction={"row"} justifyContent="space-between" wrap="nowrap">
|
||||||
|
<Grid2 size={6}>
|
||||||
|
<Box height={150} width="100%">
|
||||||
|
<FieldMinimap field={row} borderSpacing={0.001} squared />
|
||||||
|
</Box>
|
||||||
|
</Grid2>
|
||||||
|
<Grid2 size={6}>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<Typography textAlign="center">{row.grainName()}</Typography>
|
||||||
|
<Typography textAlign="center">{row.acres()} Acres</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Button variant="contained" color="primary">Go</Button>
|
||||||
|
</Grid2>
|
||||||
|
</Grid2>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
|
<Button
|
||||||
|
onClick={()=>{setShareOpen(true)}}
|
||||||
|
variant="contained"
|
||||||
|
color="primary">
|
||||||
|
Share All
|
||||||
|
</Button>
|
||||||
<Tabs
|
<Tabs
|
||||||
value={value}
|
value={tabValue}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
TabIndicatorProps={{ style: { background: "rgba(255,255,0,255)" } }}>
|
TabIndicatorProps={{ style: { background: "rgba(255,255,0,255)" } }}>
|
||||||
<Tab label="Field Overview" />
|
<Tab label="Adaptive" />
|
||||||
{/* {!isMobile && <Tab label="Current Harvest Plans" />}
|
<Tab label="John Deere" />
|
||||||
{!isMobile && <Tab label="Plan History" />} */}
|
<Tab label="Case New Holland" />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<TabPanelMine index={0} value={value}>
|
{fieldTable(fields)}
|
||||||
<TableContainer component={Paper}>
|
{/* <HarvestSettings
|
||||||
<Table style={{ minWidth: 1000 }}>
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>Field Name</TableCell>
|
|
||||||
<TableCell>Land Location</TableCell>
|
|
||||||
<TableCell>Main Crop Type</TableCell>
|
|
||||||
<TableCell>Acres ({calcTotalAcres()} Total)</TableCell>
|
|
||||||
<TableCell>Create New Plan</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>{listFieldsInfo}</TableBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
</TabPanelMine>
|
|
||||||
{/* need to re-factor the harvest table file before restoring these functions */}
|
|
||||||
{/* <TabPanelMine index={1} value={value}>
|
|
||||||
<HarvestTable fields={fields} display="current" />
|
|
||||||
</TabPanelMine>
|
|
||||||
<TabPanelMine index={2} value={value}>
|
|
||||||
<HarvestTable fields={fields} display="history" />
|
|
||||||
</TabPanelMine> */}
|
|
||||||
<HarvestSettings
|
|
||||||
open={openHarvestSettings}
|
open={openHarvestSettings}
|
||||||
close={() => setOpenHarvestSettings(false)}
|
close={() => setOpenHarvestSettings(false)}
|
||||||
field={fieldForPlan}
|
field={fieldForPlan}
|
||||||
|
/> */}
|
||||||
|
<FieldSettings
|
||||||
|
selectedField={selectedField}
|
||||||
|
removeField={() => {
|
||||||
|
//will need to re-load the fields after one is deleted
|
||||||
|
loadAdaptiveFields()
|
||||||
|
}}
|
||||||
|
open={openFieldSettings}
|
||||||
|
onClose={() => {
|
||||||
|
setOpenFieldSettings(false);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
<ShareAllFields open={shareOpen} close={() => {setShareOpen(false)}}/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -152,9 +152,7 @@ export default function FieldSettings(props: Props) {
|
||||||
|
|
||||||
const fieldInformation = () => {
|
const fieldInformation = () => {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<React.Fragment>
|
||||||
<Box>
|
|
||||||
<DialogTitle>Field Information</DialogTitle>
|
|
||||||
<TextField
|
<TextField
|
||||||
value={fieldName}
|
value={fieldName}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
|
|
@ -258,7 +256,14 @@ export default function FieldSettings(props: Props) {
|
||||||
Field Colour
|
Field Colour
|
||||||
<ColourPicker onChange={color => setFeatColor(color)} />
|
<ColourPicker onChange={color => setFeatColor(color)} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResponsiveDialog fullWidth open={props.open} onClose={onClose}>
|
||||||
|
<DialogTitle>Field Information</DialogTitle>
|
||||||
|
<DialogContent>{fieldInformation()}</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Grid container direction="row">
|
<Grid container direction="row">
|
||||||
<Grid>
|
<Grid>
|
||||||
|
|
@ -282,13 +287,6 @@ export default function FieldSettings(props: Props) {
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ResponsiveDialog fullWidth fullScreen={false} open={props.open} onClose={onClose}>
|
|
||||||
<DialogContent>{fieldInformation()}</DialogContent>
|
|
||||||
</ResponsiveDialog>
|
</ResponsiveDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
61
src/field/Fieldminimap.tsx
Normal file
61
src/field/Fieldminimap.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { CircularProgress } from "@mui/material";
|
||||||
|
import { Field } from "models";
|
||||||
|
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
|
||||||
|
squared?: boolean
|
||||||
|
borderSpacing?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FieldMinimap(props: Props) {
|
||||||
|
const { field, squared, borderSpacing } = props
|
||||||
|
const MAPBOX_TOKEN = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;
|
||||||
|
const [geoCollection, setGeoCollection] = useState<FeatureCollection>();
|
||||||
|
|
||||||
|
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 geoCollection ?
|
||||||
|
(
|
||||||
|
<Map
|
||||||
|
zoom={1}
|
||||||
|
maxBounds={
|
||||||
|
field.fieldBounds(borderSpacing, squared)
|
||||||
|
}
|
||||||
|
reuseMaps
|
||||||
|
style={{width: "100%", height: "100%"}}
|
||||||
|
mapboxAccessToken={MAPBOX_TOKEN}
|
||||||
|
mapStyle="mapbox://styles/mapbox/satellite-streets-v11"
|
||||||
|
>
|
||||||
|
<GeoMapLayer
|
||||||
|
objectCollection={geoCollection}
|
||||||
|
/>
|
||||||
|
</Map>
|
||||||
|
)
|
||||||
|
:
|
||||||
|
(<CircularProgress />)
|
||||||
|
}
|
||||||
201
src/field/ShareAllFields.tsx
Normal file
201
src/field/ShareAllFields.tsx
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
import { Box, Button, Checkbox, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, FormGroup, FormLabel, Grid2, Stack, Switch, TextField } from "@mui/material";
|
||||||
|
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
import { useFieldAPI, useSnackbar } from "providers";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import TeamSearch from "teams/TeamSearch";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean
|
||||||
|
close: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ShareAllFields(props: Props) {
|
||||||
|
const {open, close} = props
|
||||||
|
const [teamShare, setTeamShare] = useState(false)
|
||||||
|
const fieldAPI = useFieldAPI();
|
||||||
|
const { openSnack } = useSnackbar()
|
||||||
|
const [team, setTeam] = useState<string>("")
|
||||||
|
const [user, setUser] = useState<string>("")
|
||||||
|
const [sharedPermissions, setSharedPermissions] = useState<pond.Permission[]>([
|
||||||
|
pond.Permission.PERMISSION_READ
|
||||||
|
])
|
||||||
|
|
||||||
|
const share = () => {
|
||||||
|
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")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = () => {
|
||||||
|
// toggle for whether sharing to user or team
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Grid2 container alignItems="center">
|
||||||
|
<Grid2>User</Grid2>
|
||||||
|
<Grid2>
|
||||||
|
<Switch
|
||||||
|
color="default"
|
||||||
|
value={teamShare}
|
||||||
|
checked={teamShare}
|
||||||
|
onChange={(_, checked) => {
|
||||||
|
setTeamShare(checked)
|
||||||
|
}}
|
||||||
|
name="storage"
|
||||||
|
/>
|
||||||
|
</Grid2>
|
||||||
|
<Grid2>Team</Grid2>
|
||||||
|
</Grid2>
|
||||||
|
{teamShare ?
|
||||||
|
<Box>
|
||||||
|
<TeamSearch label="Team" setTeamCallback={(teamKey) => {setTeam(teamKey)}}/>
|
||||||
|
</Box> :
|
||||||
|
<Box>
|
||||||
|
<TextField
|
||||||
|
label="User Email"
|
||||||
|
fullWidth
|
||||||
|
value={user}
|
||||||
|
onChange={(e) => {
|
||||||
|
setUser(e.target.value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>}
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
// if sharing to user have a text field
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<React.Fragment>
|
||||||
|
<FormControl sx={{marginTop: 2}} component="fieldset" fullWidth variant="outlined">
|
||||||
|
<FormLabel component="legend">Permissions</FormLabel>
|
||||||
|
<FormGroup>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
disabled={sharedPermissions.includes(pond.Permission.PERMISSION_READ)}
|
||||||
|
checked={sharedPermissions.includes(pond.Permission.PERMISSION_READ)}
|
||||||
|
onChange={(_, checked) => {changePermissions(checked, pond.Permission.PERMISSION_READ)}}
|
||||||
|
value={pond.Permission.PERMISSION_READ as pond.Permission}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="View"
|
||||||
|
labelPlacement="end"
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={sharedPermissions.includes(pond.Permission.PERMISSION_WRITE)}
|
||||||
|
onChange={(_, checked) => {changePermissions(checked, pond.Permission.PERMISSION_WRITE)}}
|
||||||
|
value={pond.Permission.PERMISSION_WRITE as pond.Permission}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Edit"
|
||||||
|
labelPlacement="end"
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={sharedPermissions.includes(pond.Permission.PERMISSION_SHARE)}
|
||||||
|
onChange={(_, checked) => {changePermissions(checked, pond.Permission.PERMISSION_SHARE)}}
|
||||||
|
value={pond.Permission.PERMISSION_SHARE as pond.Permission}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Share"
|
||||||
|
labelPlacement="end"
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={sharedPermissions.includes(pond.Permission.PERMISSION_USERS)}
|
||||||
|
onChange={(_, checked) => {changePermissions(checked, pond.Permission.PERMISSION_USERS)}}
|
||||||
|
value={pond.Permission.PERMISSION_USERS as pond.Permission}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Users"
|
||||||
|
labelPlacement="end"
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={sharedPermissions.includes(pond.Permission.PERMISSION_FILE_MANAGEMENT)}
|
||||||
|
onChange={(_, checked) => {changePermissions(checked, pond.Permission.PERMISSION_FILE_MANAGEMENT)}}
|
||||||
|
value={pond.Permission.PERMISSION_FILE_MANAGEMENT as pond.Permission}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Files"
|
||||||
|
labelPlacement="end"
|
||||||
|
/>
|
||||||
|
{/* billing */}
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={sharedPermissions.includes(pond.Permission.PERMISSION_BILLING)}
|
||||||
|
onChange={(_, checked) => {changePermissions(checked, pond.Permission.PERMISSION_BILLING)}}
|
||||||
|
value={pond.Permission.PERMISSION_BILLING as pond.Permission}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Billing"
|
||||||
|
labelPlacement="end"
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</FormControl>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = () => {
|
||||||
|
//the buttons to shaare or cancel
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Button onClick={close}>Close</Button>
|
||||||
|
<Button onClick={share} variant="contained" color="primary">Share</Button>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResponsiveDialog open={open} onClose={close}>
|
||||||
|
<DialogTitle>Share All Fields</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Box minWidth={300}>
|
||||||
|
{target()}
|
||||||
|
{permissions()}
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
{actions()}
|
||||||
|
</DialogActions>
|
||||||
|
</ResponsiveDialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -7,16 +7,17 @@ import {
|
||||||
TextField
|
TextField
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import ResponsiveDialog from "common/ResponsiveDialog";
|
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||||
|
import { Option } from "common/SearchSelect";
|
||||||
import { Field, HarvestPlan, Task } from "models";
|
import { Field, HarvestPlan, Task } from "models";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { pond } from "protobuf-ts/pond";
|
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";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
close: () => void;
|
close: () => void;
|
||||||
fields: Field[];
|
fields?: Field[];
|
||||||
plan: HarvestPlan;
|
plan: HarvestPlan;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,10 +30,14 @@ export default function DuplicateHarvestPlan(props: Props) {
|
||||||
const harvestPlanAPI = useHarvestPlanAPI();
|
const harvestPlanAPI = useHarvestPlanAPI();
|
||||||
const taskAPI = useTaskAPI();
|
const taskAPI = useTaskAPI();
|
||||||
const [ready, setReady] = useState(false);
|
const [ready, setReady] = useState(false);
|
||||||
|
const [fieldOptions, setFieldOptions] = useState<Field[]>([])
|
||||||
|
const fieldAPI = useFieldAPI();
|
||||||
|
|
||||||
const loadTasks = useCallback(() => {
|
const loadTasks = useCallback(() => {
|
||||||
taskAPI.listTasks(50, 0, "asc", "start", plan.key(), undefined).then(resp => {
|
taskAPI.listTasks(50, 0, "asc", "start", plan.key(), undefined).then(resp => {
|
||||||
|
if(resp.data.tasks){
|
||||||
setPlanTasks(resp.data.tasks.map(t => Task.any(t)));
|
setPlanTasks(resp.data.tasks.map(t => Task.any(t)));
|
||||||
|
}
|
||||||
setReady(true);
|
setReady(true);
|
||||||
});
|
});
|
||||||
}, [taskAPI, plan]);
|
}, [taskAPI, plan]);
|
||||||
|
|
@ -41,9 +46,25 @@ export default function DuplicateHarvestPlan(props: Props) {
|
||||||
loadTasks();
|
loadTasks();
|
||||||
}, [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 = () => {
|
const duplicate = () => {
|
||||||
let planSettings = HarvestPlan.clone(plan).settings;
|
let planSettings = HarvestPlan.clone(plan).settings;
|
||||||
planSettings.field = fields[newFieldIndex].key();
|
planSettings.field = fieldOptions[newFieldIndex].key();
|
||||||
planSettings.createDate = moment.now();
|
planSettings.createDate = moment.now();
|
||||||
if (title !== "") {
|
if (title !== "") {
|
||||||
planSettings.title = title;
|
planSettings.title = title;
|
||||||
|
|
@ -69,7 +90,7 @@ export default function DuplicateHarvestPlan(props: Props) {
|
||||||
value={newFieldIndex}
|
value={newFieldIndex}
|
||||||
onChange={e => setNewFieldIndex(+e.target.value)}
|
onChange={e => setNewFieldIndex(+e.target.value)}
|
||||||
color="primary">
|
color="primary">
|
||||||
{fields.map((option, i) => (
|
{fieldOptions.map((option, i) => (
|
||||||
<MenuItem key={option.key()} value={i}>
|
<MenuItem key={option.key()} value={i}>
|
||||||
{option.fieldName()}
|
{option.fieldName()}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
@ -98,8 +119,8 @@ export default function DuplicateHarvestPlan(props: Props) {
|
||||||
color="primary"
|
color="primary"
|
||||||
disabled={
|
disabled={
|
||||||
!ready ||
|
!ready ||
|
||||||
(fields[newFieldIndex] &&
|
(fieldOptions[newFieldIndex] &&
|
||||||
!fields[newFieldIndex].permissions.includes(pond.Permission.PERMISSION_WRITE))
|
!fieldOptions[newFieldIndex].permissions.includes(pond.Permission.PERMISSION_WRITE))
|
||||||
}>
|
}>
|
||||||
Duplicate
|
Duplicate
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import ShareObjectIcon from "@mui/icons-material/Share";
|
||||||
import ObjectUsersIcon from "@mui/icons-material/AccountCircle";
|
import ObjectUsersIcon from "@mui/icons-material/AccountCircle";
|
||||||
import ObjectTeamsIcon from "@mui/icons-material/SupervisedUserCircle";
|
import ObjectTeamsIcon from "@mui/icons-material/SupervisedUserCircle";
|
||||||
import { pond } from "protobuf-ts/pond";
|
import { pond } from "protobuf-ts/pond";
|
||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import ObjectUsers from "user/ObjectUsers";
|
import ObjectUsers from "user/ObjectUsers";
|
||||||
import ShareObject from "user/ShareObject";
|
import ShareObject from "user/ShareObject";
|
||||||
import { isOffline } from "utils/environment";
|
import { isOffline } from "utils/environment";
|
||||||
|
|
@ -43,7 +43,7 @@ const useStyles = makeStyles((theme: Theme) => ({
|
||||||
interface Props {
|
interface Props {
|
||||||
plan: HarvestPlan;
|
plan: HarvestPlan;
|
||||||
planField: Field;
|
planField: Field;
|
||||||
fieldList: Field[];
|
fieldList?: Field[];
|
||||||
permissions: pond.Permission[];
|
permissions: pond.Permission[];
|
||||||
refreshCallback: (updatedPlan?: HarvestPlan) => void;
|
refreshCallback: (updatedPlan?: HarvestPlan) => void;
|
||||||
hideNew?: boolean;
|
hideNew?: boolean;
|
||||||
|
|
@ -215,7 +215,7 @@ export default function HarvestPlanActions(props: Props) {
|
||||||
setUpdatePlan(true);
|
setUpdatePlan(true);
|
||||||
setOpenState({ ...openState, settings: true });
|
setOpenState({ ...openState, settings: true });
|
||||||
}}>
|
}}>
|
||||||
<Edit /> Edit or Add Activity
|
<Edit /> Activities
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import {
|
||||||
import { Notes } from "@mui/icons-material";
|
import { Notes } from "@mui/icons-material";
|
||||||
import GrainDescriber from "grain/GrainDescriber";
|
import GrainDescriber from "grain/GrainDescriber";
|
||||||
import { Field, HarvestPlan } from "models";
|
import { Field, HarvestPlan } from "models";
|
||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { getThemeType } from "theme";
|
import { getThemeType } from "theme";
|
||||||
import Chat from "chat/Chat";
|
import Chat from "chat/Chat";
|
||||||
import { pond } from "protobuf-ts/pond";
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
|
@ -23,7 +23,7 @@ interface Props {
|
||||||
permissions: pond.Permission[];
|
permissions: pond.Permission[];
|
||||||
planField: Field;
|
planField: Field;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
fieldList: Field[];
|
fieldList?: Field[];
|
||||||
changePlan: (updatedPlan?: HarvestPlan) => void;
|
changePlan: (updatedPlan?: HarvestPlan) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -175,7 +175,7 @@ export default function HarvestPlanDisplay(props: Props) {
|
||||||
<Typography
|
<Typography
|
||||||
variant="h5"
|
variant="h5"
|
||||||
style={{ fontWeight: 700, marginTop: "auto", marginBottom: "auto" }}>
|
style={{ fontWeight: 700, marginTop: "auto", marginBottom: "auto" }}>
|
||||||
Crop Plan{plan.key() !== "" ? " - " + plan.settings.title : " - None Found"}
|
{plan.key() !== "" ? plan.settings.title : "None Found"}
|
||||||
</Typography>
|
</Typography>
|
||||||
{!isMobile && permissions.includes(pond.Permission.PERMISSION_WRITE) && (
|
{!isMobile && permissions.includes(pond.Permission.PERMISSION_WRITE) && (
|
||||||
<HarvestPlanActions
|
<HarvestPlanActions
|
||||||
|
|
|
||||||
202
src/harvestPlan/HarvestPlanTable.tsx
Normal file
202
src/harvestPlan/HarvestPlanTable.tsx
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
import { ExpandMore } from "@mui/icons-material"
|
||||||
|
import { Accordion, AccordionDetails, AccordionSummary, Box, Theme, Typography } from "@mui/material"
|
||||||
|
import { makeStyles } from "@mui/styles"
|
||||||
|
import ResponsiveTable, { Column } from "common/ResponsiveTable"
|
||||||
|
import GrainDescriber from "grain/GrainDescriber"
|
||||||
|
import { cloneDeep } from "lodash"
|
||||||
|
import { Field, HarvestPlan } from "models"
|
||||||
|
import { useHarvestPlanAPI } from "providers"
|
||||||
|
import { useCallback, useEffect, useState } from "react"
|
||||||
|
import { getThemeType } from "theme"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
field: Field
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
|
dark: {
|
||||||
|
backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(50, 50, 50)",
|
||||||
|
padding: 5
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
backgroundColor: getThemeType() === "light" ? "rgb(235, 235, 235)" : "rgb(60, 60, 60)",
|
||||||
|
padding: 5
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function HarvestPlanTable(props: Props){
|
||||||
|
const {field} = props
|
||||||
|
const harvestAPI = useHarvestPlanAPI()
|
||||||
|
const [page, setPage] = useState(0)
|
||||||
|
const [pageSize, setPageSize] = useState(10)
|
||||||
|
const [loadedPlans, setLoadedPlans] = useState<HarvestPlan[]>([])
|
||||||
|
const [total, setTotal] = useState(0)
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
harvestAPI.listHarvestPlans(pageSize, page*pageSize, "desc", "harvestYear", field.key()).then(resp => {
|
||||||
|
if(resp.data.harvestPlan){
|
||||||
|
setLoadedPlans(resp.data.harvestPlan.map(hp => HarvestPlan.create(hp)))
|
||||||
|
}
|
||||||
|
setTotal(resp.data.total)
|
||||||
|
})
|
||||||
|
},[page, pageSize, field])
|
||||||
|
|
||||||
|
const columns: Column<HarvestPlan>[] = [
|
||||||
|
//these columns would be the ones on by default
|
||||||
|
//field - hideable
|
||||||
|
{
|
||||||
|
title: "Field",
|
||||||
|
hidden: field !== undefined,
|
||||||
|
render: (row) => <Typography align="center">{row.field()}</Typography>
|
||||||
|
},
|
||||||
|
//title
|
||||||
|
{
|
||||||
|
title: "Plan",
|
||||||
|
render: (row) => <Typography align="center">{row.settings.title}</Typography>
|
||||||
|
},
|
||||||
|
//year
|
||||||
|
{
|
||||||
|
title: "Year",
|
||||||
|
render: (row) => <Typography align="center">{row.harvestYear()}</Typography>
|
||||||
|
},
|
||||||
|
//grain - crop_type
|
||||||
|
{
|
||||||
|
title: "Grain",
|
||||||
|
render: (row) => <Typography align="center">{GrainDescriber(row.cropType()).name}</Typography>
|
||||||
|
},
|
||||||
|
//variant - grain_type
|
||||||
|
{
|
||||||
|
title: "Variant",
|
||||||
|
render: (row) => <Typography align="center">{row.grainType()}</Typography>
|
||||||
|
},
|
||||||
|
//yield target
|
||||||
|
{
|
||||||
|
title: "Yield Target (bu)",
|
||||||
|
render: (row) => <Typography align="right">{row.yieldTarget()}</Typography>
|
||||||
|
},
|
||||||
|
//actual yield
|
||||||
|
{
|
||||||
|
title: "Actual Yield (bu)",
|
||||||
|
render: (row) => <Typography align="right">{row.actualYield()}</Typography>
|
||||||
|
},
|
||||||
|
//bushel price
|
||||||
|
{
|
||||||
|
title: "Bushel Price",
|
||||||
|
render: (row) => <Typography align="right">{row.bushelPrice()}</Typography>
|
||||||
|
},
|
||||||
|
//material cost
|
||||||
|
{
|
||||||
|
title: "Material Cost",
|
||||||
|
render: (row) => <Typography align="right">{row.totalMaterialCost()}</Typography>
|
||||||
|
},
|
||||||
|
//equipment cost
|
||||||
|
{
|
||||||
|
title: "Equipment Cost",
|
||||||
|
render: (row) => <Typography align="right">{row.totalEquipmentCost()}</Typography>
|
||||||
|
}
|
||||||
|
//these columns could be optional (disabled by default but turned on later?)
|
||||||
|
//pre-seed materials
|
||||||
|
//pre-seed equpment
|
||||||
|
//seed materials
|
||||||
|
//seed equpment
|
||||||
|
//post-seed materials
|
||||||
|
//post-seed equpment
|
||||||
|
//harvest materials
|
||||||
|
//harvest equpment
|
||||||
|
//fall materials
|
||||||
|
//fall equpment
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResponsiveTable<HarvestPlan>
|
||||||
|
columns={columns}
|
||||||
|
title={"Harvest Plans"}
|
||||||
|
resizeable
|
||||||
|
page={page}
|
||||||
|
pageSize={pageSize}
|
||||||
|
rows={loadedPlans}
|
||||||
|
total={total}
|
||||||
|
handleRowsPerPageChange={(e)=>{
|
||||||
|
setPageSize(e.target.value)
|
||||||
|
}}
|
||||||
|
setPage={(page)=>{
|
||||||
|
setPage(page)
|
||||||
|
}}
|
||||||
|
loadMore={() => {
|
||||||
|
harvestAPI.listHarvestPlans(pageSize, loadedPlans.length, "desc", "harvestYear", field.key()).then(resp => {
|
||||||
|
let currentPlans = cloneDeep(loadedPlans)
|
||||||
|
let newPlans: HarvestPlan[] = []
|
||||||
|
if(resp.data.harvestPlan){
|
||||||
|
newPlans = resp.data.harvestPlan.map(hp => HarvestPlan.create(hp))
|
||||||
|
}
|
||||||
|
setLoadedPlans(currentPlans.concat(newPlans))
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
renderMobile={(row, index) => {
|
||||||
|
// since these are more about previous harvest plans i am only showing the final tallys rather than the breakdowns
|
||||||
|
return (
|
||||||
|
<Accordion>
|
||||||
|
<AccordionSummary expandIcon={<ExpandMore />}>
|
||||||
|
<Typography sx={{fontWeight: 650}}>
|
||||||
|
{row.settings.title} - {row.harvestYear()}
|
||||||
|
</Typography>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
{/* grain */}
|
||||||
|
<Box className={classes.light} style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<Typography variant="subtitle1">Grain:</Typography>
|
||||||
|
<Typography variant="subtitle1">{GrainDescriber(row.cropType()).name}</Typography>
|
||||||
|
</Box>
|
||||||
|
{/* variant */}
|
||||||
|
<Box className={classes.dark} style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<Typography variant="subtitle1">Variant:</Typography>
|
||||||
|
<Typography variant="subtitle1">{row.grainType()}</Typography>
|
||||||
|
</Box>
|
||||||
|
{/* yield target */}
|
||||||
|
{/* <Box className={classes.dark} style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<Typography variant="subtitle1">Yield Target:</Typography>
|
||||||
|
<Typography variant="subtitle1">{row.yieldTarget()}bu</Typography>
|
||||||
|
</Box> */}
|
||||||
|
{/* actual yield */}
|
||||||
|
<Box className={classes.light} style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<Typography variant="subtitle1">Actual Yield:</Typography>
|
||||||
|
<Typography variant="subtitle1">{row.actualYield()}bu</Typography>
|
||||||
|
</Box>
|
||||||
|
{/* bushel price */}
|
||||||
|
<Box className={classes.dark} style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<Typography variant="subtitle1">Bushel Price:</Typography>
|
||||||
|
<Typography variant="subtitle1">${row.bushelPrice()}</Typography>
|
||||||
|
</Box>
|
||||||
|
{/* material cost */}
|
||||||
|
<Box className={classes.light} style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<Typography variant="subtitle1">Material Cost:</Typography>
|
||||||
|
<Typography variant="subtitle1">${row.totalMaterialCost() * field.acres()}</Typography>
|
||||||
|
</Box>
|
||||||
|
{/* equipment cost */}
|
||||||
|
<Box className={classes.dark} style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<Typography variant="subtitle1">Equipment Cost:</Typography>
|
||||||
|
<Typography variant="subtitle1">${row.totalEquipmentCost() * field.acres()}</Typography>
|
||||||
|
</Box>
|
||||||
|
{/* pre-seed materials */}
|
||||||
|
{/* pre-seed equpment */}
|
||||||
|
{/* seed materials */}
|
||||||
|
{/* seed equpment */}
|
||||||
|
{/* post-seed materials */}
|
||||||
|
{/* post-seed equpment */}
|
||||||
|
{/* harvest materials */}
|
||||||
|
{/* harvest equpment */}
|
||||||
|
{/* fall materials */}
|
||||||
|
{/* fall equpment */}
|
||||||
|
{/* total cost */}
|
||||||
|
<Box className={classes.light} style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<Typography variant="subtitle1">Total Cost:</Typography>
|
||||||
|
<Typography variant="subtitle1">${(row.totalEquipmentCost() + row.totalMaterialCost()) * field.acres()}</Typography>
|
||||||
|
</Box>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -273,7 +273,7 @@ export default function FieldDrawer(props: Props) {
|
||||||
<Weather longitude={field.center().longitude} latitude={field.center().latitude} />
|
<Weather longitude={field.center().longitude} latitude={field.center().latitude} />
|
||||||
</TabPanelMine>
|
</TabPanelMine>
|
||||||
<TabPanelMine value={value} index={2}>
|
<TabPanelMine value={value} index={2}>
|
||||||
<TaskViewer drawerView objectKey={field.key()} loadKeys={taskLoadKeys} />
|
<TaskViewer drawerView objectKey={field.key()} loadKeys={taskLoadKeys} overlayButton />
|
||||||
</TabPanelMine>
|
</TabPanelMine>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ interface Props {
|
||||||
objectCollection: FeatureCollection;
|
objectCollection: FeatureCollection;
|
||||||
measurementCollection?: FeatureCollection;
|
measurementCollection?: FeatureCollection;
|
||||||
showTitle?: boolean;
|
showTitle?: boolean;
|
||||||
setInteractiveLayers: (layers: string[]) => void;
|
setInteractiveLayers?: (layers: string[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function GeoMapLayer(props: Props) {
|
export default function GeoMapLayer(props: Props) {
|
||||||
|
|
@ -18,7 +18,9 @@ export default function GeoMapLayer(props: Props) {
|
||||||
// }, [measurementCollection, objectCollection]);
|
// }, [measurementCollection, objectCollection]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if(setInteractiveLayers){
|
||||||
setInteractiveLayers(["shapefill", "shapeborder", "measurementBorder"]);
|
setInteractiveLayers(["shapefill", "shapeborder", "measurementBorder"]);
|
||||||
|
}
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -60,6 +62,7 @@ export default function GeoMapLayer(props: Props) {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Source>
|
</Source>
|
||||||
|
{measurementCollection &&
|
||||||
<Source type="geojson" data={measurementCollection} id={"measurements"}>
|
<Source type="geojson" data={measurementCollection} id={"measurements"}>
|
||||||
<Layer
|
<Layer
|
||||||
id="measurementBorder"
|
id="measurementBorder"
|
||||||
|
|
@ -90,6 +93,7 @@ export default function GeoMapLayer(props: Props) {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Source>
|
</Source>
|
||||||
|
}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import area from "@turf/area";
|
||||||
import { Feature } from "geojson";
|
import { Feature } from "geojson";
|
||||||
import { GeometryMapping } from "./GeometryMapping";
|
import { GeometryMapping } from "./GeometryMapping";
|
||||||
import GrainDescriber from "grain/GrainDescriber";
|
import GrainDescriber from "grain/GrainDescriber";
|
||||||
|
import { LngLat, LngLatBounds } from "mapbox-gl";
|
||||||
|
|
||||||
export class Field {
|
export class Field {
|
||||||
public settings: pond.FieldSettings = pond.FieldSettings.create();
|
public settings: pond.FieldSettings = pond.FieldSettings.create();
|
||||||
|
|
@ -14,9 +15,11 @@ export class Field {
|
||||||
public static create(pf?: pond.Field): Field {
|
public static create(pf?: pond.Field): Field {
|
||||||
let my = new Field();
|
let my = new Field();
|
||||||
if (pf) {
|
if (pf) {
|
||||||
my.settings = pond.FieldSettings.fromObject(cloneDeep(or(pf.settings, {})));
|
//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
|
||||||
my.status = pond.FieldStatus.fromObject(cloneDeep(or(pf.status, {})));
|
let field = pond.Field.fromObject(cloneDeep(or(pf, pond.Field.create())))
|
||||||
my.permissions = pf.fieldPermissions;
|
my.settings = field.settings ? field.settings : pond.FieldSettings.create()
|
||||||
|
my.status = field.status ? field.status : pond.FieldStatus.create()
|
||||||
|
my.permissions = field.fieldPermissions
|
||||||
}
|
}
|
||||||
return my;
|
return my;
|
||||||
}
|
}
|
||||||
|
|
@ -152,6 +155,60 @@ export class Field {
|
||||||
return coords;
|
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 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 {
|
||||||
|
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 (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
|
||||||
|
let longDiff = maxLong - minLong
|
||||||
|
let latDiff = maxLat - minLat
|
||||||
|
//find which is narrower
|
||||||
|
if(longDiff > 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
|
||||||
|
minLat = minLat - spacing
|
||||||
|
maxLong = maxLong + spacing
|
||||||
|
maxLat = maxLat + spacing
|
||||||
|
}
|
||||||
|
return new LngLatBounds(
|
||||||
|
new LngLat(minLong, minLat),
|
||||||
|
new LngLat(maxLong, maxLat)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public grainName(): string {
|
public grainName(): string {
|
||||||
if (this.grain() !== pond.Grain.GRAIN_INVALID) {
|
if (this.grain() !== pond.Grain.GRAIN_INVALID) {
|
||||||
if (this.grain() === pond.Grain.GRAIN_CUSTOM) {
|
if (this.grain() === pond.Grain.GRAIN_CUSTOM) {
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ const Contract = lazy(() => import("pages/Contract"));
|
||||||
const JohnDeere = lazy(() => import("pages/JohnDeere"));
|
const JohnDeere = lazy(() => import("pages/JohnDeere"));
|
||||||
const CNHi = lazy(() => import("pages/CNHi"));
|
const CNHi = lazy(() => import("pages/CNHi"));
|
||||||
const LibraCart = lazy(() => import("pages/LibraCart"));
|
const LibraCart = lazy(() => import("pages/LibraCart"));
|
||||||
|
const FieldPage = lazy(()=>import("pages/Field"));
|
||||||
|
|
||||||
export const appendToUrl = (appendage: number | string) => {
|
export const appendToUrl = (appendage: number | string) => {
|
||||||
const basePath = location.pathname.replace(/\/$/, "");
|
const basePath = location.pathname.replace(/\/$/, "");
|
||||||
|
|
@ -70,6 +71,7 @@ export default function Router() {
|
||||||
<Route path="terminals/*" element={<TerminalGatesRoute />} />
|
<Route path="terminals/*" element={<TerminalGatesRoute />} />
|
||||||
<Route path="jobsites/*" element={<JobsitesRoute />} />
|
<Route path="jobsites/*" element={<JobsitesRoute />} />
|
||||||
<Route path="heaters/*" element={<ObjectHeatersRoute />} />
|
<Route path="heaters/*" element={<ObjectHeatersRoute />} />
|
||||||
|
<Route path="fields/*" element={<FieldsRoute />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -284,6 +286,21 @@ export default function Router() {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const FieldsRoute = () => {
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path=""
|
||||||
|
element={<Fields />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/:fieldID"
|
||||||
|
element={<FieldPage />}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isLoading) return null;
|
if (isLoading) return null;
|
||||||
// if (!isAuthenticated) {
|
// if (!isAuthenticated) {
|
||||||
// loginWithRedirect()
|
// loginWithRedirect()
|
||||||
|
|
@ -325,7 +342,6 @@ export default function Router() {
|
||||||
{user.hasFeature("admin") &&
|
{user.hasFeature("admin") &&
|
||||||
<Route path="logs" element={<Logs />} />
|
<Route path="logs" element={<Logs />} />
|
||||||
}
|
}
|
||||||
<Route path="fields" element={<Fields />} />
|
|
||||||
<Route path="marketplace" element={<Marketplace />} />
|
<Route path="marketplace" element={<Marketplace />} />
|
||||||
{user.hasFeature("admin") && (
|
{user.hasFeature("admin") && (
|
||||||
<Route path="firmware" element={<Firmware />} />
|
<Route path="firmware" element={<Firmware />} />
|
||||||
|
|
|
||||||
314
src/pages/Field.tsx
Normal file
314
src/pages/Field.tsx
Normal file
|
|
@ -0,0 +1,314 @@
|
||||||
|
import { Field, fieldScope, HarvestPlan, teamScope } from "models";
|
||||||
|
import { useFieldAPI, useGlobalState, useHarvestPlanAPI } from "providers";
|
||||||
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
|
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||||
|
import PageContainer from "./PageContainer";
|
||||||
|
import { useMobile, useSnackbar, useUserAPI } from "hooks";
|
||||||
|
import { Box, Card, Grid2, IconButton, Tab, Tabs, Theme, Typography, darken } 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";
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
|
import HarvestPlanTable from "harvestPlan/HarvestPlanTable";
|
||||||
|
import { makeStyles } from "@mui/styles";
|
||||||
|
import FieldSettings from "field/FieldSettings";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => {
|
||||||
|
return ({
|
||||||
|
card: {
|
||||||
|
height: "100%",
|
||||||
|
bgcolor: darken(theme.palette.background.paper, 0.05),
|
||||||
|
},
|
||||||
|
sectionHeader: {
|
||||||
|
fontSize: 35,
|
||||||
|
fontWeight: 650
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
interface TabPanelProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
index: any;
|
||||||
|
value: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabPanel(props: TabPanelProps) {
|
||||||
|
const { children, value, index, ...other } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="tabpanel"
|
||||||
|
hidden={value !== index}
|
||||||
|
aria-labelledby={`simple-tab-${index}`}
|
||||||
|
style={{ height: "94%", paddingTop: "15px" }}
|
||||||
|
{...other}>
|
||||||
|
{value === index && <React.Fragment>{children}</React.Fragment>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FieldPage() {
|
||||||
|
const { state } = useLocation();
|
||||||
|
//const [{ as }] = useGlobalState()
|
||||||
|
const classes = useStyles();
|
||||||
|
const fieldAPI = useFieldAPI();
|
||||||
|
const userAPI = useUserAPI();
|
||||||
|
const [{as, user}] = useGlobalState();
|
||||||
|
const {openSnack} = useSnackbar();
|
||||||
|
const fieldID = useParams<{ fieldID: string }>()?.fieldID ?? "";
|
||||||
|
const [field, setField] = useState<Field>(state?.field ? Field.create(state.field) : Field.create())
|
||||||
|
const [permissions, setPermissions] = useState<pond.Permission[]>(state?.permissions ? state.permissions : [])
|
||||||
|
const isMobile = useMobile()
|
||||||
|
const [openFieldSettings, setOpenFieldSettings] = useState(false)
|
||||||
|
const [planLoading, setPlanLoading] = useState(false)
|
||||||
|
const [hPlan, setHPlan] = useState<HarvestPlan>(HarvestPlan.create());
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const hPlanAPI = useHarvestPlanAPI();
|
||||||
|
const [tabVal, setTabVal] = useState(0)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const loadField = useCallback(() => {
|
||||||
|
if(state.field){
|
||||||
|
fieldAPI.getField(state.field).then(resp => {
|
||||||
|
setField(Field.create(resp.data))
|
||||||
|
}).catch(err => {
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},[as])
|
||||||
|
|
||||||
|
const goTo = () => {
|
||||||
|
navigate("/fields");
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
//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 (
|
||||||
|
<Grid2 container direction={"row"} justifyContent="space-between" alignContent="center" alignItems="center">
|
||||||
|
<Grid2>
|
||||||
|
<Typography className={classes.sectionHeader}>
|
||||||
|
{field.name()}
|
||||||
|
</Typography>
|
||||||
|
</Grid2>
|
||||||
|
<Grid2>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
setOpenFieldSettings(true)
|
||||||
|
}}><Settings /></IconButton>
|
||||||
|
<FieldActions field={field} permissions={permissions} refreshCallback={()=>{}}/>
|
||||||
|
</Grid2>
|
||||||
|
</Grid2>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const map = () => {
|
||||||
|
return (
|
||||||
|
<Box minHeight={550} height="100%">
|
||||||
|
<FieldMinimap field={field} borderSpacing={0.001} squared/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const weather = () => {
|
||||||
|
return (<Weather longitude={field.center().longitude} latitude={field.center().latitude} />)
|
||||||
|
}
|
||||||
|
|
||||||
|
const harvestPlans = () => {
|
||||||
|
return (
|
||||||
|
<HarvestPlanDisplay
|
||||||
|
plan={hPlan}
|
||||||
|
permissions={permissions}
|
||||||
|
planField={field}
|
||||||
|
loading={planLoading}
|
||||||
|
changePlan={updatedPlan => {
|
||||||
|
if (updatedPlan) {
|
||||||
|
setHPlan(updatedPlan);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tasks = () => {
|
||||||
|
let taskLoadKeys: string[] = [];
|
||||||
|
if (!planLoading) {
|
||||||
|
field.key() !== "" && taskLoadKeys.push(field.key());
|
||||||
|
//hPlan.key() !== "" && taskLoadKeys.push(hPlan.key());
|
||||||
|
}
|
||||||
|
return (<TaskViewer drawerView objectKey={field.key()} loadKeys={taskLoadKeys} overlayButton={isMobile}/>)
|
||||||
|
}
|
||||||
|
|
||||||
|
const desktopView = () => {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Box>
|
||||||
|
<Grid2 container spacing={1}>
|
||||||
|
<Grid2 size={4}>
|
||||||
|
<Card raised className={classes.card}>
|
||||||
|
{map()}
|
||||||
|
</Card>
|
||||||
|
</Grid2>
|
||||||
|
<Grid2 size={3}>
|
||||||
|
<Card raised className={classes.card}>
|
||||||
|
{weather()}
|
||||||
|
</Card>
|
||||||
|
</Grid2>
|
||||||
|
<Grid2 size={5}>
|
||||||
|
<Card raised className={classes.card}>
|
||||||
|
{tasks()}
|
||||||
|
</Card>
|
||||||
|
</Grid2>
|
||||||
|
</Grid2>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography className={classes.sectionHeader}>
|
||||||
|
Harvest Overview
|
||||||
|
</Typography>
|
||||||
|
<Grid2 container spacing={1}>
|
||||||
|
<Grid2 size={4}>
|
||||||
|
<Card raised className={classes.card}>
|
||||||
|
{harvestPlans()}
|
||||||
|
</Card>
|
||||||
|
</Grid2>
|
||||||
|
<Grid2 size={8}>
|
||||||
|
<Card raised>
|
||||||
|
<HarvestPlanTable field={field}/>
|
||||||
|
</Card>
|
||||||
|
</Grid2>
|
||||||
|
</Grid2>
|
||||||
|
</Box>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mobileView = () => {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Tabs
|
||||||
|
value={tabVal}
|
||||||
|
onChange={(_, val) => {
|
||||||
|
setTabVal(val)
|
||||||
|
}}
|
||||||
|
TabIndicatorProps={{ style: { background: "rgba(255,255,0,255)" } }}>
|
||||||
|
<Tab label="Overview" />
|
||||||
|
<Tab label="Activities" />
|
||||||
|
<Tab label="Harvest" />
|
||||||
|
</Tabs>
|
||||||
|
<TabPanel value={tabVal} index={0}>
|
||||||
|
<Grid2 container spacing={2} direction="column">
|
||||||
|
<Grid2 size={12}>
|
||||||
|
<Card raised sx={{height: "50vh"}}>
|
||||||
|
{map()}
|
||||||
|
</Card>
|
||||||
|
</Grid2>
|
||||||
|
<Grid2 size={12}>
|
||||||
|
<Card raised>
|
||||||
|
{weather()}
|
||||||
|
</Card>
|
||||||
|
</Grid2>
|
||||||
|
</Grid2>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={tabVal} index={1}>
|
||||||
|
<Card raised>
|
||||||
|
{tasks()}
|
||||||
|
</Card>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={tabVal} index={2}>
|
||||||
|
<Grid2>
|
||||||
|
<Grid2 size={12}>
|
||||||
|
<Card raised className={classes.card}>
|
||||||
|
{harvestPlans()}
|
||||||
|
</Card>
|
||||||
|
</Grid2>
|
||||||
|
<Grid2>
|
||||||
|
<HarvestPlanTable field={field}/>
|
||||||
|
</Grid2>
|
||||||
|
</Grid2>
|
||||||
|
</TabPanel>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldSettings = () => {
|
||||||
|
return (
|
||||||
|
<FieldSettings
|
||||||
|
open={openFieldSettings}
|
||||||
|
onClose={() => {
|
||||||
|
setOpenFieldSettings(false)
|
||||||
|
}}
|
||||||
|
selectedField={field}
|
||||||
|
updateFields={() => {
|
||||||
|
loadField()
|
||||||
|
}}
|
||||||
|
removeField={()=>{
|
||||||
|
goTo()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<Box padding={2}>
|
||||||
|
{fieldActions()}
|
||||||
|
{isMobile ? mobileView() : desktopView()}
|
||||||
|
</Box>
|
||||||
|
{fieldSettings()}
|
||||||
|
</PageContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -39,8 +39,8 @@ export default function Heaters() {
|
||||||
objectHeaterAPI
|
objectHeaterAPI
|
||||||
.listObjectHeatersPageData(pageSize, tablePage * pageSize, undefined, undefined, searchText, as)
|
.listObjectHeatersPageData(pageSize, tablePage * pageSize, undefined, undefined, searchText, as)
|
||||||
.then(resp => {
|
.then(resp => {
|
||||||
|
setHeaters(resp.data.heaterData ?? []);
|
||||||
setTotal(resp.data.total)
|
setTotal(resp.data.total)
|
||||||
setHeaters(resp.data.heaterData);
|
|
||||||
});
|
});
|
||||||
}, [objectHeaterAPI, as, pageSize, tablePage, searchText]);
|
}, [objectHeaterAPI, as, pageSize, tablePage, searchText]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ export default function Tasks() {
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<Box padding={2}>
|
<Box padding={2}>
|
||||||
<TaskViewer />
|
<TaskViewer overlayButton/>
|
||||||
</Box>
|
</Box>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,11 @@ import { useGlobalState } from "providers";
|
||||||
import React, { createContext, PropsWithChildren, useContext } from "react";
|
import React, { createContext, PropsWithChildren, useContext } from "react";
|
||||||
import { or } from "utils";
|
import { or } from "utils";
|
||||||
import { pondURL } from "./pond";
|
import { pondURL } from "./pond";
|
||||||
|
import { permissionToString } from "pbHelpers/Permission";
|
||||||
|
|
||||||
export interface IFieldAPIContext {
|
export interface IFieldAPIContext {
|
||||||
addField: (field: pond.FieldSettings, otherTeam?: string) => Promise<any>;
|
addField: (field: pond.FieldSettings, otherTeam?: string) => Promise<any>;
|
||||||
getField: (fieldId: string, otherTeam?: string) => Promise<any>;
|
getField: (fieldId: string, otherTeam?: string) => Promise<AxiosResponse<pond.Field>>;
|
||||||
listFields: (
|
listFields: (
|
||||||
limit: number,
|
limit: number,
|
||||||
offset: number,
|
offset: number,
|
||||||
|
|
@ -24,7 +25,15 @@ export interface IFieldAPIContext {
|
||||||
field: pond.FieldSettings,
|
field: pond.FieldSettings,
|
||||||
asRoot?: true,
|
asRoot?: true,
|
||||||
otherTeam?: string
|
otherTeam?: string
|
||||||
) => Promise<AxiosResponse<pond.UpdateSiteResponse>>;
|
) => Promise<AxiosResponse<pond.UpdateFieldResponse>>;
|
||||||
|
shareAll: (
|
||||||
|
email: string,
|
||||||
|
permissions: pond.Permission[],
|
||||||
|
) => Promise<AxiosResponse<any>>
|
||||||
|
shareAllByKey: (
|
||||||
|
key: string,
|
||||||
|
permissions: pond.Permission[]
|
||||||
|
) => Promise<AxiosResponse<any>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FieldAPIContext = createContext<IFieldAPIContext>({} as IFieldAPIContext);
|
export const FieldAPIContext = createContext<IFieldAPIContext>({} as IFieldAPIContext);
|
||||||
|
|
@ -44,8 +53,8 @@ export default function FieldProvider(props: PropsWithChildren<Props>) {
|
||||||
|
|
||||||
const getField = (fieldId: string, otherTeam?: string) => {
|
const getField = (fieldId: string, otherTeam?: string) => {
|
||||||
const view = otherTeam ? otherTeam : as
|
const view = otherTeam ? otherTeam : as
|
||||||
if (view) return get(pondURL("/field/" + fieldId + "?as=" + view));
|
if (view) return get<pond.Field>(pondURL("/field/" + fieldId + "?as=" + view));
|
||||||
return get(pondURL("/field/" + fieldId));
|
return get<pond.Field>(pondURL("/field/" + fieldId));
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeField = (key: string, otherTeam?: string) => {
|
const removeField = (key: string, otherTeam?: string) => {
|
||||||
|
|
@ -93,6 +102,36 @@ export default function FieldProvider(props: PropsWithChildren<Props>) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const shareAll = (email: string, permissions: pond.Permission[]) => {
|
||||||
|
let url = pondURL("/shareAllFields")
|
||||||
|
if (as) url = url + "?as=" + as
|
||||||
|
return new Promise<AxiosResponse>((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("/shareAllFieldsByKey")
|
||||||
|
if (as) url = url + "?as=" + as
|
||||||
|
return new Promise<AxiosResponse>((resolve, reject) => {
|
||||||
|
post(url, {
|
||||||
|
key: key,
|
||||||
|
permissions: permissions.map(permission => permissionToString(permission))
|
||||||
|
}).then(resp => {
|
||||||
|
return resolve(resp)
|
||||||
|
}).catch(err => {
|
||||||
|
return reject(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FieldAPIContext.Provider
|
<FieldAPIContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
|
@ -100,7 +139,9 @@ export default function FieldProvider(props: PropsWithChildren<Props>) {
|
||||||
getField,
|
getField,
|
||||||
listFields,
|
listFields,
|
||||||
removeField,
|
removeField,
|
||||||
updateField
|
updateField,
|
||||||
|
shareAll,
|
||||||
|
shareAllByKey
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</FieldAPIContext.Provider>
|
</FieldAPIContext.Provider>
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,6 @@ export default function PermissionProvider(props: PropsWithChildren<Props>) {
|
||||||
let url = pondURL("/" + scope.kind + "s/" + scope.key + "/shareByKey")
|
let url = pondURL("/" + scope.kind + "s/" + scope.key + "/shareByKey")
|
||||||
if (as) url = pondURL(`/${scope.kind}s/${scope.key}/shareByKey?as=${as}`)
|
if (as) url = pondURL(`/${scope.kind}s/${scope.key}/shareByKey?as=${as}`)
|
||||||
return new Promise<AxiosResponse>((resolve, reject) => {
|
return new Promise<AxiosResponse>((resolve, reject) => {
|
||||||
|
|
||||||
post(url, {
|
post(url, {
|
||||||
key: key,
|
key: key,
|
||||||
permissions: permissions.map(permission => permissionToString(permission))
|
permissions: permissions.map(permission => permissionToString(permission))
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import React, { useState } from "react";
|
||||||
import { EventClickArg } from "@fullcalendar/core";
|
import { EventClickArg } from "@fullcalendar/core";
|
||||||
import { DateClickArg } from "@fullcalendar/interaction";
|
import { DateClickArg } from "@fullcalendar/interaction";
|
||||||
import { useGlobalState } from "providers";
|
import { useGlobalState } from "providers";
|
||||||
import { useMobile, useSnackbar, useThemeType } from "hooks";
|
import { useMobile, useSnackbar } from "hooks";
|
||||||
import { useTaskAPI } from "providers";
|
import { useTaskAPI } from "providers";
|
||||||
import { useEffect, useCallback } from "react";
|
import { useEffect, useCallback } from "react";
|
||||||
import TaskCalendar from "./TaskCalendar";
|
import TaskCalendar from "./TaskCalendar";
|
||||||
|
|
@ -12,7 +12,6 @@ import {
|
||||||
Button,
|
Button,
|
||||||
Fab,
|
Fab,
|
||||||
Grid2 as Grid,
|
Grid2 as Grid,
|
||||||
IconButton,
|
|
||||||
LinearProgress,
|
LinearProgress,
|
||||||
Theme,
|
Theme,
|
||||||
Typography
|
Typography
|
||||||
|
|
@ -21,8 +20,6 @@ import { Task } from "models";
|
||||||
import TaskSettings from "./TaskSettings";
|
import TaskSettings from "./TaskSettings";
|
||||||
import { DayPicker } from "react-day-picker";
|
import { DayPicker } from "react-day-picker";
|
||||||
import "react-day-picker/dist/style.css";
|
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 AddIcon from "@mui/icons-material/Add";
|
||||||
import TaskDrawer from "./TaskDrawer";
|
import TaskDrawer from "./TaskDrawer";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
|
@ -33,6 +30,7 @@ interface ViewProps {
|
||||||
label?: string;
|
label?: string;
|
||||||
drawerView?: boolean;
|
drawerView?: boolean;
|
||||||
loadKeys?: string[]; //if you want to load tasks for a specific object(s)
|
loadKeys?: string[]; //if you want to load tasks for a specific object(s)
|
||||||
|
overlayButton?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TabPanelProps {
|
interface TabPanelProps {
|
||||||
|
|
@ -42,17 +40,7 @@ interface TabPanelProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => {
|
const useStyles = makeStyles((theme: Theme) => {
|
||||||
const themeType = useThemeType();
|
|
||||||
|
|
||||||
return ({
|
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: {
|
active: {
|
||||||
color: theme.palette.getContrastText(theme.palette.secondary.main),
|
color: theme.palette.getContrastText(theme.palette.secondary.main),
|
||||||
backgroundColor: theme.palette.secondary.main,
|
backgroundColor: theme.palette.secondary.main,
|
||||||
|
|
@ -64,6 +52,22 @@ const useStyles = makeStyles((theme: Theme) => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fab: {
|
fab: {
|
||||||
|
zIndex: 20,
|
||||||
|
background: theme.palette.primary.main,
|
||||||
|
"&:hover": {
|
||||||
|
background: theme.palette.primary.main
|
||||||
|
},
|
||||||
|
"&:focus": {
|
||||||
|
background: theme.palette.primary.main
|
||||||
|
},
|
||||||
|
position: "absolute",
|
||||||
|
bottom: theme.spacing(8), //for mobile navigator
|
||||||
|
right: theme.spacing(2),
|
||||||
|
[theme.breakpoints.up("sm")]: {
|
||||||
|
bottom: theme.spacing(2)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
overlayFab: {
|
||||||
zIndex: 20,
|
zIndex: 20,
|
||||||
background: theme.palette.primary.main,
|
background: theme.palette.primary.main,
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
|
|
@ -83,19 +87,19 @@ const useStyles = makeStyles((theme: Theme) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function TaskViewer(props: ViewProps) {
|
export default function TaskViewer(props: ViewProps) {
|
||||||
const { objectKey, label, drawerView, loadKeys } = props;
|
const { objectKey, label, drawerView, loadKeys, overlayButton } = props;
|
||||||
const [{ user }] = useGlobalState();
|
const [{ user, as }] = useGlobalState();
|
||||||
const [{ as }] = useGlobalState();
|
//const [{ as }] = useGlobalState();
|
||||||
const taskAPI = useTaskAPI();
|
const taskAPI = useTaskAPI();
|
||||||
const { openSnack } = useSnackbar();
|
const { openSnack } = useSnackbar();
|
||||||
const [tasks, setTasks] = useState<Map<string, Task>>(new Map<string, Task>());
|
const [tasks, setTasks] = useState<Map<string, Task>>(new Map<string, Task>([]));
|
||||||
const [viewTask, setViewTask] = useState<Task>(Task.create());
|
const [viewTask, setViewTask] = useState<Task>();
|
||||||
const [newTaskDialog, setNewTaskDialog] = useState(false);
|
const [newTaskDialog, setNewTaskDialog] = useState(false);
|
||||||
const [editTask, setEditTask] = useState<Task>();
|
const [editTask, setEditTask] = useState<Task>();
|
||||||
const [loaded, setLoaded] = useState(false);
|
const [loaded, setLoaded] = useState(false);
|
||||||
const [currentDate, setCurrentDate] = useState(new Date());
|
const [currentDate, setCurrentDate] = useState(new Date());
|
||||||
const isMobile = useMobile();
|
const isMobile = useMobile();
|
||||||
const [value, setValue] = React.useState(0);
|
const [value, setValue] = useState(0);
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const [expand, setExpand] = useState(false);
|
const [expand, setExpand] = useState(false);
|
||||||
const [hideCal, setHideCal] = useState(false);
|
const [hideCal, setHideCal] = useState(false);
|
||||||
|
|
@ -146,11 +150,13 @@ export default function TaskViewer(props: ViewProps) {
|
||||||
taskAPI
|
taskAPI
|
||||||
.getMultiTasks(loadKeys, as)
|
.getMultiTasks(loadKeys, as)
|
||||||
.then(resp => {
|
.then(resp => {
|
||||||
|
if(resp.data.tasks){
|
||||||
resp.data.tasks.forEach(task => {
|
resp.data.tasks.forEach(task => {
|
||||||
if (task.settings) {
|
if (task.settings) {
|
||||||
temp.set(task.key, Task.any(task));
|
temp.set(task.key, Task.any(task));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
setTasks(temp);
|
setTasks(temp);
|
||||||
setLoaded(true);
|
setLoaded(true);
|
||||||
})
|
})
|
||||||
|
|
@ -167,15 +173,18 @@ export default function TaskViewer(props: ViewProps) {
|
||||||
taskAPI
|
taskAPI
|
||||||
.listTasks(50, 0, "asc", "start", undefined, undefined, as, prevMonth, nextMonth)
|
.listTasks(50, 0, "asc", "start", undefined, undefined, as, prevMonth, nextMonth)
|
||||||
.then(resp => {
|
.then(resp => {
|
||||||
|
if(resp.data.tasks){
|
||||||
resp.data.tasks.forEach(task => {
|
resp.data.tasks.forEach(task => {
|
||||||
if (task.settings) {
|
if (task.settings) {
|
||||||
temp.set(task.key, Task.any(task));
|
temp.set(task.key, Task.any(task));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
setTasks(temp);
|
setTasks(temp);
|
||||||
setLoaded(true);
|
setLoaded(true);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
console.log(err)
|
||||||
openSnack("Failed to load tasks");
|
openSnack("Failed to load tasks");
|
||||||
});
|
});
|
||||||
}, [taskAPI, as, user, openSnack, nextMonth, prevMonth]);
|
}, [taskAPI, as, user, openSnack, nextMonth, prevMonth]);
|
||||||
|
|
@ -216,33 +225,6 @@ export default function TaskViewer(props: ViewProps) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const tabIcons = () => {
|
|
||||||
return (
|
|
||||||
<Box display="flex" justifyContent="left" alignItems="center" zIndex={5}>
|
|
||||||
<IconButton
|
|
||||||
className={value === 0 ? classes.active : classes.avatar}
|
|
||||||
component="span"
|
|
||||||
style={{
|
|
||||||
marginBottom: "0.75rem",
|
|
||||||
marginRight: "0.5rem"
|
|
||||||
}}
|
|
||||||
onClick={() => setValue(0)}>
|
|
||||||
<CalendarToday />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
className={value === 1 ? classes.active : classes.avatar}
|
|
||||||
component="span"
|
|
||||||
style={{
|
|
||||||
marginBottom: "0.75rem",
|
|
||||||
marginRight: "0.5rem"
|
|
||||||
}}
|
|
||||||
onClick={() => setValue(1)}>
|
|
||||||
<NotesIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function TabPanelMine(props: TabPanelProps) {
|
function TabPanelMine(props: TabPanelProps) {
|
||||||
const { children, value, index, ...other } = props;
|
const { children, value, index, ...other } = props;
|
||||||
|
|
||||||
|
|
@ -266,15 +248,7 @@ export default function TaskViewer(props: ViewProps) {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{isMobile || drawerView ? (
|
{isMobile || drawerView ? (
|
||||||
<Box>
|
<Box height={"100%"} position={"relative"}>
|
||||||
<Fab
|
|
||||||
onClick={openDialog}
|
|
||||||
aria-label="Create Task"
|
|
||||||
className={classes.fab}
|
|
||||||
size={isMobile ? "medium" : "large"}>
|
|
||||||
<AddIcon />
|
|
||||||
</Fab>
|
|
||||||
{!drawerView && tabIcons()}
|
|
||||||
<TabPanelMine value={value} index={0}>
|
<TabPanelMine value={value} index={0}>
|
||||||
<Box paddingBottom={2}>
|
<Box paddingBottom={2}>
|
||||||
<TaskCalendar
|
<TaskCalendar
|
||||||
|
|
@ -347,6 +321,13 @@ export default function TaskViewer(props: ViewProps) {
|
||||||
openTask={(taskId: string) => openSelectedTask(taskId)}
|
openTask={(taskId: string) => openSelectedTask(taskId)}
|
||||||
/>
|
/>
|
||||||
</TabPanelMine>
|
</TabPanelMine>
|
||||||
|
<Fab
|
||||||
|
onClick={openDialog}
|
||||||
|
aria-label="Create Task"
|
||||||
|
className={overlayButton ? classes.overlayFab : classes.fab}
|
||||||
|
size={isMobile ? "medium" : "large"}>
|
||||||
|
<AddIcon />
|
||||||
|
</Fab>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<Grid container direction="row" alignContent="center" alignItems="center">
|
<Grid container direction="row" alignContent="center" alignItems="center">
|
||||||
|
|
@ -436,7 +417,7 @@ export default function TaskViewer(props: ViewProps) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box height={"100%"}>
|
||||||
{!loaded ? <LinearProgress style={{ marginTop: 20 }} /> : viewer()}
|
{!loaded ? <LinearProgress style={{ marginTop: 20 }} /> : viewer()}
|
||||||
<TaskSettings
|
<TaskSettings
|
||||||
open={newTaskDialog}
|
open={newTaskDialog}
|
||||||
|
|
@ -455,7 +436,7 @@ export default function TaskViewer(props: ViewProps) {
|
||||||
setNewTaskDialog(false);
|
setNewTaskDialog(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{
|
{viewTask &&
|
||||||
<TaskDrawer
|
<TaskDrawer
|
||||||
task={viewTask}
|
task={viewTask}
|
||||||
open={openDrawer}
|
open={openDrawer}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue