added the sites page and all other required imports for it to work, also fixed the map card

This commit is contained in:
csawatzky 2025-04-03 12:54:00 -06:00
parent c5afdc2c3d
commit 3990dfbf4f
14 changed files with 1595 additions and 21 deletions

2
package-lock.json generated
View file

@ -10770,7 +10770,7 @@
}, },
"node_modules/protobuf-ts": { "node_modules/protobuf-ts": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#229e52a3f667df826cbd2e9b9de73fd0de70ab0c", "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#d1cf77df1de7c39e0cdf6663c7a25a364935f242",
"dependencies": { "dependencies": {
"protobufjs": "^6.8.8" "protobufjs": "^6.8.8"
} }

View file

@ -3,7 +3,9 @@ import { Beenhere } from "@mui/icons-material";
import { useThemeType } from "hooks"; import { useThemeType } from "hooks";
import moment from "moment"; import moment from "moment";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import ReactMapGL, { Marker } from "react-map-gl/mapbox-legacy"; import Map, { Marker } from "react-map-gl/mapbox-legacy";
const MAPBOX_TOKEN = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;
export interface Coordinate { export interface Coordinate {
latitude: number; latitude: number;
@ -79,27 +81,27 @@ export default function MapGL(props: Props) {
); );
}); });
}); });
console.log(elems)
return elems; return elems;
}; };
let style = let style =
themeType === "dark" ? "mapbox://styles/mapbox/dark-v10" : "mapbox://styles/mapbox/light-v10"; themeType === "dark" ? "mapbox://styles/mapbox/dark-v10" : "mapbox://styles/mapbox/light-v10";
return ( return (
<ReactMapGL <Map
mapStyle={style} mapStyle={style}
initialViewState={viewport} initialViewState={viewport}
//width="100%" mapboxAccessToken={MAPBOX_TOKEN}
//height="100%"
{...viewport} {...viewport}
// onViewStateChange={(e: any) => { onMove={(e: any) => {
// setViewport({ setViewport({
// longitude: e.viewState.longitude, longitude: e.viewState.longitude,
// latitude: e.viewState.latitude, latitude: e.viewState.latitude,
// zoom: e.viewState.zoom, zoom: e.viewState.zoom,
// }); });
// }} }}
> >
{markers()} {markers()}
</ReactMapGL> </Map>
); );
} }

View file

@ -45,7 +45,7 @@ const {terminals, currentTerminal } = props;
const classes = useStyles(); const classes = useStyles();
const [gateDialog, setGateDialog] = useState(false); const [gateDialog, setGateDialog] = useState(false);
const [tablePage, setTablePage] = useState(0) const [tablePage, setTablePage] = useState(0)
const [pageSize, setPageSize] = useState(5) const [pageSize, setPageSize] = useState(10)
const [total, setTotal] = useState(0) const [total, setTotal] = useState(0)
const [terminalMap, setTerminalMap] = useState<Map<string, string>>(new Map<string, string>()); const [terminalMap, setTerminalMap] = useState<Map<string, string>>(new Map<string, string>());
const gateAPI = useGateAPI(); const gateAPI = useGateAPI();
@ -77,11 +77,11 @@ const {terminals, currentTerminal } = props;
setGates(resp.data.gates.map(gate => Gate.create(gate))) setGates(resp.data.gates.map(gate => Gate.create(gate)))
setTotal(resp.data.total) setTotal(resp.data.total)
}) })
},[currentTerminal, search]) },[currentTerminal, search, pageSize, tablePage])
useEffect(() => { useEffect(() => {
loadGates() loadGates()
},[currentTerminal, search]) },[loadGates])
const handleChange = (event: any) => { const handleChange = (event: any) => {
setPageSize(event.target.value); setPageSize(event.target.value);
@ -213,7 +213,7 @@ const {terminals, currentTerminal } = props;
renderGutter={isMobile || props.useMobile ? gutter : undefined} renderGutter={isMobile || props.useMobile ? gutter : undefined}
gutterPadding={0} gutterPadding={0}
rows={gates} rows={gates}
setPage={(page) => {console.log(page)}} setPage={(page) => {setTablePage(page)}}
total={total} total={total}
onRowClick={(gate) => {goToGate(gate)}} onRowClick={(gate) => {goToGate(gate)}}
hideKeys={isMobile || props.useMobile} hideKeys={isMobile || props.useMobile}

319
src/jobsite/SiteActions.tsx Normal file
View file

@ -0,0 +1,319 @@
import {
Button,
DialogActions,
DialogContent,
DialogTitle,
IconButton,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
TextField,
Theme,
Tooltip
} from "@mui/material";
import { blue } from "@mui/material/colors";
import RemoveSelfIcon from "@mui/icons-material/ExitToApp";
import MoreIcon from "@mui/icons-material/MoreVert";
import ShareObjectIcon from "@mui/icons-material/Share";
import ObjectUsersIcon from "@mui/icons-material/AccountCircle";
import ObjectTeamsIcon from "@mui/icons-material/SupervisedUserCircle";
import { pond } from "protobuf-ts/pond";
import React, { useState } from "react";
import ObjectUsers from "user/ObjectUsers";
import RemoveSelfFromObject from "user/RemoveSelfFromObject";
import ShareObject from "user/ShareObject";
import { isOffline } from "utils/environment";
import ObjectTeams from "teams/ObjectTeams";
import { siteScope, Site } from "models";
import ResponsiveDialog from "common/ResponsiveDialog";
import { useSiteAPI } from "providers";
import { useSnackbar } from "hooks";
import GroupSettingsIcon from "@mui/icons-material/Settings";
import { makeStyles } from "@mui/styles";
const useStyles = makeStyles((theme: Theme) => ({
shareIcon: {
color: blue["500"],
"&:hover": {
color: blue["600"]
}
},
removeIcon: {
color: "var(--status-alert)"
},
red: {
color: "var(--status-alert)"
}
}));
interface Props {
site: Site;
permissions: pond.Permission[];
refreshCallback: () => void;
}
interface OpenState {
share: boolean;
users: boolean;
teams: boolean;
settings: boolean;
removeSelf: boolean;
}
export default function SiteActions(props: Props) {
const classes = useStyles();
const { site, permissions, refreshCallback } = props;
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const [siteDialog, setSiteDialog] = useState(false);
const [deleteConfirmation, setDeleteConfirmation] = useState(false);
const siteAPI = useSiteAPI();
const [siteName, setSiteName] = useState(site.siteName());
const [update, setUpdate] = useState(false);
const { openSnack } = useSnackbar();
const [openState, setOpenState] = useState<OpenState>({
share: false,
users: false,
teams: false,
settings: false,
removeSelf: false
});
const groupMenu = () => {
const canShare = permissions.includes(pond.Permission.PERMISSION_SHARE);
const canManageUsers = permissions.includes(pond.Permission.PERMISSION_USERS);
return (
<Menu
id="groupMenu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
keepMounted
disableAutoFocusItem>
{!isOffline() && canShare && (
<MenuItem
dense
onClick={() => {
setOpenState({ ...openState, share: true });
setAnchorEl(null);
}}>
<ListItemIcon>
<ShareObjectIcon className={classes.shareIcon} />
</ListItemIcon>
<ListItemText primary="Share" />
</MenuItem>
)}
{!isOffline() && canManageUsers && (
<MenuItem
dense
onClick={() => {
setOpenState({ ...openState, users: true });
setAnchorEl(null);
}}>
<ListItemIcon>
<ObjectUsersIcon />
</ListItemIcon>
<ListItemText primary="Users" />
</MenuItem>
)}
{!isOffline() && canManageUsers && (
<MenuItem
dense
onClick={() => {
setOpenState({ ...openState, teams: true });
setAnchorEl(null);
}}>
<ListItemIcon>
<ObjectTeamsIcon />
</ListItemIcon>
<ListItemText primary="Teams" />
</MenuItem>
)}
<MenuItem
dense
onClick={() => {
setOpenState({ ...openState, removeSelf: true });
setAnchorEl(null);
}}>
<ListItemIcon>
<RemoveSelfIcon className={classes.red} />
</ListItemIcon>
<ListItemText primary="Leave" />
</MenuItem>
</Menu>
);
};
const dialogs = () => {
const key = site.key();
const label = site.siteName();
return (
<React.Fragment>
<ShareObject
scope={siteScope(key)}
label={label}
permissions={permissions}
isDialogOpen={openState.share}
closeDialogCallback={() => setOpenState({ ...openState, share: false })}
/>
<ObjectUsers
scope={siteScope(key)}
label={label}
permissions={permissions}
isDialogOpen={openState.users}
closeDialogCallback={() => setOpenState({ ...openState, users: false })}
refreshCallback={refreshCallback}
/>
<RemoveSelfFromObject
scope={siteScope(key)}
label={label}
isDialogOpen={openState.removeSelf}
closeDialogCallback={() => setOpenState({ ...openState, removeSelf: false })}
/>
{dialogForSite()}
{deleteDialog()}
</React.Fragment>
);
};
const closeSiteDialog = () => {
setSiteDialog(false);
setUpdate(false);
};
const updateSite = () => {
let newSite = site;
newSite.settings.siteName = siteName;
siteAPI
.updateSite(newSite.key(), newSite.settings)
.then(resp => {
openSnack("Site Updated");
closeSiteDialog();
})
.catch(err => {
openSnack("Could Not Update Site");
closeSiteDialog();
});
};
//deletes a site from the DB and removes the coordinates of that site from the array
const deleteSite = () => {
siteAPI
.removeSite(site.key())
.then(resp => {
openSnack("Site has been removed");
closeSiteDialog();
})
.catch(err => {
openSnack("Site could not be removed");
});
};
const openUpdate = () => {
setUpdate(true);
setSiteDialog(true);
};
const deleteDialog = () => {
return (
<ResponsiveDialog
fullWidth
open={deleteConfirmation}
onClose={() => {
setDeleteConfirmation(false);
}}>
<DialogTitle>Delete Site</DialogTitle>
<DialogContent>Are you sure you wish to delete {site.siteName()}</DialogContent>
<DialogActions>
<Button
onClick={() => {
setDeleteConfirmation(false);
}}
color="primary">
Cancel
</Button>
<Button onClick={deleteSite} style={{ color: "red" }}>
Confirm
</Button>
</DialogActions>
</ResponsiveDialog>
);
};
//dialog the renders the forms for creating a new site
const dialogForSite = () => {
return (
<ResponsiveDialog fullWidth fullScreen={false} open={siteDialog} onClose={closeSiteDialog}>
<DialogTitle>Enter Name of Site</DialogTitle>
<DialogContent>
<TextField
value={siteName}
color="primary"
autoFocus
margin="dense"
id="name"
label="Site Name"
type="text"
fullWidth
onChange={e => setSiteName(e.target.value)}
/>
</DialogContent>
{update ? (
<DialogActions>
<Button
onClick={() => {
setDeleteConfirmation(true);
}}
style={{ color: "red" }}>
Delete Site
</Button>
<Button onClick={updateSite} color="primary">
Update Site
</Button>
</DialogActions>
) : (
<DialogActions>
<Button onClick={closeSiteDialog} color="primary">
Cancel
</Button>
{/* <Button onClick={saveSite} color="primary" disabled={siteName === ""}>
Add Site
</Button> */}
</DialogActions>
)}
</ResponsiveDialog>
);
};
return (
<React.Fragment>
{permissions.includes(pond.Permission.PERMISSION_WRITE) && (
<Tooltip title="Jobsite Settings">
<IconButton
onClick={() => {
openUpdate();
}}>
<GroupSettingsIcon />
</IconButton>
</Tooltip>
)}
<IconButton
aria-owns={anchorEl ? "groupMenu" : undefined}
aria-haspopup="true"
onClick={(event: React.MouseEvent<HTMLButtonElement>) => setAnchorEl(event.currentTarget)}>
<MoreIcon />
</IconButton>
{groupMenu()}
{dialogs()}
<ObjectTeams
scope={siteScope(site.key())}
label="Teams"
permissions={permissions}
isDialogOpen={openState.teams}
refreshCallback={refreshCallback}
closeDialogCallback={() => setOpenState({ ...openState, teams: false })}
/>
</React.Fragment>
);
}

View file

@ -0,0 +1,33 @@
import { cloneDeep } from "lodash";
import { pond } from "protobuf-ts/pond";
export class ObjectHeater {
public settings: pond.ObjectHeaterSettings = pond.ObjectHeaterSettings.create();
public name: string = "Heater";
public key: string = "";
public static create(pb?: pond.ObjectHeater): ObjectHeater {
let my = new ObjectHeater();
if (pb) {
my.settings = pond.ObjectHeaterSettings.fromObject(cloneDeep(pb.settings ?? {}));
my.name = pb.name;
my.key = pb.key;
}
return my;
}
public static clone(other?: ObjectHeater): ObjectHeater {
if (other) {
return ObjectHeater.create(
pond.ObjectHeater.fromObject({
settings: cloneDeep(other.settings)
})
);
}
return ObjectHeater.create();
}
public static any(data: any): ObjectHeater {
return ObjectHeater.create(pond.ObjectHeater.fromObject(cloneDeep(data)));
}
}

View file

@ -13,6 +13,9 @@ import Terminals from "pages/Terminals";
import Gate from "pages/Gate"; import Gate from "pages/Gate";
import AviationMap from "pages/AviationMap"; import AviationMap from "pages/AviationMap";
import ConstructionSiteMap from "pages/ConstructionSiteMap"; import ConstructionSiteMap from "pages/ConstructionSiteMap";
import Sites from "pages/Sites";
import Site from "pages/Site";
//import Site from "pages/Site";
const DeviceHistory = lazy(() => import("pages/DeviceHistory")); const DeviceHistory = lazy(() => import("pages/DeviceHistory"));
const DevicePage = lazy(() => import("pages/Device")); const DevicePage = lazy(() => import("pages/Device"));
@ -51,6 +54,7 @@ export default function Router(props: Props) {
<Route path="bins/*" element={<BinsRoute/>} /> <Route path="bins/*" element={<BinsRoute/>} />
<Route path="mines/*" element={<MinesRoute/>} /> <Route path="mines/*" element={<MinesRoute/>} />
<Route path="terminals/*" element={<TerminalGatesRoute />} /> <Route path="terminals/*" element={<TerminalGatesRoute />} />
<Route path="jobsites/*" element={<JobsitesRoute />} />
</Routes> </Routes>
) )
} }
@ -178,6 +182,28 @@ export default function Router(props: Props) {
); );
}; };
const JobsitesRoute = () => {
return (
<div>
<Routes>
<Route
key="Jobsite page route"
path="" // "/settings/basic"
element={<Sites key={"Jobsites page"} />}
/>
<Route
path="/:siteKey" // "/settings/basic"
element={<Site />}
/>
<Route
path="/:siteKey/*" // "/settings/basic"
element={<RelativeRoutes />}
/>
</Routes>
</div>
);
};
const GroupsRoute = () => { const GroupsRoute = () => {
return ( return (

View file

@ -36,6 +36,7 @@ import { useAuth0 } from "@auth0/auth0-react";
import FieldsIcon from "products/AgIcons/FieldsIcon"; import FieldsIcon from "products/AgIcons/FieldsIcon";
import PlaneIcon from "products/AviationIcons/PlaneIcon"; import PlaneIcon from "products/AviationIcons/PlaneIcon";
import AirportMapIcon from "products/AviationIcons/AirportMapIcon"; import AirportMapIcon from "products/AviationIcons/AirportMapIcon";
import JobsiteIcon from "products/Construction/JobSiteIcon";
const drawerWidth = 230; const drawerWidth = 230;
@ -265,6 +266,20 @@ export default function SideNavigator(props: Props) {
</ListItemButton> </ListItemButton>
</Tooltip> </Tooltip>
)} )}
{(isAdCon || user.hasFeature("admin")) && (
<Tooltip title="Jobsites" placement="right">
<ListItemButton
id="tour-jobsites"
onClick={() => goTo("/jobsites")}
classes={getClasses("/jobsites")}
>
<ListItemIcon>
<JobsiteIcon />
</ListItemIcon>
{open && <ListItemText primary="Jobsites" />}
</ListItemButton>
</Tooltip>
)}
</List> </List>
) )
} }

View file

@ -0,0 +1,409 @@
import {
Avatar,
Box,
Button,
Card,
CardContent,
Grid2 as Grid,
ListItemAvatar,
ListItemText,
Theme,
Typography,
useTheme
} from "@mui/material";
import { GetDefaultDateRange } from "common/time/DateRange";
import UnitMeasurementSummary from "component/UnitMeasurementSummary";
import { useThemeType } from "hooks";
import { Component, Device } from "models";
import { ObjectHeater } from "models/ObjectHeater";
import { UnitMeasurement } from "models/UnitMeasurement";
import moment from "moment";
import { GetComponentIcon } from "pbHelpers/ComponentType";
import ObjectHeaterIcon from "products/Construction/ObjectHeaterIcon";
import { pond, quack } from "protobuf-ts/pond";
import { useGlobalState, useMutationAPI } from "providers";
import React, { useEffect, useState } from "react";
import FuelDark from "assets/components/fuelDark.png";
import FuelLight from "assets/components/fuelLight.png";
//import { useHistory } from "react-router";
import { useNavigate } from "react-router-dom";
import Link from "@mui/icons-material/Link";
import EventBlocker from "common/EventBlocker";
import { GetDeviceProductIcon } from "products/DeviceProduct";
import { makeStyles } from "@mui/styles";
interface Props {
heaterData: pond.ObjectHeaterData;
linkDevice: (heater: ObjectHeater, linkedDevices: pond.ComprehensiveDevice[]) => void;
}
const useStyles = makeStyles((theme: Theme) => ({
avatarIcon: {
width: theme.spacing(3),
height: theme.spacing(3)
},
listAvatar: {
minWidth: theme.spacing(5)
},
avatarImage: {
width: theme.spacing(3),
height: theme.spacing(3)
},
textOverflow: {
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
textTransform: "capitalize"
},
listText: {
margin: 0,
height: "100%"
}
})
);
export default function ObjectHeaterCard(props: Props) {
const { heaterData, linkDevice } = props;
const [heater, setHeater] = useState<ObjectHeater>(ObjectHeater.create());
const [connectedDevice, setConnectedDevice] = useState<Device>();
const mutationAPI = useMutationAPI();
const [heaterComponent, setHeaterComponent] = useState<Component>();
const [heaterTempComponent, setHeaterTempComponent] = useState<Component>();
const [siteTempComponent, setSiteTempComponent] = useState<Component>();
const [mutationsLoading, setMutationsLoading] = useState(false);
const classes = useStyles();
const themeType = useThemeType();
const [{ user }] = useGlobalState();
const [lastFuelCalc, setLastFuelCalc] = useState<pond.ValueAt>();
const [lastFuelMutation, setLastFuelMutation] = useState<pond.LinearMutation>();
const [allComponentsRecent, setAllComponentsRecent] = useState(true);
//const history = useHistory();
const navigate = useNavigate();
const theme = useTheme();
useEffect(() => {
let heater = ObjectHeater.any(heaterData.heater);
setHeater(heater);
if (!mutationsLoading && heater) {
let fuelMutation: pond.LinearMutation | undefined;
heater.settings.mutations.forEach(mutation => {
if (mutation.mutation === pond.Mutator.MUTATOR_FUEL_LEVEL) {
fuelMutation = mutation;
}
});
if (fuelMutation !== undefined) {
mutationAPI
.linearMutation(
fuelMutation,
GetDefaultDateRange()
.end.add(-1, "hour")
.toISOString(),
GetDefaultDateRange().end.toISOString()
)
.then(resp => {
resp.data.values.forEach(val => {
setLastFuelCalc(val);
});
setLastFuelMutation(fuelMutation);
setMutationsLoading(false);
});
}
}
if (heaterData.devices[0]) {
setConnectedDevice(Device.any(heaterData.devices[0].device));
heaterData.devices[0].components.forEach(c => {
//loop through the components and assign them accordingly
let component = Component.any(c);
if (
component.type() === quack.ComponentType.COMPONENT_TYPE_BOOLEAN_OUTPUT &&
component.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER
) {
setHeaterComponent(component);
component.lastMeasurement.forEach(um => {
if (
um.timestamps.length > 0 &&
um.type === quack.MeasurementType.MEASUREMENT_TYPE_BOOLEAN
) {
if (
moment().diff(moment(um.timestamps[0]), "milliseconds") >
component.settings.reportPeriodMs
) {
setAllComponentsRecent(false);
}
}
});
}
if (component.type() === quack.ComponentType.COMPONENT_TYPE_TEMPERATURE) {
setHeaterTempComponent(component);
component.lastMeasurement.forEach(um => {
if (
um.timestamps.length > 0 &&
um.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE
) {
if (
moment().diff(moment(um.timestamps[0]), "milliseconds") >
component.settings.reportPeriodMs
) {
setAllComponentsRecent(false);
}
}
});
}
if (component.type() === quack.ComponentType.COMPONENT_TYPE_DHT) {
setSiteTempComponent(component);
component.lastMeasurement.forEach(um => {
if (
um.timestamps.length > 0 &&
um.type === quack.MeasurementType.MEASUREMENT_TYPE_BOOLEAN
) {
if (
moment().diff(moment(um.timestamps[0]), "milliseconds") >
component.settings.reportPeriodMs
) {
setAllComponentsRecent(false);
}
}
});
}
});
}
}, [heaterData, mutationAPI]); //eslint-disable-line react-hooks/exhaustive-deps
const goToHeater = () => {
//history.push("/objectHeaters/" + heater.key);
let path = "/objectHeaters/" + heater.key;
navigate(path, { state: {heater: heater} });
};
const componentDisplay = (component?: Component) => {
return (
<Grid container direction="row" wrap="nowrap" alignContent="center" alignItems="center">
{component ? (
<React.Fragment>
<Grid>
<Avatar
variant="square"
alt="icon"
src={GetComponentIcon(component.type(), component.subType(), themeType)}
style={{ float: "left" }}
className={classes.avatarIcon}
/>
</Grid>
<Grid style={{ paddingLeft: 5 }}>
<UnitMeasurementSummary
component={component}
reading={UnitMeasurement.convertLastMeasurement(
component.lastMeasurement.map(um => UnitMeasurement.any(um, user))
)}
omitTime
dense
centered
onlyRecent
/>
</Grid>
</React.Fragment>
) : (
"Component Not Found"
)}
</Grid>
);
};
//// TODO-CS: Make a mutator describer to put the get functions into ////
const getMutationIcon = (mutation: pond.Mutator, themeType: string) => {
if (mutation === pond.Mutator.MUTATOR_FUEL_LEVEL) {
return themeType === "light" ? FuelDark : FuelLight;
}
return "";
};
const getMutationColour = (mutation: pond.Mutator) => {
if (mutation === pond.Mutator.MUTATOR_FUEL_LEVEL) {
return "lightBlue";
}
return "";
};
const getMutationUnit = (mutation: pond.Mutator) => {
if (mutation === pond.Mutator.MUTATOR_FUEL_LEVEL) {
return "%";
}
return "";
};
//// /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ ////
const fuelMutationDisplay = () => {
if (!lastFuelMutation) {
return (
<Grid container direction="row" wrap="nowrap" alignContent="center" alignItems="center">
{/* <Grid item>
<Avatar
variant="square"
alt="icon"
src={getMutationIcon(pond.Mutator.MUTATOR_FUEL_LEVEL, themeType)}
style={{ float: "left" }}
className={classes.avatarIcon}
/>
</Grid> */}
<Grid style={{ paddingLeft: 5 }}>
N/A
</Grid>
</Grid>
);
}
if (lastFuelCalc) {
return (
<Grid container>
<Grid>
<Avatar
variant="square"
alt="icon"
src={getMutationIcon(pond.Mutator.MUTATOR_FUEL_LEVEL, themeType)}
style={{ float: "left" }}
className={classes.avatarIcon}
/>
</Grid>
<Grid style={{ paddingLeft: 5 }}>
<Typography variant="caption" color="textPrimary">
<span>
<span style={{ color: getMutationColour(pond.Mutator.MUTATOR_FUEL_LEVEL) }}>
{(lastFuelCalc.value / 10).toFixed(2) +
getMutationUnit(pond.Mutator.MUTATOR_FUEL_LEVEL)}
</span>
</span>
</Typography>
</Grid>
</Grid>
);
}
return (
<Grid container direction="row" wrap="nowrap" alignContent="center" alignItems="center">
<Grid>
<Avatar
variant="square"
alt="icon"
src={getMutationIcon(pond.Mutator.MUTATOR_FUEL_LEVEL, themeType)}
style={{ float: "left" }}
className={classes.avatarIcon}
/>
</Grid>
<Grid style={{ paddingLeft: 5 }}>
No Recent Data
</Grid>
</Grid>
);
};
//content of the card of there is no device linked to the heater
const unlinkedCard = () => {
return (
<React.Fragment>
<CardContent style={{ paddingLeft: 30, paddingRight: 30 }}>
<Grid
container
direction="row"
alignContent="center"
alignItems="center">
<Grid size={4}>
<EventBlocker>
<Button
variant="contained"
onClick={() => {
linkDevice(heater, heaterData.devices);
}}>
<Link />
</Button>
</EventBlocker>
</Grid>
<Grid size={7}>
<Typography>{heater.name}</Typography>
</Grid>
<Grid size={1}>
<ObjectHeaterIcon />
</Grid>
</Grid>
</CardContent>
</React.Fragment>
);
};
//content of the card if a device is linked to the heater
const linkedCard = (device: Device) => {
const productIcon = GetDeviceProductIcon(device);
return (
<React.Fragment>
<Box height="100%" display="flex" alignItems="center" flexWrap="nowrap" padding={2}>
<ListItemAvatar className={classes.listAvatar}>
<Avatar
variant="square"
src={productIcon}
className={classes.avatarImage}
alt="product"
/>
</ListItemAvatar>
<Grid container direction="column">
<Grid>
<ListItemText
primary={heater.name}
primaryTypographyProps={{
className: classes.textOverflow,
variant: "subtitle1"
}}
className={classes.listText}
/>
</Grid>
<Grid>
<ListItemText
primary={"Device: " + device.id()}
primaryTypographyProps={{
className: classes.textOverflow,
variant: "subtitle2"
}}
className={classes.listText}
/>
</Grid>
<Grid>
<ListItemText
primary={"Last Checked In: " + moment(device.status.lastActive).fromNow()}
primaryTypographyProps={{
className: classes.textOverflow,
variant: "subtitle2"
}}
className={classes.listText}
/>
</Grid>
</Grid>
</Box>
{allComponentsRecent ? (
<CardContent>
<Grid
container
direction="row"
alignContent="center"
alignItems="center">
<Grid>{componentDisplay(heaterComponent)}</Grid>
<Grid>{componentDisplay(heaterTempComponent)}</Grid>
<Grid>{componentDisplay(siteTempComponent)}</Grid>
<Grid>{fuelMutationDisplay()}</Grid>
</Grid>
</CardContent>
) : (
<Box
display="flex"
justifyContent="center"
alignItems="center"
style={{ backgroundColor: "orange", padding: 10, borderRadius: "0px 0px 10px 10px" }}>
A Component Is Not Reporting
</Box>
)}
</React.Fragment>
);
};
return (
<Card raised onClick={goToHeater} style={{ margin: 5 }}>
{connectedDevice ? linkedCard(connectedDevice) : unlinkedCard()}
</Card>
);
}

214
src/pages/Site.tsx Normal file
View file

@ -0,0 +1,214 @@
import { Card, Grid2 as Grid, Theme, Typography } from "@mui/material";
import DeviceLinkDrawer from "common/DeviceLinkDrawer";
import MapCard from "common/MapCard";
import ObjectControls from "common/ObjectControls";
import { useUserAPI, useMobile } from "hooks";
import SiteActions from "jobsite/SiteActions";
import { Device, Site as ISite, siteScope, teamScope } from "models";
import { ObjectHeater } from "models/ObjectHeater";
import ObjectHeaterCard from "objectHeater/ObjectHeaterCard";
import { pond } from "protobuf-ts/pond";
import { useGlobalState, useObjectHeaterAPI, useSiteAPI } from "providers";
import React, { useCallback, useEffect, useState } from "react";
//import { Redirect, useHistory, useRouteMatch } from "react-router";
import ObjectUsers from "user/ObjectUsers";
import PageContainer from "./PageContainer";
import { useNavigate, useParams } from "react-router-dom";
import { makeStyles } from "@mui/styles";
const useStyles = makeStyles((theme: Theme) => ({
card: {
padding: theme.spacing(1),
height: 520,
overflowY: "scroll"
}
})
);
export default function Site() {
//const location = useLocation();
//const match = useRouteMatch<MatchParams>();
//const siteKey = match.params.siteID;
const siteKey = useParams<{ siteKey: string }>()?.siteKey ?? "";
//const history = useHistory();
const navigate = useNavigate()
const classes = useStyles();
const [site, setSite] = useState<ISite>();
const [devices, setDevices] = useState<Map<string, pond.ComprehensiveDevice>>(
new Map<string, pond.ComprehensiveDevice>()
);
const [heaters, setHeaters] = useState<pond.ObjectHeaterData[]>([]);
//const deviceAPI = useDeviceAPI();
const userAPI = useUserAPI();
const siteAPI = useSiteAPI();
const [{ as, user }] = useGlobalState();
const [openLinkDrawer, setOpenLinkDrawer] = useState(false);
const [permissions, setPermissions] = useState<pond.Permission[]>([]);
const [loadingSite, setLoadingSite] = useState(false);
const isMobile = useMobile();
const [heaterToLink, setHeaterToLink] = useState<ObjectHeater>();
const objectHeaterAPI = useObjectHeaterAPI();
const loadSitePageData = useCallback(() => {
if (!loadingSite) {
setLoadingSite(true);
siteAPI
.getSitePage(siteKey)
.then(resp => {
setSite(ISite.any(resp.data.site));
setHeaters(resp.data.heaterData);
setLoadingSite(false);
})
.catch(err => {
console.log(err);
});
}
}, [siteAPI]); //eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
loadSitePageData();
let scope = siteScope(siteKey);
if (as) {
scope = teamScope(as);
}
userAPI.getUser(user.id(), scope).then(resp => {
setPermissions(resp.permissions);
});
}, [as, user, userAPI, loadSitePageData]); //eslint-disable-line react-hooks/exhaustive-deps
const goToMap = () => {
navigate("/constructionsitemap", { state: {
long: site?.longitude(),
lat: site?.latitude()
}
});
};
const mobileView = () => {
return (
<React.Fragment>
{heaters.map(h => (
<ObjectHeaterCard
key={h.heater?.key}
heaterData={h}
linkDevice={(heater, linkedDevices) => {
setHeaterToLink(heater);
setOpenLinkDrawer(true);
let d = new Map<string, pond.ComprehensiveDevice>();
linkedDevices.forEach(compDev => {
let device = Device.any(compDev.device);
d.set(device.id().toString(), compDev);
});
setDevices(d);
}}
/>
))}
</React.Fragment>
);
};
const desktopView = () => {
return (
<React.Fragment>
<Grid container direction="row" spacing={2} style={{ padding: 10 }}>
<Grid size={4}>
<Card className={classes.card}>
{heaters.map(h => (
<ObjectHeaterCard
key={h.heater?.key}
heaterData={h}
linkDevice={(heater, linkedDevices) => {
setHeaterToLink(heater);
setOpenLinkDrawer(true);
let d = new Map<string, pond.ComprehensiveDevice>();
linkedDevices.forEach(compDev => {
let device = Device.any(compDev.device);
d.set(device.id().toString(), compDev);
});
setDevices(d);
}}
/>
))}
</Card>
</Grid>
<Grid size={4}>
{site && (
<MapCard
key={"map"}
path={[{ latitude: site.latitude(), longitude: site.longitude(), timestamp: "" }]}
/>
)}
</Grid>
<Grid size={4}>
<Card className={classes.card}>
{site && (
<ObjectUsers
scope={siteScope(site.key())}
label={"Users For " + site.siteName()}
permissions={permissions}
isDialogOpen={true}
refreshCallback={() => {}}
closeDialogCallback={() => {}}
cardMode
/>
)}
</Card>
</Grid>
</Grid>
</React.Fragment>
);
};
return (
<PageContainer>
<DeviceLinkDrawer
open={openLinkDrawer}
close={() => {
setOpenLinkDrawer(false);
}}
linkedDevices={devices}
updateLinkedDevices={(dev, linked) => {
let deviceID = dev.device?.settings?.deviceId;
if (deviceID && heaterToLink) {
if (linked) {
objectHeaterAPI
.updateLink(heaterToLink.key, "objectHeater", deviceID.toString(), "device", [
"read",
"write",
"grant",
"revoke"
])
.then(resp => {
if (deviceID) {
devices.set(deviceID.toString(), dev);
setOpenLinkDrawer(false);
loadSitePageData();
}
});
}
}
}}
/>
<ObjectControls
actions={
<SiteActions
site={site ?? ISite.create()}
permissions={permissions}
refreshCallback={() => {}}
/>
}
mapFunction={goToMap}
// linkDeviceFunction={() => {
// setOpenLinkDrawer(true);
// }}
teamScope={isMobile && site ? siteScope(site.key()) : undefined}
permissions={permissions}
/>
<Typography variant="h5" style={{ margin: 10, fontWeight: 650 }}>
{site?.siteName()}
</Typography>
{isMobile ? mobileView() : desktopView()}
</PageContainer>
);
}

306
src/pages/Sites.tsx Normal file
View file

@ -0,0 +1,306 @@
import { Avatar, Box, Card, Grid2 as Grid, Switch, Tab, Tabs, Tooltip, Typography } from "@mui/material";
import { Group } from "@mui/icons-material";
//import MaterialTable from "material-table";
import { Site, User } from "models";
import { pond } from "protobuf-ts/pond";
import { useGlobalState, useSiteAPI } from "providers";
import React, { useCallback, useEffect, useState } from "react";
import PageContainer from "./PageContainer";
//import { getTableIcons } from "common/ResponsiveTable";
//import { useHistory } from "react-router";
import { useMobile } from "hooks";
import JobsiteIcon from "products/Construction/JobSiteIcon";
import ResponsiveTable, { Column } from "common/ResponsiveTable";
import { useNavigate } from "react-router-dom";
interface SiteWithData {
site: Site;
users: User[];
heaterData: pond.ObjectHeaterData[];
}
interface TabPanelProps {
children?: React.ReactNode;
index: any;
value: any;
}
export default function Sites() {
const siteAPI = useSiteAPI();
const [loading, setLoading] = useState(false);
const [{ as }] = useGlobalState();
//const history = useHistory();
const navigate = useNavigate()
const [sites, setSites] = useState<SiteWithData[]>([]);
const isMobile = useMobile();
const [currentTab, setCurrentTab] = useState(0);
const [tablePage, setTablePage] = useState(0);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0)
const loadSites = useCallback(() => {
if (loading) return;
setLoading(true);
//make a site api that gets an array of objects that contain the site, its devices, and teams
siteAPI
.listSitesPageData(pageSize, tablePage * pageSize, "asc", "siteName", undefined, as)
.then(resp => {
console.log(resp)
setTotal(resp.data.total);
buildTableData(resp.data.sites);
})
.finally(() => {
setLoading(false);
});
}, [as, siteAPI, pageSize, tablePage]); //eslint-disable-line react-hooks/exhaustive-deps
const buildTableData = (siteData: pond.SiteData[]) => {
let data: SiteWithData[] = [];
siteData.forEach(site => {
if (site.site?.settings?.siteName) {
let newTableData: SiteWithData = {
site: Site.any(site.site),
users: [],
heaterData: []
};
if (site.users) {
newTableData.users = site.users.map(u => User.any(u));
}
if (site.heaterData) {
newTableData.heaterData = site.heaterData;
}
data.push(newTableData);
}
});
setSites([...data]);
};
useEffect(() => {
loadSites();
}, [loadSites]);
const siteInfoDisplay = (site: SiteWithData) => {
return (
<Grid container direction="column">
<Grid>
<Typography style={{ fontWeight: 650 }}>{site.site.siteName()}</Typography>
</Grid>
<Grid>
{site.heaterData.length + (site.heaterData.length > 1 ? " Heaters" : " Heater")}
</Grid>
</Grid>
);
};
const goToSite = (jobsite: SiteWithData) => {
// let path = "/jobsites/" + jobsite.site.settings.key;
// history.push(path, {});
let path = "/jobsites/" + jobsite.site.key();
navigate(path, { state: jobsite.site })
};
const updateActive = (active: boolean, site: Site) => {
site.settings.active = active;
siteAPI
.updateSite(site.key(), site.settings)
.then(resp => {})
.catch(err => {});
setSites([...sites]);
};
const desktopColumns = (): Column<SiteWithData>[] => {
return ([
{
title: "Site",
render: row => (
<Box padding={2}>
<Typography>
{row.site.siteName()}
</Typography>
</Box>
)
},
{
title: "Heaters",
render: row => (
<Grid container spacing={1}>
{row.heaterData.map((hd: pond.ObjectHeaterData) => (
<Grid key={hd.heater?.key}>
{hd.heater?.name}
</Grid>
))}
</Grid>
)
},
{
title: "Users",
render: row => (
<Grid container spacing={1}>
{row.users.map((user: User) => (
<Grid key={user.id()}>
<Tooltip title={user.name()}>
{user.settings.avatar ? (
<Avatar alt={user.settings.name} src={user.settings.avatar} />
) : (
<Avatar alt={user.settings.name}>
<Group />
</Avatar>
)}
</Tooltip>
</Grid>
))}
</Grid>
)
},
{
title: "Active",
render: row => (
<React.Fragment>
<Switch
checked={row.site.settings.active}
onClick={e => {
e.stopPropagation();
}}
onChange={e => {
updateActive(e.target.checked, row.site);
}}
/>
</React.Fragment>
),
}
])
}
const handleTablePage = (event: any) => {
setPageSize(event.target.value);
};
const desktopView = () => {
return (
<ResponsiveTable<SiteWithData>
title={"Jobsites"}
page={tablePage}
pageSize={pageSize}
columns={desktopColumns}
setPage={(newPage) => {setTablePage(newPage)}}
total={total}
rows={sites}
handleRowsPerPageChange={handleTablePage}
onRowClick={goToSite}
/>
)
};
function TabPanelMine(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
aria-labelledby={`simple-tab-${index}`}
{...other}>
{value === index && <React.Fragment>{children}</React.Fragment>}
</div>
);
}
const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
setCurrentTab(newValue);
};
const mobileView = () => {
return (
<React.Fragment>
<Tabs
value={currentTab}
indicatorColor="primary"
textColor="primary"
onChange={handleChange}
centered
style={{
marginLeft: "auto"
}}>
<Tab label="Active" />
<Tab label="Inactive" />
</Tabs>
<TabPanelMine value={currentTab} index={0}>
{sites.map(site => {
let siteCards: JSX.Element[] = [];
if (site.site.settings.active) {
siteCards.push(
<Card
key={site.site.key()}
style={{ margin: 10, padding: 5 }}
onClick={() => {
goToSite(site);
}}>
<Grid container alignContent="center" alignItems="center">
<Grid size={2} style={{ textAlign: "center" }}>
<JobsiteIcon size={50} />
</Grid>
<Grid size={7}>
{/* information (site name, number of devices, teams in site) */}
{siteInfoDisplay(site)}
</Grid>
<Grid size={3}>
<Switch
defaultChecked={site.site.settings.active}
onClick={e => {
e.stopPropagation();
}}
onChange={e => {
updateActive(e.target.checked, site.site);
}}
/>
</Grid>
</Grid>
</Card>
);
}
return siteCards;
})}
</TabPanelMine>
<TabPanelMine value={currentTab} index={1}>
{sites.map(site => {
let siteCards: JSX.Element[] = [];
if (!site.site.settings.active) {
siteCards.push(
<Card
key={site.site.key()}
style={{ margin: 10, padding: 5 }}
onClick={() => {
goToSite(site);
}}>
<Grid container alignContent="center" alignItems="center">
<Grid size={2} style={{ textAlign: "center" }}>
<JobsiteIcon size={50} />
</Grid>
<Grid size={7}>
{/* information (site name, number of devices, teams in site) */}
{siteInfoDisplay(site)}
</Grid>
<Grid size={3}>
<Switch
defaultChecked={site.site.settings.active}
onClick={e => {
e.stopPropagation();
}}
onChange={e => {
updateActive(e.target.checked, site.site);
}}
/>
</Grid>
</Grid>
</Card>
);
}
return siteCards;
})}
</TabPanelMine>
</React.Fragment>
);
};
return <PageContainer>{isMobile ? mobileView() : desktopView()}</PageContainer>;
}

View file

@ -38,8 +38,8 @@ export {
// useKeyManagerAPI, // useKeyManagerAPI,
useTerminalAPI, //TODO: update api with resolve,reject useTerminalAPI, //TODO: update api with resolve,reject
useGateAPI, useGateAPI,
// useObjectHeaterAPI, useObjectHeaterAPI, //TODO: update api with resolve,reject
// useMutationAPI, useMutationAPI, //TODO: update api with resolve,reject
useGrainBagAPI, useGrainBagAPI,
useTransactionAPI, useTransactionAPI,
useFileControllerAPI useFileControllerAPI

View file

@ -0,0 +1,194 @@
import { AxiosResponse } from "axios";
import { useHTTP } from "hooks";
import { pond } from "protobuf-ts/pond";
import { useGlobalState } from "providers";
import React, { createContext, PropsWithChildren, useContext } from "react";
import { or } from "utils";
import { pondURL } from "./pond";
export interface IObjectHeaterAPIContext {
addObjectHeater: (
name: string,
heater: pond.ObjectHeaterSettings
) => Promise<AxiosResponse<pond.AddObjectHeaterResponse>>;
getObjectHeater: (key: string) => Promise<AxiosResponse<pond.GetObjectHeaterResponse>>;
listObjectHeaters: (
limit: number,
offset: number,
order?: "asc" | "desc",
orderBy?: string,
search?: string,
asRoot?: boolean,
as?: string,
keys?: string[],
types?: string[],
numerical?: boolean
) => Promise<AxiosResponse<pond.ListObjectHeatersResponse>>;
removeObjectHeater: (key: string) => Promise<AxiosResponse<pond.RemoveObjectHeaterResponse>>;
updateObjectHeater: (
key: string,
name: string,
settings: pond.ObjectHeaterSettings,
asRoot?: true
) => Promise<AxiosResponse<pond.UpdateObjectHeaterResponse>>;
updateLink: (
parentKey: string,
parentType: string,
objectID: string,
objectType: string,
permissions: string[]
) => Promise<any>;
listObjectHeatersPageData: (
limit: number,
offset: number,
order?: "asc" | "desc",
orderBy?: string,
search?: string,
as?: string,
asRoot?: boolean
) => Promise<AxiosResponse<pond.ListObjectHeatersPageDataResponse>>;
}
export const ObjectHeaterAPIContext = createContext<IObjectHeaterAPIContext>(
{} as IObjectHeaterAPIContext
);
interface Props {}
export default function ObjectHeaterProvider(props: PropsWithChildren<Props>) {
const { children } = props;
const { get, del, post, put } = useHTTP();
const [{ as }] = useGlobalState();
const addObjectHeater = (name: string, settings: pond.ObjectHeaterSettings) => {
if (as)
return post<pond.AddObjectHeaterResponse>(
pondURL("/objectHeaters?name=" + name + "&as=" + as),
settings
);
return post<pond.AddObjectHeaterResponse>(pondURL("/objectHeaters?name=" + name), settings);
};
const getObjectHeater = (key: string) => {
if (as) {
return get<pond.GetObjectHeaterResponse>(pondURL("/objectHeaters/" + key + "?as=" + as));
}
return get<pond.GetObjectHeaterResponse>(pondURL("/objectHeaters/" + key));
};
const removeObjectHeater = (key: string) => {
if (as)
return del<pond.RemoveObjectHeaterResponse>(pondURL("/objectHeaters/" + key + "?as=" + as));
return del<pond.RemoveObjectHeaterResponse>(pondURL("/objectHeaters/" + key));
};
const listObjectHeaters = (
limit: number,
offset: number,
order?: "asc" | "desc",
orderBy?: string,
search?: string,
asRoot?: boolean,
as?: string,
keys?: string[],
types?: string[],
numerical?: boolean
) => {
return get<pond.ListObjectHeatersResponse>(
pondURL(
"/objectHeaters" +
"?limit=" +
limit +
"&offset=" +
offset +
("&order=" + (order ? order : "asc")) +
("&by=" + (orderBy ? orderBy : "key")) +
(search ? "&search=" + search : "") +
(numerical ? "&numerical=true" : "") +
(asRoot ? "&asRoot=" + asRoot.toString() : "") +
(as ? "&as=" + as : "") +
(keys ? "&keys=" + keys.toString() : "") +
(types ? "&types=" + types.toString() : "")
)
);
};
const updateObjectHeater = (key: string, name: string, settings: pond.ObjectHeaterSettings) => {
if (as) {
return put<pond.UpdateObjectHeaterResponse>(
pondURL("/objectHeaters/" + key + "?as=" + as + "&name=" + name),
settings
);
}
return put<pond.UpdateObjectHeaterResponse>(
pondURL("/objectHeaters/" + key + "?name=" + name),
settings
);
};
const updateLink = (
parentID: string,
parentType: string,
objectID: string,
objectType: string,
permissions: string[]
) => {
if (as)
return post(pondURL(`/objectHeaters/` + parentID + `/link?as=${as}`), {
Key: objectID,
Type: objectType,
Parent: parentID,
ParentType: parentType,
Permissions: permissions
});
return post(pondURL(`/objectHeaters/` + parentID + `/link`), {
Key: objectID,
Type: objectType,
Parent: parentID,
ParentType: parentType,
Permissions: permissions
});
};
const listObjectHeatersPageData = (
limit: number,
offset: number,
order?: "asc" | "desc",
orderBy?: string,
search?: string,
as?: string,
asRoot?: boolean
) => {
return get<pond.ListObjectHeatersPageDataResponse>(
pondURL(
"/objectHeatersWithData" +
"?limit=" +
limit +
"&offset=" +
offset +
("&order=" + or(order, "asc")) +
("&by=" + or(orderBy, "key")) +
(as ? "&as=" + as : "") +
(asRoot ? "&asRoot=" + asRoot.toString() : "") +
(search ? "&search=" + search : "")
)
);
};
return (
<ObjectHeaterAPIContext.Provider
value={{
addObjectHeater,
getObjectHeater,
listObjectHeaters,
removeObjectHeater,
updateObjectHeater,
updateLink,
listObjectHeatersPageData
}}>
{children}
</ObjectHeaterAPIContext.Provider>
);
}
export const useObjectHeaterAPI = () => useContext(ObjectHeaterAPIContext);

View file

@ -0,0 +1,48 @@
import { useHTTP } from "hooks";
import { createContext, PropsWithChildren, useContext } from "react";
import { pond } from "protobuf-ts/pond";
import { pondURL } from "./pond";
import { AxiosResponse } from "axios";
import { useGlobalState } from "providers";
export interface IMutationAPIContext {
linearMutation: (
linearMutation: pond.LinearMutation,
start: string,
end: string
) => Promise<AxiosResponse<pond.LinearMutationResponse>>;
}
export const MutationAPIContext = createContext<IMutationAPIContext>({} as IMutationAPIContext);
interface Props {}
export default function MutationProvider(props: PropsWithChildren<Props>) {
const { children } = props;
const { post } = useHTTP();
//const permissionAPI = usePermissionAPI();
const [{ as }] = useGlobalState();
const linearMutation = (linearMutation: pond.LinearMutation, start: string, end: string) => {
if (as)
return post<pond.LinearMutationResponse>(
pondURL("/linearMutation?start=" + start + "&end=" + end + "&as=" + as),
linearMutation
);
return post<pond.LinearMutationResponse>(
pondURL("/linearMutation?start=" + start + "&end=" + end),
linearMutation
);
};
return (
<MutationAPIContext.Provider
value={{
linearMutation
}}>
{children}
</MutationAPIContext.Provider>
);
}
export const useMutationAPI = () => useContext(MutationAPIContext);

View file

@ -26,6 +26,8 @@ import HomeMarkerProvider, {useHomeMarkerAPI} from "./homeMarkerAPI";
import HarvestPlanProvider, {useHarvestPlanAPI} from "./harvestPlanAPI"; import HarvestPlanProvider, {useHarvestPlanAPI} from "./harvestPlanAPI";
import TerminalProvider, {useTerminalAPI} from "./terminalAPI" import TerminalProvider, {useTerminalAPI} from "./terminalAPI"
import SiteProvider, {useSiteAPI} from "./siteAPI"; import SiteProvider, {useSiteAPI} from "./siteAPI";
import ObjectHeaterProvider, {useObjectHeaterAPI} from "./ObjectHeaterAPI";
import MutationProvider, {useMutationAPI} from "./mutationAPI"
// import NoteProvider from "providers/noteAPI"; // import NoteProvider from "providers/noteAPI";
export const pondURL = (partial: string, demo: boolean = false): string => { export const pondURL = (partial: string, demo: boolean = false): string => {
@ -68,7 +70,11 @@ export default function PondProvider(props: PropsWithChildren<any>) {
<HarvestPlanProvider> <HarvestPlanProvider>
<TerminalProvider> <TerminalProvider>
<SiteProvider> <SiteProvider>
<ObjectHeaterProvider>
<MutationProvider>
{children} {children}
</MutationProvider>
</ObjectHeaterProvider>
</SiteProvider> </SiteProvider>
</TerminalProvider> </TerminalProvider>
</HarvestPlanProvider> </HarvestPlanProvider>
@ -123,5 +129,7 @@ export {
useHomeMarkerAPI, useHomeMarkerAPI,
useHarvestPlanAPI, useHarvestPlanAPI,
useTerminalAPI, useTerminalAPI,
useSiteAPI useSiteAPI,
useObjectHeaterAPI,
useMutationAPI
}; };