added the sites page and all other required imports for it to work, also fixed the map card
This commit is contained in:
parent
c5afdc2c3d
commit
3990dfbf4f
14 changed files with 1595 additions and 21 deletions
2
package-lock.json
generated
2
package-lock.json
generated
|
|
@ -10770,7 +10770,7 @@
|
|||
},
|
||||
"node_modules/protobuf-ts": {
|
||||
"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": {
|
||||
"protobufjs": "^6.8.8"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ import { Beenhere } from "@mui/icons-material";
|
|||
import { useThemeType } from "hooks";
|
||||
import moment from "moment";
|
||||
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 {
|
||||
latitude: number;
|
||||
|
|
@ -79,27 +81,27 @@ export default function MapGL(props: Props) {
|
|||
);
|
||||
});
|
||||
});
|
||||
console.log(elems)
|
||||
return elems;
|
||||
};
|
||||
|
||||
let style =
|
||||
themeType === "dark" ? "mapbox://styles/mapbox/dark-v10" : "mapbox://styles/mapbox/light-v10";
|
||||
return (
|
||||
<ReactMapGL
|
||||
<Map
|
||||
mapStyle={style}
|
||||
initialViewState={viewport}
|
||||
//width="100%"
|
||||
//height="100%"
|
||||
mapboxAccessToken={MAPBOX_TOKEN}
|
||||
{...viewport}
|
||||
// onViewStateChange={(e: any) => {
|
||||
// setViewport({
|
||||
// longitude: e.viewState.longitude,
|
||||
// latitude: e.viewState.latitude,
|
||||
// zoom: e.viewState.zoom,
|
||||
// });
|
||||
// }}
|
||||
onMove={(e: any) => {
|
||||
setViewport({
|
||||
longitude: e.viewState.longitude,
|
||||
latitude: e.viewState.latitude,
|
||||
zoom: e.viewState.zoom,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{markers()}
|
||||
</ReactMapGL>
|
||||
</Map>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ const {terminals, currentTerminal } = props;
|
|||
const classes = useStyles();
|
||||
const [gateDialog, setGateDialog] = useState(false);
|
||||
const [tablePage, setTablePage] = useState(0)
|
||||
const [pageSize, setPageSize] = useState(5)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
const [total, setTotal] = useState(0)
|
||||
const [terminalMap, setTerminalMap] = useState<Map<string, string>>(new Map<string, string>());
|
||||
const gateAPI = useGateAPI();
|
||||
|
|
@ -77,11 +77,11 @@ const {terminals, currentTerminal } = props;
|
|||
setGates(resp.data.gates.map(gate => Gate.create(gate)))
|
||||
setTotal(resp.data.total)
|
||||
})
|
||||
},[currentTerminal, search])
|
||||
},[currentTerminal, search, pageSize, tablePage])
|
||||
|
||||
useEffect(() => {
|
||||
loadGates()
|
||||
},[currentTerminal, search])
|
||||
},[loadGates])
|
||||
|
||||
const handleChange = (event: any) => {
|
||||
setPageSize(event.target.value);
|
||||
|
|
@ -213,7 +213,7 @@ const {terminals, currentTerminal } = props;
|
|||
renderGutter={isMobile || props.useMobile ? gutter : undefined}
|
||||
gutterPadding={0}
|
||||
rows={gates}
|
||||
setPage={(page) => {console.log(page)}}
|
||||
setPage={(page) => {setTablePage(page)}}
|
||||
total={total}
|
||||
onRowClick={(gate) => {goToGate(gate)}}
|
||||
hideKeys={isMobile || props.useMobile}
|
||||
|
|
|
|||
319
src/jobsite/SiteActions.tsx
Normal file
319
src/jobsite/SiteActions.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
33
src/models/ObjectHeater.ts
Normal file
33
src/models/ObjectHeater.ts
Normal 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)));
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,9 @@ import Terminals from "pages/Terminals";
|
|||
import Gate from "pages/Gate";
|
||||
import AviationMap from "pages/AviationMap";
|
||||
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 DevicePage = lazy(() => import("pages/Device"));
|
||||
|
|
@ -51,6 +54,7 @@ export default function Router(props: Props) {
|
|||
<Route path="bins/*" element={<BinsRoute/>} />
|
||||
<Route path="mines/*" element={<MinesRoute/>} />
|
||||
<Route path="terminals/*" element={<TerminalGatesRoute />} />
|
||||
<Route path="jobsites/*" element={<JobsitesRoute />} />
|
||||
</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 = () => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import { useAuth0 } from "@auth0/auth0-react";
|
|||
import FieldsIcon from "products/AgIcons/FieldsIcon";
|
||||
import PlaneIcon from "products/AviationIcons/PlaneIcon";
|
||||
import AirportMapIcon from "products/AviationIcons/AirportMapIcon";
|
||||
import JobsiteIcon from "products/Construction/JobSiteIcon";
|
||||
|
||||
const drawerWidth = 230;
|
||||
|
||||
|
|
@ -265,6 +266,20 @@ export default function SideNavigator(props: Props) {
|
|||
</ListItemButton>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
409
src/objectHeater/ObjectHeaterCard.tsx
Normal file
409
src/objectHeater/ObjectHeaterCard.tsx
Normal 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
214
src/pages/Site.tsx
Normal 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
306
src/pages/Sites.tsx
Normal 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>;
|
||||
}
|
||||
|
|
@ -38,8 +38,8 @@ export {
|
|||
// useKeyManagerAPI,
|
||||
useTerminalAPI, //TODO: update api with resolve,reject
|
||||
useGateAPI,
|
||||
// useObjectHeaterAPI,
|
||||
// useMutationAPI,
|
||||
useObjectHeaterAPI, //TODO: update api with resolve,reject
|
||||
useMutationAPI, //TODO: update api with resolve,reject
|
||||
useGrainBagAPI,
|
||||
useTransactionAPI,
|
||||
useFileControllerAPI
|
||||
|
|
|
|||
194
src/providers/pond/ObjectHeaterAPI.tsx
Normal file
194
src/providers/pond/ObjectHeaterAPI.tsx
Normal 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);
|
||||
48
src/providers/pond/mutationAPI.tsx
Normal file
48
src/providers/pond/mutationAPI.tsx
Normal 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);
|
||||
|
|
@ -26,6 +26,8 @@ import HomeMarkerProvider, {useHomeMarkerAPI} from "./homeMarkerAPI";
|
|||
import HarvestPlanProvider, {useHarvestPlanAPI} from "./harvestPlanAPI";
|
||||
import TerminalProvider, {useTerminalAPI} from "./terminalAPI"
|
||||
import SiteProvider, {useSiteAPI} from "./siteAPI";
|
||||
import ObjectHeaterProvider, {useObjectHeaterAPI} from "./ObjectHeaterAPI";
|
||||
import MutationProvider, {useMutationAPI} from "./mutationAPI"
|
||||
// import NoteProvider from "providers/noteAPI";
|
||||
|
||||
export const pondURL = (partial: string, demo: boolean = false): string => {
|
||||
|
|
@ -68,7 +70,11 @@ export default function PondProvider(props: PropsWithChildren<any>) {
|
|||
<HarvestPlanProvider>
|
||||
<TerminalProvider>
|
||||
<SiteProvider>
|
||||
{children}
|
||||
<ObjectHeaterProvider>
|
||||
<MutationProvider>
|
||||
{children}
|
||||
</MutationProvider>
|
||||
</ObjectHeaterProvider>
|
||||
</SiteProvider>
|
||||
</TerminalProvider>
|
||||
</HarvestPlanProvider>
|
||||
|
|
@ -123,5 +129,7 @@ export {
|
|||
useHomeMarkerAPI,
|
||||
useHarvestPlanAPI,
|
||||
useTerminalAPI,
|
||||
useSiteAPI
|
||||
useSiteAPI,
|
||||
useObjectHeaterAPI,
|
||||
useMutationAPI
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue