Merge branch 'master' into i2c_detect
This commit is contained in:
commit
33eb4e6eca
47 changed files with 1819 additions and 459 deletions
BIN
src/assets/marketplaceImages/Libra_Cart_temp.jpg
Normal file
BIN
src/assets/marketplaceImages/Libra_Cart_temp.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
|
|
@ -249,9 +249,9 @@ export default function BinCard(props: Props) {
|
|||
noWrap
|
||||
style={{ fontSize: "0.85rem", fontWeight: 650, color: tempColour }}>
|
||||
{display +
|
||||
(user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS
|
||||
? "°C"
|
||||
: "°F")}
|
||||
(user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT
|
||||
? "°F"
|
||||
: "°C")}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ interface Props {
|
|||
loadMore?: () => void;
|
||||
//startingTranslate: number;
|
||||
valDisplay?: "high" | "low" | "average";
|
||||
insert?: boolean
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((_theme) => {
|
||||
|
|
@ -39,7 +40,7 @@ const useStyles = makeStyles((_theme) => {
|
|||
});
|
||||
|
||||
export default function BinsList(props: Props) {
|
||||
const { bins, duplicateBin, title, gridView, loadMore, valDisplay } = props;
|
||||
const { bins, duplicateBin, title, gridView, loadMore, valDisplay, insert } = props;
|
||||
const classes = useStyles();
|
||||
// const history = useHistory();
|
||||
const navigate = useNavigate()
|
||||
|
|
@ -114,11 +115,16 @@ export default function BinsList(props: Props) {
|
|||
{bins.map((b, i) =>
|
||||
(
|
||||
<Grid
|
||||
size={{
|
||||
size={insert ? {
|
||||
xs: 6,
|
||||
sm: 6,
|
||||
md: 4,
|
||||
lg: 3,
|
||||
} : {
|
||||
xs: 6,
|
||||
sm: 4,
|
||||
md: 3,
|
||||
lg: 2,
|
||||
lg: 1.5,
|
||||
}}
|
||||
key={i}
|
||||
className={classes.gridListTile}
|
||||
|
|
|
|||
|
|
@ -316,6 +316,7 @@ export default function BinyardDisplay(props: Props) {
|
|||
<Grid size={12}>
|
||||
<BinsList
|
||||
gridView
|
||||
insert
|
||||
valDisplay="average"
|
||||
bins={grainBins}
|
||||
duplicateBin={duplicateBin}
|
||||
|
|
@ -332,6 +333,7 @@ export default function BinyardDisplay(props: Props) {
|
|||
<Grid size={12}>
|
||||
<BinsList
|
||||
gridView
|
||||
insert
|
||||
valDisplay="average"
|
||||
bins={fertBins}
|
||||
duplicateBin={duplicateBin}
|
||||
|
|
@ -348,6 +350,7 @@ export default function BinyardDisplay(props: Props) {
|
|||
<Grid size={12}>
|
||||
<BinsList
|
||||
gridView
|
||||
insert
|
||||
valDisplay="average"
|
||||
bins={emptyBins}
|
||||
duplicateBin={duplicateBin}
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ export default function DisplayDrawer(props: Props) {
|
|||
classes={{ paper: classes.drawerPaper }}>
|
||||
<Box
|
||||
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 display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Box>
|
||||
|
|
|
|||
|
|
@ -114,6 +114,29 @@ export default function StatusDust(props: Props) {
|
|||
)
|
||||
}
|
||||
|
||||
function compareTimestamps(timestamp1: string, timestamp2: string): number {
|
||||
const date1 = new Date(timestamp1);
|
||||
const date2 = new Date(timestamp2);
|
||||
|
||||
// Check if dates are valid
|
||||
if (isNaN(date1.getTime()) || isNaN(date2.getTime())) {
|
||||
throw new Error("Invalid RFC3339 timestamp format");
|
||||
}
|
||||
|
||||
// Compare using getTime() for millisecond precision
|
||||
return date1.getTime() - date2.getTime();
|
||||
}
|
||||
|
||||
if (device.status.sen5x?.timestamp) {
|
||||
const now = new Date();
|
||||
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
if (compareTimestamps(device.status.sen5x?.timestamp, oneDayAgo.toISOString()) < 0) {
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip title={tooltip()}>
|
||||
<PulseBox color={color} className={classes.box} >
|
||||
|
|
|
|||
|
|
@ -152,9 +152,31 @@ export default function StatusGas(props: Props) {
|
|||
)
|
||||
}
|
||||
|
||||
function compareTimestamps(timestamp1: string, timestamp2: string): number {
|
||||
const date1 = new Date(timestamp1);
|
||||
const date2 = new Date(timestamp2);
|
||||
|
||||
// Check if dates are valid
|
||||
if (isNaN(date1.getTime()) || isNaN(date2.getTime())) {
|
||||
throw new Error("Invalid RFC3339 timestamp format");
|
||||
}
|
||||
|
||||
// Compare using getTime() for millisecond precision
|
||||
return date1.getTime() - date2.getTime();
|
||||
}
|
||||
|
||||
const gasDisplay = () => {
|
||||
const gas = gasType === "all" ? gasTypes[currentIndex] : gasType
|
||||
// if (gasType !== "all") {
|
||||
if (device.status[gas]?.timestamp) {
|
||||
const now = new Date();
|
||||
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
if (compareTimestamps(device.status[gas]?.timestamp, oneDayAgo.toISOString()) < 0) {
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<Tooltip title={tooltip()}>
|
||||
<PulseBox color={color} className={gasType === "all" ? classes.boxTall : classes.box}>
|
||||
|
|
|
|||
|
|
@ -80,6 +80,29 @@ export default function StatusPlenum(props: Props) {
|
|||
)
|
||||
}
|
||||
|
||||
function compareTimestamps(timestamp1: string, timestamp2: string): number {
|
||||
const date1 = new Date(timestamp1);
|
||||
const date2 = new Date(timestamp2);
|
||||
|
||||
// Check if dates are valid
|
||||
if (isNaN(date1.getTime()) || isNaN(date2.getTime())) {
|
||||
throw new Error("Invalid RFC3339 timestamp format");
|
||||
}
|
||||
|
||||
// Compare using getTime() for millisecond precision
|
||||
return date1.getTime() - date2.getTime();
|
||||
}
|
||||
|
||||
if (device.status.plenum?.timestamp) {
|
||||
const now = new Date();
|
||||
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
if (compareTimestamps(device.status.plenum?.timestamp, oneDayAgo.toISOString()) < 0) {
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip title={tooltip()}>
|
||||
<PulseBox color={color} className={classes.box} >
|
||||
|
|
|
|||
|
|
@ -120,6 +120,29 @@ export default function StatusSen5x(props: Props) {
|
|||
)
|
||||
}
|
||||
|
||||
function compareTimestamps(timestamp1: string, timestamp2: string): number {
|
||||
const date1 = new Date(timestamp1);
|
||||
const date2 = new Date(timestamp2);
|
||||
|
||||
// Check if dates are valid
|
||||
if (isNaN(date1.getTime()) || isNaN(date2.getTime())) {
|
||||
throw new Error("Invalid RFC3339 timestamp format");
|
||||
}
|
||||
|
||||
// Compare using getTime() for millisecond precision
|
||||
return date1.getTime() - date2.getTime();
|
||||
}
|
||||
|
||||
if (device.status.sen5x?.timestamp) {
|
||||
const now = new Date();
|
||||
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
if (compareTimestamps(device.status.sen5x?.timestamp, oneDayAgo.toISOString()) < 0) {
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip title={tooltip()}>
|
||||
<PulseBox color={color} className={classes.box} >
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
MenuItem,
|
||||
OutlinedInput,
|
||||
Select,
|
||||
TextField,
|
||||
Theme
|
||||
} from "@mui/material";
|
||||
import moment, { Moment } from "moment";
|
||||
|
|
@ -16,6 +17,7 @@ import ReactDOM from "react-dom";
|
|||
import { DateRangePreset, SetDefaultPreset } from "./DateRange";
|
||||
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||
import { withStyles, WithStyles } from "@mui/styles";
|
||||
import { cloneDeep } from "lodash";
|
||||
|
||||
const styles = (_theme: Theme) => createStyles({});
|
||||
|
||||
|
|
@ -249,18 +251,30 @@ class DateSelect extends React.Component<Props, State> {
|
|||
onClose={this.closeDateRangeDialog}
|
||||
aria-labelledby="date-range-dialog">
|
||||
<DialogContent>
|
||||
{/* <StaticDateRangePicker
|
||||
disableFuture
|
||||
value={dateRange}
|
||||
onChange={date => this.updateDateRange(date)}
|
||||
renderInput={(startProps, endProps) => (
|
||||
<React.Fragment>
|
||||
<TextField {...startProps} />
|
||||
<DateRangeDelimiter> to </DateRangeDelimiter>
|
||||
<TextField {...endProps} />
|
||||
</React.Fragment>
|
||||
)}
|
||||
/> */}
|
||||
<TextField
|
||||
fullWidth
|
||||
margin="normal"
|
||||
type="date"
|
||||
label="Start Time"
|
||||
value={dateRange[0]?.format("YYYY-MM-DD")}
|
||||
onChange={e => {
|
||||
let range = cloneDeep(dateRange)
|
||||
range[0] = moment(e.target.value)
|
||||
this.updateDateRange(range)
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
margin="normal"
|
||||
type="date"
|
||||
label="End Time"
|
||||
value={dateRange[1]?.format("YYYY-MM-DD")}
|
||||
onChange={e => {
|
||||
let range = cloneDeep(dateRange)
|
||||
range[1] = moment(e.target.value)
|
||||
this.updateDateRange(range)
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={this.closeDateRangeDialog} color="primary">
|
||||
|
|
|
|||
|
|
@ -285,7 +285,6 @@ export default function ComponentCard(props: Props) {
|
|||
// ) : (
|
||||
<UnitMeasurementSummary
|
||||
component={component}
|
||||
excludedNodes={component.settings.excludedNodes}
|
||||
reading={UnitMeasurement.convertLastMeasurement(measurements)}
|
||||
//dense
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -449,7 +449,7 @@ export default function ComponentForm(props: Props) {
|
|||
let minimum = min ?? 0;
|
||||
let maximum = max ?? 100;
|
||||
if (compMode && form.component.settings.calibrate) {
|
||||
return Number(form.offset) <= maximum && Number(form.offset) > minimum;
|
||||
return Number(form.offset) <= maximum && Number(form.offset) >= minimum;
|
||||
}
|
||||
return !isNaN(Number(form.offset));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -270,7 +270,7 @@ export default function UpgradeDevice(props: Props) {
|
|||
<Divider />
|
||||
<DialogContent className={classes.dialogContent}>
|
||||
<DialogContentText id="alert-dialog-slide-description">
|
||||
Upgrading from {currentFirmware()} to {latestFirmware()}
|
||||
Upgrading {device.platformName()} from {currentFirmware()} to {latestFirmware()}
|
||||
<br />
|
||||
<br />
|
||||
It is recommended that the device be plugged in during the upgrade as it will take
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Box,
|
||||
Button,
|
||||
Grid2,
|
||||
IconButton,
|
||||
Paper,
|
||||
Stack,
|
||||
Tab,
|
||||
Table,
|
||||
TableBody,
|
||||
|
|
@ -9,16 +15,26 @@ import {
|
|||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Tabs
|
||||
Tabs,
|
||||
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 { 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;
|
||||
|
|
@ -26,128 +42,244 @@ interface TabPanelProps {
|
|||
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() {
|
||||
const [{ as }] = useGlobalState();
|
||||
const isMobile = useMobile();
|
||||
const fieldAPI = useFieldAPI();
|
||||
const jdAPI = useJohnDeereProxyAPI();
|
||||
const cnhAPI = useCNHiProxyAPI();
|
||||
const { openSnack } = useSnackbar();
|
||||
const [fields, setFields] = useState<Field[]>([]);
|
||||
const [value, setValue] = React.useState(0);
|
||||
const [tabValue, setTabValue] = React.useState(0);
|
||||
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) => {
|
||||
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 => {
|
||||
setFields(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]);
|
||||
//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 = () => {
|
||||
let totalAcres = 0;
|
||||
fields.forEach(field => {
|
||||
totalAcres += field.acres();
|
||||
});
|
||||
totalAcres = Math.round(totalAcres);
|
||||
return totalAcres;
|
||||
};
|
||||
const goTo = (field: Field) => {
|
||||
let path = field.key();
|
||||
navigate(path, { state: {field: field, permissions: field.permissions} });
|
||||
}
|
||||
|
||||
const listFieldsInfo = fields.map((field, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{field.fieldName()}</TableCell>
|
||||
<TableCell>{field.landLoc()}</TableCell>
|
||||
<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={() => {
|
||||
setFieldForPlan(field);
|
||||
setOpenHarvestSettings(true);
|
||||
}}>
|
||||
New Crop Plan
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
));
|
||||
const fieldActions = (field: Field) => {
|
||||
return (
|
||||
<EventBlocker>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setSelectedField(field)
|
||||
setOpenFieldSettings(true)
|
||||
}}><Settings /></IconButton>
|
||||
<FieldActions field={field} permissions={field.permissions} refreshCallback={()=>{}}/>
|
||||
{isMobile && <IconButton onClick={() => {goTo(field)}}><ArrowForward /></IconButton>}
|
||||
</EventBlocker>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<Box>
|
||||
<Button
|
||||
onClick={()=>{setShareOpen(true)}}
|
||||
variant="contained"
|
||||
color="primary">
|
||||
Share All
|
||||
</Button>
|
||||
<Tabs
|
||||
value={value}
|
||||
value={tabValue}
|
||||
onChange={handleChange}
|
||||
TabIndicatorProps={{ style: { background: "rgba(255,255,0,255)" } }}>
|
||||
<Tab label="Field Overview" />
|
||||
{/* {!isMobile && <Tab label="Current Harvest Plans" />}
|
||||
{!isMobile && <Tab label="Plan History" />} */}
|
||||
<Tab label="Adaptive" />
|
||||
<Tab label="John Deere" />
|
||||
<Tab label="Case New Holland" />
|
||||
</Tabs>
|
||||
<TabPanelMine index={0} value={value}>
|
||||
<TableContainer component={Paper}>
|
||||
<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
|
||||
{fieldTable(fields)}
|
||||
{/* <HarvestSettings
|
||||
open={openHarvestSettings}
|
||||
close={() => setOpenHarvestSettings(false)}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -152,9 +152,7 @@ export default function FieldSettings(props: Props) {
|
|||
|
||||
const fieldInformation = () => {
|
||||
return (
|
||||
<Box>
|
||||
<Box>
|
||||
<DialogTitle>Field Information</DialogTitle>
|
||||
<React.Fragment>
|
||||
<TextField
|
||||
value={fieldName}
|
||||
margin="normal"
|
||||
|
|
@ -258,7 +256,14 @@ export default function FieldSettings(props: Props) {
|
|||
Field Colour
|
||||
<ColourPicker onChange={color => setFeatColor(color)} />
|
||||
</Box>
|
||||
</Box>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ResponsiveDialog fullWidth open={props.open} onClose={onClose}>
|
||||
<DialogTitle>Field Information</DialogTitle>
|
||||
<DialogContent>{fieldInformation()}</DialogContent>
|
||||
<DialogActions>
|
||||
<Grid container direction="row">
|
||||
<Grid>
|
||||
|
|
@ -282,13 +287,6 @@ export default function FieldSettings(props: Props) {
|
|||
</Grid>
|
||||
</Grid>
|
||||
</DialogActions>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ResponsiveDialog fullWidth fullScreen={false} open={props.open} onClose={onClose}>
|
||||
<DialogContent>{fieldInformation()}</DialogContent>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
|
@ -249,9 +249,9 @@ export default function GateSVG(props: Props) {
|
|||
<g key={"finalTemp"} id={"finalTemp"} data-name={"finalTemp"}>
|
||||
<text fontSize={7} className={classes.fontBase} x={100} y={85}>
|
||||
{convertFinalTemp(finalTemp).toFixed(2)}°
|
||||
{user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS
|
||||
? "C"
|
||||
: "F"}
|
||||
{user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT
|
||||
? "°F"
|
||||
: "°C"}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
|
|
@ -260,9 +260,9 @@ export default function GateSVG(props: Props) {
|
|||
<g key={"ambientTemp"} id={"ambientTemp"} data-name={"ambientTemp"}>
|
||||
<text fontSize={7} className={classes.fontBase} x={30} y={114}>
|
||||
Ambient: {ambientTemp}°
|
||||
{user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS
|
||||
? "C"
|
||||
: "F"}
|
||||
{user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT
|
||||
? "°F"
|
||||
: "°C"}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
|
|
@ -271,13 +271,13 @@ export default function GateSVG(props: Props) {
|
|||
<g key={"tempChain"} id={"tempChain"} data-name={"tempChain"}>
|
||||
<text fontSize={7} className={classes.fontBase} x={30} y={149}>
|
||||
T1: {innerTemps.t1}°
|
||||
{user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS
|
||||
? "C"
|
||||
: "F"}
|
||||
{user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT
|
||||
? "°F"
|
||||
: "°C"}
|
||||
, T2: {innerTemps.t2}°
|
||||
{user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS
|
||||
? "C"
|
||||
: "F"}
|
||||
{user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT
|
||||
? "°F"
|
||||
: "°C"}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,10 +30,14 @@ export default function DuplicateHarvestPlan(props: Props) {
|
|||
const harvestPlanAPI = useHarvestPlanAPI();
|
||||
const taskAPI = useTaskAPI();
|
||||
const [ready, setReady] = useState(false);
|
||||
const [fieldOptions, setFieldOptions] = useState<Field[]>([])
|
||||
const fieldAPI = useFieldAPI();
|
||||
|
||||
const loadTasks = useCallback(() => {
|
||||
taskAPI.listTasks(50, 0, "asc", "start", plan.key(), undefined).then(resp => {
|
||||
setPlanTasks(resp.data.tasks.map(t => Task.any(t)));
|
||||
if(resp.data.tasks){
|
||||
setPlanTasks(resp.data.tasks.map(t => Task.any(t)));
|
||||
}
|
||||
setReady(true);
|
||||
});
|
||||
}, [taskAPI, plan]);
|
||||
|
|
@ -41,9 +46,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 +90,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) => (
|
||||
<MenuItem key={option.key()} value={i}>
|
||||
{option.fieldName()}
|
||||
</MenuItem>
|
||||
|
|
@ -98,8 +119,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
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -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 /> Edit or Add Activity
|
||||
<Edit /> Activities
|
||||
</Button>
|
||||
)}
|
||||
<IconButton
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
import { Notes } from "@mui/icons-material";
|
||||
import GrainDescriber from "grain/GrainDescriber";
|
||||
import { Field, HarvestPlan } from "models";
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { getThemeType } from "theme";
|
||||
import Chat from "chat/Chat";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
|
|
@ -23,7 +23,7 @@ interface Props {
|
|||
permissions: pond.Permission[];
|
||||
planField: Field;
|
||||
loading: boolean;
|
||||
fieldList: Field[];
|
||||
fieldList?: Field[];
|
||||
changePlan: (updatedPlan?: HarvestPlan) => void;
|
||||
}
|
||||
|
||||
|
|
@ -175,7 +175,7 @@ export default function HarvestPlanDisplay(props: Props) {
|
|||
<Typography
|
||||
variant="h5"
|
||||
style={{ fontWeight: 700, marginTop: "auto", marginBottom: "auto" }}>
|
||||
Crop Plan{plan.key() !== "" ? " - " + plan.settings.title : " - None Found"}
|
||||
{plan.key() !== "" ? plan.settings.title : "None Found"}
|
||||
</Typography>
|
||||
{!isMobile && permissions.includes(pond.Permission.PERMISSION_WRITE) && (
|
||||
<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>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -210,7 +210,7 @@ export default function LibraCartAccess() {
|
|||
</Grid>
|
||||
<Box paddingTop={2}>
|
||||
<Typography>
|
||||
Libra Cart data will sync every 6 hours, if the data is needed immediately you can select the Sync button. It may take up to 15 minutes for the data to appear in our platform.
|
||||
Libra Cart data will sync every 6 hours, if the data is needed immediately you can select the Sync button.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
|
|
|
|||
|
|
@ -241,7 +241,7 @@ export default function MapBase(props: Props) {
|
|||
homeMarkerAPI
|
||||
.listHomeMarkers(1, 0, undefined, undefined, undefined, undefined, as)
|
||||
.then(resp => {
|
||||
if (resp.data.homeMarker.length < 1) {
|
||||
if (!resp.data.homeMarker || resp.data.homeMarker.length < 1) {
|
||||
setHomePin(undefined);
|
||||
setHaveHome(false);
|
||||
setStartingView(props.currentView);
|
||||
|
|
@ -271,6 +271,7 @@ export default function MapBase(props: Props) {
|
|||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
openSnack("Could not load Home Pin");
|
||||
});
|
||||
}, [as, homeMarkerAPI, openSnack, props.ignoreHomeLoad, user]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ export default function FieldDrawer(props: Props) {
|
|||
<Weather longitude={field.center().longitude} latitude={field.center().latitude} />
|
||||
</TabPanelMine>
|
||||
<TabPanelMine value={value} index={2}>
|
||||
<TaskViewer drawerView objectKey={field.key()} loadKeys={taskLoadKeys} />
|
||||
<TaskViewer drawerView objectKey={field.key()} loadKeys={taskLoadKeys} overlayButton />
|
||||
</TabPanelMine>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
/>
|
||||
)}
|
||||
</Source>
|
||||
{measurementCollection &&
|
||||
<Source type="geojson" data={measurementCollection} id={"measurements"}>
|
||||
<Layer
|
||||
id="measurementBorder"
|
||||
|
|
@ -90,6 +93,7 @@ export default function GeoMapLayer(props: Props) {
|
|||
}}
|
||||
/>
|
||||
</Source>
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -221,24 +221,26 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
|
|||
.then(resp => {
|
||||
let fields: Map<string, Field> = new Map();
|
||||
let fieldEntries: Result[] = [];
|
||||
resp.data.fields.forEach(f => {
|
||||
let field = Field.any(f);
|
||||
fields.set(field.key(), field);
|
||||
fieldEntries.push({
|
||||
id: field.key(),
|
||||
place_name: field.name(),
|
||||
place_type: ["field"],
|
||||
center: [field.center().longitude, field.center().latitude],
|
||||
address: "",
|
||||
type: "Feature",
|
||||
text: field.name(),
|
||||
bbox: [0,0,0,0],
|
||||
geometry: {type: "Point", coordinates: [field.center().longitude, field.center().latitude]},
|
||||
context: [],
|
||||
properties: {},
|
||||
relevance: 0
|
||||
if(resp.data.fields){
|
||||
resp.data.fields.forEach(f => {
|
||||
let field = Field.any(f);
|
||||
fields.set(field.key(), field);
|
||||
fieldEntries.push({
|
||||
id: field.key(),
|
||||
place_name: field.name(),
|
||||
place_type: ["field"],
|
||||
center: [field.center().longitude, field.center().latitude],
|
||||
address: "",
|
||||
type: "Feature",
|
||||
text: field.name(),
|
||||
bbox: [0,0,0,0],
|
||||
geometry: {type: "Point", coordinates: [field.center().longitude, field.center().latitude]},
|
||||
context: [],
|
||||
properties: {},
|
||||
relevance: 0
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
setFields(fields);
|
||||
setFieldSearchEntries(fieldEntries);
|
||||
})
|
||||
|
|
@ -314,53 +316,56 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
|
|||
let bagOp: BagModel[] = [];
|
||||
let bagMarkerData: Map<string, MarkerData> = new Map();
|
||||
let bagEntries: Result[] = [];
|
||||
resp.data.grainBags.forEach(bag => {
|
||||
let b = BagModel.create(bag);
|
||||
bags.set(b.key(), b);
|
||||
if (bag.settings) {
|
||||
if (!b.settings.startLocation?.longitude && !b.settings.endLocation?.longitude) {
|
||||
bagOp.push(b);
|
||||
} else {
|
||||
//build the data for the marker
|
||||
//TODO-CS: think about making the creation of marker data a function in the object model
|
||||
let mData: MarkerData = {
|
||||
title: b.name(),
|
||||
longitude: b.centerLocation().longitude,
|
||||
latitude: b.centerLocation().latitude,
|
||||
visibleLevels: { max: 13 },
|
||||
colour: b.settings.theme?.color ?? "white",
|
||||
clickFunc: (e, i, isMobile) => {
|
||||
clickDelay();
|
||||
closeDrawers();
|
||||
setBagDrawer(true);
|
||||
setIgnoreFeatures(true);
|
||||
setObjectKey(b.key());
|
||||
moveMap(
|
||||
b.centerLocation().latitude,
|
||||
b.centerLocation().longitude,
|
||||
zoomLevels.far,
|
||||
isMobile
|
||||
);
|
||||
}
|
||||
};
|
||||
bagMarkerData.set(b.key(), mData);
|
||||
bagEntries.push({
|
||||
center: [b.centerLocation().longitude, b.centerLocation().latitude],
|
||||
id: b.key(),
|
||||
place_name: b.name(),
|
||||
place_type: ["grainBag"],
|
||||
address: "",
|
||||
bbox: [0,0,0,0],
|
||||
context: [],
|
||||
geometry: {type: "Point", coordinates: [b.centerLocation().longitude, b.centerLocation().latitude]},
|
||||
properties: {},
|
||||
relevance: 0,
|
||||
text: "",
|
||||
type: "Feature"
|
||||
});
|
||||
if(resp.data.grainBags){
|
||||
|
||||
resp.data.grainBags.forEach(bag => {
|
||||
let b = BagModel.create(bag);
|
||||
bags.set(b.key(), b);
|
||||
if (bag.settings) {
|
||||
if (!b.settings.startLocation?.longitude && !b.settings.endLocation?.longitude) {
|
||||
bagOp.push(b);
|
||||
} else {
|
||||
//build the data for the marker
|
||||
//TODO-CS: think about making the creation of marker data a function in the object model
|
||||
let mData: MarkerData = {
|
||||
title: b.name(),
|
||||
longitude: b.centerLocation().longitude,
|
||||
latitude: b.centerLocation().latitude,
|
||||
visibleLevels: { max: 13 },
|
||||
colour: b.settings.theme?.color ?? "white",
|
||||
clickFunc: (e, i, isMobile) => {
|
||||
clickDelay();
|
||||
closeDrawers();
|
||||
setBagDrawer(true);
|
||||
setIgnoreFeatures(true);
|
||||
setObjectKey(b.key());
|
||||
moveMap(
|
||||
b.centerLocation().latitude,
|
||||
b.centerLocation().longitude,
|
||||
zoomLevels.far,
|
||||
isMobile
|
||||
);
|
||||
}
|
||||
};
|
||||
bagMarkerData.set(b.key(), mData);
|
||||
bagEntries.push({
|
||||
center: [b.centerLocation().longitude, b.centerLocation().latitude],
|
||||
id: b.key(),
|
||||
place_name: b.name(),
|
||||
place_type: ["grainBag"],
|
||||
address: "",
|
||||
bbox: [0,0,0,0],
|
||||
context: [],
|
||||
geometry: {type: "Point", coordinates: [b.centerLocation().longitude, b.centerLocation().latitude]},
|
||||
properties: {},
|
||||
relevance: 0,
|
||||
text: "",
|
||||
type: "Feature"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
setBagOptions(bagOp);
|
||||
setGrainBags(bags);
|
||||
grainBagsRef.current = bags;
|
||||
|
|
@ -382,19 +387,21 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
|
|||
binYardAPI
|
||||
.listBinYards(400, 0, "asc", undefined, undefined, undefined, as)
|
||||
.then(resp => {
|
||||
resp.data.yard.forEach(yard => {
|
||||
if (yard.settings) {
|
||||
yardMap.set(yard.settings.key, yard.settings);
|
||||
if (
|
||||
(!yard.settings.latitude && !yard.settings.longitude) ||
|
||||
(yard.settings.latitude === 0 && yard.settings.longitude === 0)
|
||||
) {
|
||||
yardOps.push(yard.settings);
|
||||
} else {
|
||||
let mData: MarkerData = {
|
||||
title: yard.settings.name,
|
||||
longitude: yard.settings.longitude,
|
||||
latitude: yard.settings.latitude,
|
||||
if(resp.data.yard){
|
||||
|
||||
resp.data.yard.forEach(yard => {
|
||||
if (yard.settings) {
|
||||
yardMap.set(yard.settings.key, yard.settings);
|
||||
if (
|
||||
(!yard.settings.latitude && !yard.settings.longitude) ||
|
||||
(yard.settings.latitude === 0 && yard.settings.longitude === 0)
|
||||
) {
|
||||
yardOps.push(yard.settings);
|
||||
} else {
|
||||
let mData: MarkerData = {
|
||||
title: yard.settings.name,
|
||||
longitude: yard.settings.longitude,
|
||||
latitude: yard.settings.latitude,
|
||||
colour: yard.settings.theme?.color ?? "green",
|
||||
markerIcon: <BinsIcon width={50 * 0.6} height={50 * 0.6} type="light" />,
|
||||
visibleLevels: { max: 17 },
|
||||
|
|
@ -429,6 +436,7 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
|
|||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
setYardMarkerData(newYardMarkers);
|
||||
setYardSearchEntries(yardEntries);
|
||||
setBinYards(yardMap);
|
||||
|
|
@ -447,6 +455,9 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
|
|||
let markerData: Map<string, MarkerData> = new Map();
|
||||
let searchEntries: Result[] = [];
|
||||
let binOp: IBin[] = [];
|
||||
if(resp.data.bins){
|
||||
|
||||
|
||||
resp.data.bins.forEach(bin => {
|
||||
let b = IBin.create(bin);
|
||||
binMap.set(b.key(), b);
|
||||
|
|
@ -495,6 +506,7 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
|
|||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
setBinMarkerData(markerData);
|
||||
setBinSearchEntries(searchEntries);
|
||||
setBins(binMap);
|
||||
|
|
@ -514,6 +526,9 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
|
|||
let searchEntries: Result[] = [];
|
||||
|
||||
let options: DeviceModel[] = [];
|
||||
if(resp.data.devices){
|
||||
|
||||
|
||||
resp.data.devices.forEach((device, i) => {
|
||||
//add device to the main map
|
||||
let d = DeviceModel.any(device);
|
||||
|
|
@ -553,6 +568,7 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
|
|||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
setDevices(map);
|
||||
setDeviceOptions(options);
|
||||
setDeviceMarkerData(newDevMarkers);
|
||||
|
|
@ -725,6 +741,9 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
|
|||
let fmMap: Map<string, FieldMarker> = new Map();
|
||||
let markerData: Map<string, MarkerData> = new Map();
|
||||
let searchEntries: Result[] = [];
|
||||
if(resp.data.fieldMarker){
|
||||
|
||||
|
||||
resp.data.fieldMarker.forEach(fieldMarker => {
|
||||
let fm = FieldMarker.create(fieldMarker);
|
||||
fmMap.set(fm.key(), fm);
|
||||
|
|
@ -750,7 +769,7 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
|
|||
});
|
||||
setAnchorFieldMarker(event.currentTarget);
|
||||
setFMIndexKey(index);
|
||||
setObjectKey(fm.key());
|
||||
setObjectKey(fm.key());
|
||||
}
|
||||
});
|
||||
searchEntries.push({
|
||||
|
|
@ -760,6 +779,7 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
|
|||
place_type: ["fieldMarker"]
|
||||
} as Result);
|
||||
});
|
||||
}
|
||||
setFieldMarkers(fmMap);
|
||||
setFieldMarkerData(markerData);
|
||||
setFieldMarkerSearchEntries(searchEntries);
|
||||
|
|
|
|||
|
|
@ -175,6 +175,7 @@ export default function AviationMapController(props: Props) {
|
|||
let terminalMarkers: Map<string, MarkerData> = new Map();
|
||||
let terminalOps: Terminal[] = [];
|
||||
let searchEntries: Result[] = [];
|
||||
if(resp.data.terminals){
|
||||
resp.data.terminals.forEach(term => {
|
||||
let terminal = Terminal.create(term);
|
||||
terminalMap.set(terminal.key, terminal);
|
||||
|
|
@ -207,6 +208,7 @@ export default function AviationMapController(props: Props) {
|
|||
terminalOps.push(terminal);
|
||||
}
|
||||
});
|
||||
}
|
||||
setTerminals(terminalMap);
|
||||
setTerminalMarkers(terminalMarkers);
|
||||
setTerminalSearchEntries(searchEntries);
|
||||
|
|
|
|||
|
|
@ -142,6 +142,7 @@ export default function ConstructionMapController(props: Props) {
|
|||
siteAPI
|
||||
.listSitesPageData(50, 0, "asc", "key", undefined, as)
|
||||
.then(resp => {
|
||||
if(resp.data.sites){
|
||||
resp.data.sites.forEach(site => {
|
||||
if (!site) return;
|
||||
let newSite: Site = Site.any(site.site);
|
||||
|
|
@ -179,6 +180,7 @@ export default function ConstructionMapController(props: Props) {
|
|||
place_type: ["site"]
|
||||
} as Result);
|
||||
});
|
||||
}
|
||||
setSiteMarkerData(sm);
|
||||
setSites(s);
|
||||
setLoadingSites(false);
|
||||
|
|
|
|||
|
|
@ -109,6 +109,35 @@ export class Device {
|
|||
return m;
|
||||
}
|
||||
|
||||
public platformName(): string {
|
||||
switch (this.settings.platform) {
|
||||
case pond.DevicePlatform.DEVICE_PLATFORM_PHOTON:
|
||||
return "Photon"
|
||||
case pond.DevicePlatform.DEVICE_PLATFORM_ELECTRON:
|
||||
return "Electron"
|
||||
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR:
|
||||
return "V2 Cellular"
|
||||
case pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI:
|
||||
return "V2 Wifi"
|
||||
case pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_S3:
|
||||
return "V2 Wifi S3"
|
||||
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_S3:
|
||||
return "V2 Cellular S3"
|
||||
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLACK:
|
||||
return "V2 Cellular Black"
|
||||
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_GREEN:
|
||||
return "V2 Callular Green"
|
||||
case pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_BLUE:
|
||||
return "V2 Wifi Blue"
|
||||
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLUE:
|
||||
return "V2 Cellular Blue"
|
||||
case pond.DevicePlatform.DEVICE_PLATFORM_V2_ETHERNET_BLUE:
|
||||
return "V2 Ethernet Blue"
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public featureSupported(feature: string): boolean {
|
||||
let versions = featureVersions.get(feature);
|
||||
if (!versions) {
|
||||
|
|
|
|||
|
|
@ -5,18 +5,21 @@ 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();
|
||||
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;
|
||||
}
|
||||
|
|
@ -152,6 +155,60 @@ 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 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 {
|
||||
if (this.grain() !== pond.Grain.GRAIN_INVALID) {
|
||||
if (this.grain() === pond.Grain.GRAIN_CUSTOM) {
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<Route path="terminals/*" element={<TerminalGatesRoute />} />
|
||||
<Route path="jobsites/*" element={<JobsitesRoute />} />
|
||||
<Route path="heaters/*" element={<ObjectHeatersRoute />} />
|
||||
<Route path="fields/*" element={<FieldsRoute />} />
|
||||
</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 (!isAuthenticated) {
|
||||
// loginWithRedirect()
|
||||
|
|
@ -325,7 +342,6 @@ export default function Router() {
|
|||
{user.hasFeature("admin") &&
|
||||
<Route path="logs" element={<Logs />} />
|
||||
}
|
||||
<Route path="fields" element={<Fields />} />
|
||||
<Route path="marketplace" element={<Marketplace />} />
|
||||
{user.hasFeature("admin") && (
|
||||
<Route path="firmware" element={<Firmware />} />
|
||||
|
|
|
|||
|
|
@ -208,6 +208,118 @@ export default function Devices() {
|
|||
})
|
||||
}, [tab])
|
||||
|
||||
function compareTimestamps(timestamp1: string, timestamp2: string): number {
|
||||
const date1 = new Date(timestamp1);
|
||||
const date2 = new Date(timestamp2);
|
||||
|
||||
// Check if dates are valid
|
||||
if (isNaN(date1.getTime()) || isNaN(date2.getTime())) {
|
||||
throw new Error("Invalid RFC3339 timestamp format");
|
||||
}
|
||||
|
||||
// Compare using getTime() for millisecond precision
|
||||
return date1.getTime() - date2.getTime();
|
||||
}
|
||||
|
||||
const doesDeviceHavePlenum = (device: pond.Device) => {
|
||||
if (device.status?.plenum?.temperature && device.status?.plenum?.timestamp) {
|
||||
if (device.status.plenum?.timestamp) {
|
||||
const now = new Date();
|
||||
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
if (compareTimestamps(device.status.plenum?.timestamp, oneDayAgo.toISOString()) < 0) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const doesDeviceHaveSen5x = (device: pond.Device) => {
|
||||
if (device.status?.sen5x?.temperature && device.status?.sen5x?.timestamp) {
|
||||
if (device.status.sen5x?.timestamp) {
|
||||
const now = new Date();
|
||||
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
if (compareTimestamps(device.status.sen5x?.timestamp, oneDayAgo.toISOString()) < 0) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// if (device.status?.o2) setHasO2(true)
|
||||
// if (device.status?.co) setHasCo(true)
|
||||
// if (device.status?.co2) setHasCo2(true)
|
||||
// if (device.status?.no2) setHasNo2(true)
|
||||
|
||||
const doesDeviceHaveO2 = (device: pond.Device) => {
|
||||
if (device.status?.o2?.ppm && device.status?.o2?.timestamp) {
|
||||
if (device.status.sen5x?.timestamp) {
|
||||
const now = new Date();
|
||||
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
if (compareTimestamps(device.status.o2?.timestamp, oneDayAgo.toISOString()) < 0) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const doesDeviceHaveCo = (device: pond.Device) => {
|
||||
if (device.status?.co?.ppm && device.status?.co?.timestamp) {
|
||||
if (device.status.co?.timestamp) {
|
||||
const now = new Date();
|
||||
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
if (compareTimestamps(device.status.co?.timestamp, oneDayAgo.toISOString()) < 0) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const doesDeviceHaveCo2 = (device: pond.Device) => {
|
||||
if (device.status?.co2?.ppm && device.status?.co2?.timestamp) {
|
||||
if (device.status.co2?.timestamp) {
|
||||
const now = new Date();
|
||||
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
if (compareTimestamps(device.status.co2?.timestamp, oneDayAgo.toISOString()) < 0) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const doesDeviceHaveNo2 = (device: pond.Device) => {
|
||||
if (device.status?.no2?.ppm && device.status?.no2?.timestamp) {
|
||||
if (device.status.no2?.timestamp) {
|
||||
const now = new Date();
|
||||
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
if (compareTimestamps(device.status.no2?.timestamp, oneDayAgo.toISOString()) < 0) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
console.log(hasPlenums)
|
||||
}, [hasPlenums])
|
||||
|
||||
const openProvisionDialog = () => {
|
||||
setIsProvisionDialogOpen(true);
|
||||
};
|
||||
|
|
@ -319,12 +431,13 @@ export default function Devices() {
|
|||
).then(resp => {
|
||||
let newDevices: Device[] = []
|
||||
resp.data.devices.forEach(device => {
|
||||
if (device.status?.plenum?.temperature) setHasPlenums(true)
|
||||
if (device.status?.sen5x?.temperature) setHasSen5x(true)
|
||||
if (device.status?.o2) setHasO2(true)
|
||||
if (device.status?.co) setHasCo(true)
|
||||
if (device.status?.co2) setHasCo2(true)
|
||||
if (device.status?.no2) setHasNo2(true)
|
||||
setHasPlenums(doesDeviceHavePlenum(device))
|
||||
// setHasPlenums(true)
|
||||
setHasSen5x(doesDeviceHaveSen5x(device))
|
||||
setHasO2(doesDeviceHaveO2(device))
|
||||
setHasCo(doesDeviceHaveCo(device))
|
||||
setHasCo2(doesDeviceHaveCo2(device))
|
||||
setHasNo2(doesDeviceHaveNo2(device))
|
||||
newDevices.push(Device.create(device))
|
||||
})
|
||||
setDevices(newDevices)
|
||||
|
|
@ -809,12 +922,12 @@ export default function Devices() {
|
|||
).then(resp => {
|
||||
let newDevices: Device[] = []
|
||||
resp.data.devices.forEach(device => {
|
||||
if (device.status?.plenum?.temperature) setHasPlenums(true)
|
||||
if (device.status?.sen5x?.temperature) setHasSen5x(true)
|
||||
if (device.status?.o2) setHasO2(true)
|
||||
if (device.status?.co) setHasCo(true)
|
||||
if (device.status?.co2) setHasCo2(true)
|
||||
if (device.status?.no2) setHasNo2(true)
|
||||
setHasPlenums(doesDeviceHavePlenum(device))
|
||||
setHasSen5x(doesDeviceHaveSen5x(device))
|
||||
setHasO2(doesDeviceHaveO2(device))
|
||||
setHasCo(doesDeviceHaveCo(device))
|
||||
setHasCo2(doesDeviceHaveCo2(device))
|
||||
setHasNo2(doesDeviceHaveNo2(device))
|
||||
newDevices.push(Device.create(device))
|
||||
})
|
||||
setDevices(currentRows.concat(newDevices))
|
||||
|
|
|
|||
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,9 @@ export default function Heaters() {
|
|||
objectHeaterAPI
|
||||
.listObjectHeatersPageData(pageSize, tablePage * pageSize, undefined, undefined, searchText, as)
|
||||
.then(resp => {
|
||||
// setHeaters(resp.data.heaterData ?? []);
|
||||
setTotal(resp.data.total)
|
||||
setHeaters(resp.data.heaterData);
|
||||
setHeaters(resp.data.heaterData ?? []);
|
||||
});
|
||||
}, [objectHeaterAPI, as, pageSize, tablePage, searchText]);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export default function Tasks() {
|
|||
return (
|
||||
<PageContainer>
|
||||
<Box padding={2}>
|
||||
<TaskViewer />
|
||||
<TaskViewer overlayButton/>
|
||||
</Box>
|
||||
</PageContainer>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -182,7 +182,9 @@ export default function Terminals(props: Props) {
|
|||
if(loading) return
|
||||
setLoading(true)
|
||||
terminalAPI.listTerminals(200, 0, "asc", "name", as).then(resp => {
|
||||
setTerminals(resp.data.terminals.map(a => Terminal.any(a)));
|
||||
if(resp.data.terminals){
|
||||
setTerminals(resp.data.terminals.map(a => Terminal.any(a)));
|
||||
}
|
||||
}).catch(err => {
|
||||
console.log("There was a problem loading Terminals")
|
||||
}).finally(() => {
|
||||
|
|
|
|||
|
|
@ -108,7 +108,8 @@ export default function Users() {
|
|||
"remove-devices",
|
||||
"copy-token",
|
||||
"recluse",
|
||||
"pause-data"
|
||||
"pause-data",
|
||||
"access-object"
|
||||
].sort();
|
||||
const features = [
|
||||
"admin",
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@ 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<any>;
|
||||
getField: (fieldId: string, otherTeam?: string) => Promise<any>;
|
||||
getField: (fieldId: string, otherTeam?: string) => Promise<AxiosResponse<pond.Field>>;
|
||||
listFields: (
|
||||
limit: number,
|
||||
offset: number,
|
||||
|
|
@ -24,7 +25,15 @@ export interface IFieldAPIContext {
|
|||
field: pond.FieldSettings,
|
||||
asRoot?: true,
|
||||
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);
|
||||
|
|
@ -44,8 +53,8 @@ export default function FieldProvider(props: PropsWithChildren<Props>) {
|
|||
|
||||
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<pond.Field>(pondURL("/field/" + fieldId + "?as=" + view));
|
||||
return get<pond.Field>(pondURL("/field/" + fieldId));
|
||||
};
|
||||
|
||||
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 (
|
||||
<FieldAPIContext.Provider
|
||||
value={{
|
||||
|
|
@ -100,7 +139,9 @@ export default function FieldProvider(props: PropsWithChildren<Props>) {
|
|||
getField,
|
||||
listFields,
|
||||
removeField,
|
||||
updateField
|
||||
updateField,
|
||||
shareAll,
|
||||
shareAllByKey
|
||||
}}>
|
||||
{children}
|
||||
</FieldAPIContext.Provider>
|
||||
|
|
|
|||
|
|
@ -90,8 +90,7 @@ export default function InteractionProvider(props: PropsWithChildren<Props>) {
|
|||
) => {
|
||||
const view = otherTeam ? otherTeam : as
|
||||
let url = pondURL(
|
||||
"/interactions/forComponents/" +
|
||||
"?componentIds=" +
|
||||
"/interactions/forComponents?componentIds=" +
|
||||
fullComponentLocations.toString() +
|
||||
(view ? "&as=" + view : "")
|
||||
)
|
||||
|
|
|
|||
|
|
@ -135,7 +135,6 @@ export default function PermissionProvider(props: PropsWithChildren<Props>) {
|
|||
let url = pondURL("/" + scope.kind + "s/" + scope.key + "/shareByKey")
|
||||
if (as) url = pondURL(`/${scope.kind}s/${scope.key}/shareByKey?as=${as}`)
|
||||
return new Promise<AxiosResponse>((resolve, reject) => {
|
||||
|
||||
post(url, {
|
||||
key: key,
|
||||
permissions: permissions.map(permission => permissionToString(permission))
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ export default function TeamProvider(props: PropsWithChildren<Props>) {
|
|||
prefixSearch?: string,
|
||||
) => {
|
||||
//let asText = team ? "&as=" + team.key() : "";
|
||||
const view = otherTeam ? otherTeam : as
|
||||
const view = otherTeam ? otherTeam : (as.length > 0 ? as : undefined)
|
||||
let url = pondURL(
|
||||
"/teams" +
|
||||
"?limit=" +
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
@ -33,6 +30,7 @@ interface ViewProps {
|
|||
label?: string;
|
||||
drawerView?: boolean;
|
||||
loadKeys?: string[]; //if you want to load tasks for a specific object(s)
|
||||
overlayButton?: boolean;
|
||||
}
|
||||
|
||||
interface TabPanelProps {
|
||||
|
|
@ -42,17 +40,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,
|
||||
|
|
@ -64,6 +52,22 @@ const useStyles = makeStyles((theme: Theme) => {
|
|||
}
|
||||
},
|
||||
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,
|
||||
background: theme.palette.primary.main,
|
||||
"&:hover": {
|
||||
|
|
@ -83,19 +87,19 @@ const useStyles = makeStyles((theme: Theme) => {
|
|||
});
|
||||
|
||||
export default function TaskViewer(props: ViewProps) {
|
||||
const { objectKey, label, drawerView, loadKeys } = props;
|
||||
const [{ user }] = useGlobalState();
|
||||
const [{ as }] = useGlobalState();
|
||||
const { objectKey, label, drawerView, loadKeys, overlayButton } = props;
|
||||
const [{ user, as }] = useGlobalState();
|
||||
//const [{ as }] = useGlobalState();
|
||||
const taskAPI = useTaskAPI();
|
||||
const { openSnack } = useSnackbar();
|
||||
const [tasks, setTasks] = useState<Map<string, Task>>(new Map<string, Task>());
|
||||
const [viewTask, setViewTask] = useState<Task>(Task.create());
|
||||
const [tasks, setTasks] = useState<Map<string, Task>>(new Map<string, Task>([]));
|
||||
const [viewTask, setViewTask] = useState<Task>();
|
||||
const [newTaskDialog, setNewTaskDialog] = useState(false);
|
||||
const [editTask, setEditTask] = useState<Task>();
|
||||
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);
|
||||
|
|
@ -138,6 +142,7 @@ export default function TaskViewer(props: ViewProps) {
|
|||
openDialog();
|
||||
};
|
||||
|
||||
|
||||
const loadMultitask = useCallback(() => {
|
||||
if (!loadKeys) return;
|
||||
if (loadKeys.length > 0) {
|
||||
|
|
@ -146,11 +151,13 @@ export default function TaskViewer(props: ViewProps) {
|
|||
taskAPI
|
||||
.getMultiTasks(loadKeys, as)
|
||||
.then(resp => {
|
||||
resp.data.tasks.forEach(task => {
|
||||
if (task.settings) {
|
||||
temp.set(task.key, Task.any(task));
|
||||
}
|
||||
});
|
||||
if(resp.data.tasks){
|
||||
resp.data.tasks.forEach(task => {
|
||||
if (task.settings) {
|
||||
temp.set(task.key, Task.any(task));
|
||||
}
|
||||
});
|
||||
}
|
||||
setTasks(temp);
|
||||
setLoaded(true);
|
||||
})
|
||||
|
|
@ -167,15 +174,18 @@ export default function TaskViewer(props: ViewProps) {
|
|||
taskAPI
|
||||
.listTasks(50, 0, "asc", "start", undefined, undefined, as, prevMonth, nextMonth)
|
||||
.then(resp => {
|
||||
resp.data.tasks.forEach(task => {
|
||||
if (task.settings) {
|
||||
temp.set(task.key, Task.any(task));
|
||||
}
|
||||
});
|
||||
if(resp.data.tasks){
|
||||
resp.data.tasks.forEach(task => {
|
||||
if (task.settings) {
|
||||
temp.set(task.key, Task.any(task));
|
||||
}
|
||||
});
|
||||
}
|
||||
setTasks(temp);
|
||||
setLoaded(true);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
openSnack("Failed to load tasks");
|
||||
});
|
||||
}, [taskAPI, as, user, openSnack, nextMonth, prevMonth]);
|
||||
|
|
@ -216,33 +226,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) {
|
||||
const { children, value, index, ...other } = props;
|
||||
|
||||
|
|
@ -266,15 +249,7 @@ export default function TaskViewer(props: ViewProps) {
|
|||
return (
|
||||
<React.Fragment>
|
||||
{isMobile || drawerView ? (
|
||||
<Box>
|
||||
<Fab
|
||||
onClick={openDialog}
|
||||
aria-label="Create Task"
|
||||
className={classes.fab}
|
||||
size={isMobile ? "medium" : "large"}>
|
||||
<AddIcon />
|
||||
</Fab>
|
||||
{!drawerView && tabIcons()}
|
||||
<Box height={"100%"} position={"relative"}>
|
||||
<TabPanelMine value={value} index={0}>
|
||||
<Box paddingBottom={2}>
|
||||
<TaskCalendar
|
||||
|
|
@ -347,6 +322,13 @@ export default function TaskViewer(props: ViewProps) {
|
|||
openTask={(taskId: string) => openSelectedTask(taskId)}
|
||||
/>
|
||||
</TabPanelMine>
|
||||
<Fab
|
||||
onClick={openDialog}
|
||||
aria-label="Create Task"
|
||||
className={overlayButton ? classes.overlayFab : classes.fab}
|
||||
size={isMobile ? "medium" : "large"}>
|
||||
<AddIcon />
|
||||
</Fab>
|
||||
</Box>
|
||||
) : (
|
||||
<Grid container direction="row" alignContent="center" alignItems="center">
|
||||
|
|
@ -436,7 +418,7 @@ export default function TaskViewer(props: ViewProps) {
|
|||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box height={"100%"}>
|
||||
{!loaded ? <LinearProgress style={{ marginTop: 20 }} /> : viewer()}
|
||||
<TaskSettings
|
||||
open={newTaskDialog}
|
||||
|
|
@ -455,15 +437,15 @@ export default function TaskViewer(props: ViewProps) {
|
|||
setNewTaskDialog(false);
|
||||
}}
|
||||
/>
|
||||
{
|
||||
<TaskDrawer
|
||||
task={viewTask}
|
||||
open={openDrawer}
|
||||
closeCallback={() => setOpenDrawer(false)}
|
||||
completeTask={markComplete}
|
||||
deleteTask={deleteTask}
|
||||
editTask={setTaskToEdit}
|
||||
/>
|
||||
{viewTask &&
|
||||
<TaskDrawer
|
||||
task={viewTask}
|
||||
open={openDrawer}
|
||||
closeCallback={() => setOpenDrawer(false)}
|
||||
completeTask={markComplete}
|
||||
deleteTask={deleteTask}
|
||||
editTask={setTaskToEdit}
|
||||
/>
|
||||
}
|
||||
</Box>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -266,7 +266,7 @@ export default function UserMenu() {
|
|||
</MenuItem>
|
||||
)}
|
||||
<Divider />
|
||||
{hasAdmin && (
|
||||
{(hasAdmin || user.allowedTo("access-object")) && (
|
||||
<Tooltip title="Access Object">
|
||||
<MenuItem onClick={() => openAccessObject()} aria-label="Access Object" dense>
|
||||
<ListItemIcon>
|
||||
|
|
@ -276,7 +276,7 @@ export default function UserMenu() {
|
|||
</MenuItem>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Divider />
|
||||
{(hasAdmin || user.allowedTo("access-object")) && <Divider /> }
|
||||
<MenuItem onClick={handleLogout} aria-label="Sign Out" dense>
|
||||
<ListItemIcon>
|
||||
<ExitToApp className={classes.red} />
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import FeatureCard from "./FeatureCard";
|
|||
//import { ImgIcon } from "common/ImgIcon";
|
||||
//images to import for the image in the card for the feature make sure to have a 2:1 aspect ratio to keep the cards symmetrical
|
||||
import FeatureImageTest from "assets/marketplaceImages/VisualFarmMPImage.png";
|
||||
import LibraCartImage from "assets/marketplaceImages/Libra_Cart_temp.jpg";
|
||||
|
||||
// export interface FAQ {
|
||||
// question: string;
|
||||
|
|
@ -62,14 +63,14 @@ const agFeatureList: ProductDetails[] = [
|
|||
// bulletPoints: ["bullet one", "bullet two"],
|
||||
// questions: [],
|
||||
},
|
||||
// {
|
||||
// featureName: "libra-cart",
|
||||
// featureLogo: <LibraCartIcon />,
|
||||
// featureCost: 0,
|
||||
// cardImage: FeatureImageTest,
|
||||
// featureTitle: "Libra Cart",
|
||||
// longDescription: "Integrate with Libra Cart to bring your data into our platform"
|
||||
// }
|
||||
{
|
||||
featureName: "libra-cart",
|
||||
featureLogo: <LibraCartIcon />,
|
||||
featureCost: 0,
|
||||
cardImage: LibraCartImage,
|
||||
featureTitle: "Libra Cart",
|
||||
longDescription: "Integrate with Libra Cart to bring your data into our platform"
|
||||
}
|
||||
];
|
||||
|
||||
// const constructionFeatureList: ProductDetails[] = []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue