frontend/src/pages/Gate.tsx
2025-06-11 13:47:02 -06:00

477 lines
16 KiB
TypeScript

import { Gate as IGate } from "models/Gate";
//import { MatchParams } from "navigation/Routes";
//import { Redirect, useHistory, useRouteMatch } from "react-router";
import React, { useCallback, useEffect, useState } from "react";
import PageContainer from "./PageContainer";
import { useGlobalState, useGateAPI, useUserAPI } from "providers";
import {
Box,
ButtonBase,
Card,
CircularProgress,
Drawer,
Grid2 as Grid,
IconButton,
MenuItem,
Tab,
Tabs,
Theme,
Typography
} from "@mui/material";
import { pond } from "protobuf-ts/pond";
import DeviceLinkDrawer from "common/DeviceLinkDrawer";
import { Component, Device, Scope } from "models";
import GateActions from "gate/GateActions";
import GateDevice from "gate/GateDevice";
import ObjectControls from "common/ObjectControls";
import { Link } from "@mui/icons-material";
import Chat from "chat/Chat";
import PlaneIcon from "products/AviationIcons/PlaneIcon";
import NotesIcon from "@mui/icons-material/Notes";
import { useMobile, useSnackbar, useThemeType } from "hooks";
import { clone } from "lodash";
import { useNavigate, useParams } from "react-router-dom";
import { makeStyles } from "@mui/styles";
interface TabPanelProps {
children?: React.ReactNode;
index: any;
value: any;
}
function TabPanelMine(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
aria-labelledby={`simple-tab-${index}`}
{...other}>
{value === index && <React.Fragment>{children}</React.Fragment>}
</div>
);
}
interface Props {
gateKey?: string;
useMobile?: boolean;
}
const useStyles = makeStyles((theme: Theme) => ({
//const themeType = theme.palette.type;
inactiveButton: {
color: useThemeType() === "light" ? theme.palette.common.black : theme.palette.common.white,
backgroundColor: "transparent",
width: theme.spacing(5),
height: theme.spacing(5),
border: "1px solid",
borderColor: theme.palette.divider
},
activeButton: {
color: useThemeType() === "light" ? theme.palette.common.black : theme.palette.common.white,
backgroundColor: theme.palette.primary.main,
width: theme.spacing(5),
height: theme.spacing(5),
border: "1px solid",
borderColor: theme.palette.divider
},
drawerPaper: {
height: "100%",
width: "30%"
}
}));
export default function Gate(props: Props) {
//const match = useRouteMatch<MatchParams>();
//const gateID = props.gateID ?? match.params.gateID;
const { gateKey } = props
const gateID = gateKey ?? useParams<{ gateKey: string }>()?.gateKey ?? "";
const classes = useStyles();
const gateAPI = useGateAPI();
const userAPI = useUserAPI();
const [gate, setGate] = useState<IGate>(IGate.create());
const [devices, setDevices] = useState<Map<string, pond.ComprehensiveDevice>>(
new Map<string, pond.ComprehensiveDevice>()
);
const [components, setComponents] = useState<Component[]>([]);
const [openDeviceDrawer, setOpenDeviceDrawer] = useState(false);
const [loadingGate, setLoadingGate] = useState(false);
const [{ user, as }] = useGlobalState();
const [permissions, setPermissions] = useState<pond.Permission[]>([]);
const [tabVal, setTabVal] = useState(0);
//const history = useHistory();
const navigate = useNavigate();
const [displayTab, setDisplayTab] = useState(0);
const [openNoteDrawer, setOpenNoteDrawer] = useState(false);
const isMobile = useMobile();
const [devPrefs, setDevPrefs] = useState<Map<number, number>>(new Map<number, number>());
const { openSnack } = useSnackbar();
const [invalid, setInvalid] = useState(false);
const goToMap = () => {
navigate("/aviationMap", { state: { long: gate.longitude(), lat:gate.latitude() }})
};
useEffect(() => {
let key = gateID;
let kind = "gate";
if (as) {
key = as;
kind = "team";
}
userAPI.getUser(user.id(), { key: key, kind: kind } as Scope).then(resp => {
setPermissions(resp.permissions);
});
}, [as, gateID, userAPI, user]);
const loadGate = useCallback(() => {
let id = gateID;
setTabVal(0);
if (loadingGate || id === undefined || id === "") return;
setLoadingGate(true);
gateAPI
.getGatePageData(id, as)
.then(resp => {
//console.log(resp.data);
let p = new Map<number, pond.GateDeviceType>();
Object.keys(resp.data.preferences).forEach(k => {
let prefKey = parseInt(k);
p.set(
prefKey,
pond.GatePreferences.fromObject(resp.data.preferences[k]).gateDevice ??
pond.GateDeviceType.GATE_DEVICE_TYPE_UNKNOWN
);
});
setDevPrefs(p);
if (resp.data.gate) {
setGate(IGate.any(resp.data.gate));
}
if (resp.data.linkedDevices) {
let devMap = new Map<string, pond.ComprehensiveDevice>();
resp.data.linkedDevices.forEach(dev => {
if (dev.device?.settings?.deviceId) {
devMap.set(dev.device.settings.deviceId.toString(), dev);
}
});
setDevices(devMap);
}
if (resp.data.linkedComponents) {
setComponents(resp.data.linkedComponents.map(c => Component.any(c)));
}
})
.catch(err => {
setInvalid(true);
})
.finally(() => {
setLoadingGate(false);
});
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [gateAPI, gateID, as]);
useEffect(() => {
loadGate();
}, [loadGate]);
const updateGateDevicePrefs = (deviceID: number, newPref: pond.GateDeviceType) => {
gateAPI
.updatePrefs(gate.key, "device", deviceID.toString(), newPref, [gate.key], ["gate"], as)
.then(resp => {
openSnack("Updated gate device type");
// have to do the clone method to force the select box to update
let newPrefMap = clone(devPrefs);
newPrefMap.set(deviceID, newPref);
setDevPrefs(newPrefMap);
})
.catch(err => {
openSnack("Failed to update device type");
});
};
const deviceDrawer = () => {
return (
<DeviceLinkDrawer
deviceTags={["omniair"]}
devicePrefMap={devPrefs}
prefOptions={[
<MenuItem
key={pond.GateDeviceType.GATE_DEVICE_TYPE_UNKNOWN}
value={pond.GateDeviceType.GATE_DEVICE_TYPE_UNKNOWN}>
None
</MenuItem>,
<MenuItem
key={pond.GateDeviceType.GATE_DEVICE_TYPE_PCA}
value={pond.GateDeviceType.GATE_DEVICE_TYPE_PCA}>
PCA Unit
</MenuItem>
]}
devicePrefChanged={(device, pref) => {
updateGateDevicePrefs(device.id(), pref);
}}
open={openDeviceDrawer}
close={() => {
setOpenDeviceDrawer(false);
}}
linkedDevices={devices}
linkedComponents={components}
updateLinkedDevices={(device, linked) => {
let devMap = devices;
let id = device.device?.settings?.deviceId;
if (id) {
if (linked) {
gateAPI
.updateLink(gateID, "gate", id.toString(), "device", [
"read",
"write",
"grant",
"revoke"
], as)
.then(resp => {
if (id) {
devMap.set(id.toString(), device);
setTabVal(id);
}
setDevices(devMap);
})
.catch(err => {
console.log("error linking device");
});
} else {
gateAPI.updateLink(gateID, "gate", id.toString(), "device", [], as).then(resp => {
if (id) {
devMap.delete(id.toString());
if (tabVal === id) {
let firstEntry = Array.from(devMap.values())[0];
if (firstEntry) {
setTabVal(firstEntry.device?.settings?.deviceId ?? -1);
} else {
setTabVal(0);
}
}
}
setDevices(devMap);
});
}
}
}}
updateLinkedComponents={(deviceID, component, linked) => {
let c = components;
if (linked) {
gateAPI
.updateLink(gateID, "gate", deviceID + ":" + component.key(), "component", [
"read",
"write",
"grant",
"revoke"
],as)
.then(resp => {
c.push(component);
setComponents([...c]);
});
} else {
gateAPI
.updateLink(gateID, "gate", deviceID + ":" + component.key(), "component", [], as)
.then(resp => {
c.forEach((comp, i) => {
if (component.key() === comp.key()) {
c.splice(i, 1);
setComponents([...c]);
}
});
});
}
}}
/>
);
};
const gateDisplay = () => {
return (
<React.Fragment>
{loadingGate ? (
<Card>
<Box display="flex" justifyContent="center" alignItems="center" style={{ height: 300 }}>
<CircularProgress size={200} thickness={1.5} />
</Box>
</Card>
) : (
<React.Fragment>
<Card
style={{
//padding: isMobile ? 2 : 15,
marginLeft: isMobile ? 0 : 15,
marginRight: isMobile ? 0 : 15
}}>
<Tabs
style={{
marginBottom: 5,
display: Array.from(devices.keys()).length < 2 ? "none" : "block"
}}
value={tabVal}
indicatorColor="secondary"
//textColor="secondary"
onChange={(_, newVal) => {
setTabVal(newVal);
}}
aria-label="device tabs">
<Tab label={"Device Not Found"} value={-1} style={{ display: "none" }} />
<Tab label={"No Connected Devices"} value={0} style={{ display: "none" }} />
{Array.from(devices.values()).map(dev => {
let name = "Device Not Found";
let devKey = -1;
if (dev.device && dev.device.settings) {
name = dev.device.settings.name;
devKey = dev.device.settings.deviceId;
}
return <Tab key={devKey} label={name ?? devKey} value={devKey} />;
})}
</Tabs>
{/* panel to show if there are no devices */}
<TabPanelMine value={tabVal} index={0}>
<ButtonBase
onClick={() => {
setOpenDeviceDrawer(true);
}}
style={{ height: 150, width: "100%", margin: -15 }}>
<Grid
width="100%"
container
direction="row"
spacing={2}
alignItems="center"
alignContent="center">
<Grid>
<Box display="flex" justifyContent="center" alignItems="center">
<Link style={{ height: 50, width: 50 }} />
</Box>
</Grid>
<Grid>
<Typography style={{ fontSize: 25, fontWeight: 650 }}>
Connect Device
</Typography>
</Grid>
<Grid size={12}>
Click here to add a device to the gate. To add an additional device to a gate
use the "Link Device" icon on the top right side of this page
</Grid>
</Grid>
</ButtonBase>
</TabPanelMine>
{/* panel to show if there was an issue in the tab */}
<TabPanelMine value={tabVal} index={-1}>
Device Not Found
</TabPanelMine>
{Array.from(devices.values()).map(dev => {
let devKey = 0;
if (dev.device && dev.device.settings) {
devKey = dev.device.settings.deviceId;
if (tabVal === 0) {
setTabVal(devKey);
}
}
return (
<TabPanelMine key={devKey} value={tabVal} index={devKey}>
<GateDevice
key={devKey}
comprehensiveDevice={dev}
gate={gate}
linkedCompList={components}
drawerView={props.useMobile}
/>
</TabPanelMine>
);
})}
</Card>
</React.Fragment>
)}
</React.Fragment>
);
};
const noteDrawer = () => {
return (
<Drawer
open={openNoteDrawer}
onClose={() => {
setOpenNoteDrawer(false);
}}
anchor="right"
classes={{ paper: classes.drawerPaper }}>
<Box padding={2}>
<Typography>Notes</Typography>
<Card style={{ padding: 10 }}>
<Chat objectKey={gate.key} type={pond.NoteType.NOTE_TYPE_GATE} />
</Card>
</Box>
</Drawer>
);
};
return (
<PageContainer>
<ObjectControls
objectButton={
<IconButton
size="small"
style={{
marginTop: "0.3625rem",
marginBottom: "0.3625rem",
marginRight: "0.5rem"
}}
onClick={() => {
setDisplayTab(0);
}}
className={displayTab === 0 ? classes.activeButton : classes.inactiveButton}
component="span">
<PlaneIcon />
</IconButton>
}
linkDeviceFunction={() => {
setOpenDeviceDrawer(true);
}}
notesButton={
<IconButton
onClick={() => {
if (props.useMobile || isMobile) {
setDisplayTab(1);
} else {
setOpenNoteDrawer(true);
}
}}
size="small"
style={{
marginTop: "0.3625rem",
marginBottom: "0.3625rem",
marginRight: "0.5rem"
}}
className={displayTab === 1 ? classes.activeButton : classes.inactiveButton}
component="span">
<NotesIcon />
</IconButton>
}
mapFunction={() => {
goToMap();
}}
actions={<GateActions gate={gate} refreshCallback={loadGate} permissions={permissions} />}
permissions={permissions}
devices={Array.from(devices.values()).map(compDev => Device.any(compDev.device))}
/>
<Box marginLeft={2} marginTop={isMobile ? -1.5 : -1} marginBottom={isMobile ? 0 : 1}>
<Typography style={{ fontSize: 25, fontWeight: 650 }}>{gate.name}</Typography>
</Box>
<TabPanelMine value={displayTab} index={0}>
{gateDisplay()}
</TabPanelMine>
{/* tab for notes on mobile and the map drawer */}
<TabPanelMine value={displayTab} index={1}>
<Card style={{ padding: 10 }}>
<Chat objectKey={gate.key} type={pond.NoteType.NOTE_TYPE_GATE} />
</Card>
</TabPanelMine>
{/* drawer is for displaying notes on desktop */}
{noteDrawer()}
{deviceDrawer()}
</PageContainer>
);
}