explains better what the interactions will do and an option to add a pressure threshold that turns off the red light if it goes below, that option will change the redlight from having one toggle interaction to 4 set interactions
595 lines
18 KiB
TypeScript
595 lines
18 KiB
TypeScript
import {
|
|
DialogActions,
|
|
DialogContent,
|
|
DialogTitle,
|
|
MenuItem,
|
|
Select,
|
|
TextField,
|
|
Button,
|
|
Grid2 as Grid,
|
|
Box,
|
|
InputAdornment,
|
|
Stepper,
|
|
Step,
|
|
StepLabel,
|
|
Tabs,
|
|
Tab
|
|
} from "@mui/material";
|
|
import ResponsiveDialog from "common/ResponsiveDialog";
|
|
import { Terminal } from "models/Terminal";
|
|
import { Gate } from "models/Gate";
|
|
import { pond } from "protobuf-ts/pond";
|
|
import { useTerminalAPI, useGateAPI, useGlobalState } from "providers";
|
|
import React, { useEffect, useState } from "react";
|
|
import { useSnackbar } from "hooks";
|
|
//import { useHistory } from "react-router";
|
|
import { useNavigate } from "react-router-dom";
|
|
|
|
import DuctDescriber, { DuctOptions, FindDuctType } from "ducting/DuctDescriber";
|
|
|
|
interface DuctProps {
|
|
diameter: number;
|
|
length: number;
|
|
thermalConductivity: number;
|
|
thermalResistance: number;
|
|
frictionFactor: number;
|
|
}
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
close: (newGate?: Gate) => void;
|
|
terminals?: Terminal[];
|
|
gate?: Gate;
|
|
long?: number;
|
|
lat?: number;
|
|
}
|
|
|
|
const steps = ["Gate", "Duct"];
|
|
|
|
export default function GateSettings(props: Props) {
|
|
const { open, close, terminals, gate, long, lat } = props;
|
|
const [{as}] = useGlobalState();
|
|
const [gateName, setGateName] = useState("");
|
|
const [lowerFlowBound, setLowerFlowBound] = useState(0);
|
|
const [upperFlowBound, setUpperFlowBound] = useState(0);
|
|
const [terminalOptions, setTerminalOptions] = useState(terminals);
|
|
const [terminalKey, setTerminalKey] = useState("");
|
|
const gateAPI = useGateAPI();
|
|
const terminalAPI = useTerminalAPI();
|
|
const [loadingTerminals, setLoadingTerminals] = useState(false);
|
|
const [removeDialog, setRemoveDialog] = useState(false);
|
|
const { openSnack } = useSnackbar();
|
|
//const history = useHistory();
|
|
const navigate = useNavigate();
|
|
const ductOptions = DuctOptions();
|
|
const [ductType, setDuctType] = useState(0);
|
|
const [ductName, setDuctName] = useState("");
|
|
const [ductProps, setDuctProps] = useState<DuctProps>({
|
|
diameter: 0,
|
|
length: 0,
|
|
frictionFactor: 0,
|
|
thermalConductivity: 0,
|
|
thermalResistance: 0
|
|
});
|
|
const [pcaUnit, setPcaUnit] = useState("");
|
|
const [activeStep, setActiveStep] = useState(0);
|
|
//user set identifier to be shown on the marker on the map
|
|
const [terminalIdentifier, setTerminalIdentifier] = useState("");
|
|
const [gateIdentifier, setGateIdentifier] = useState("");
|
|
const [idleLimit, setIdleLimit] = useState<number>(0);
|
|
const [hourlyPCA, setHourlyPCA] = useState<number>(0);
|
|
const [hourlyAPU, setHourlyAPU] = useState<number>(0);
|
|
|
|
useEffect(() => {
|
|
if (!terminals) {
|
|
if (loadingTerminals) return;
|
|
setLoadingTerminals(true);
|
|
terminalAPI
|
|
.listTerminals(500, 0, undefined, undefined, undefined, as)
|
|
.then(resp => {
|
|
setTerminalOptions(resp.data.terminals.map(a => Terminal.any(a)));
|
|
})
|
|
.catch(err => {})
|
|
.finally(() => {
|
|
setLoadingTerminals(false);
|
|
});
|
|
} else {
|
|
setTerminalOptions(terminals);
|
|
}
|
|
//eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [terminals, terminalAPI, as]);
|
|
|
|
useEffect(() => {
|
|
if (gate) {
|
|
setGateName(gate.name);
|
|
setTerminalKey(gate.terminal());
|
|
setDuctProps({
|
|
diameter: gate.settings.ductDiameter,
|
|
length: gate.settings.ductLength,
|
|
frictionFactor: gate.settings.frictionFactor,
|
|
thermalConductivity: gate.settings.thermalConductivity,
|
|
thermalResistance: gate.settings.thermalResistance
|
|
});
|
|
let currentType = FindDuctType(
|
|
gate.settings.thermalResistance,
|
|
gate.settings.thermalConductivity,
|
|
gate.settings.frictionFactor
|
|
);
|
|
setDuctType(currentType);
|
|
setLowerFlowBound(gate.lowerFlow());
|
|
setUpperFlowBound(gate.upperFlow());
|
|
setPcaUnit(gate.settings.pcaType);
|
|
setDuctName(gate.settings.ductName);
|
|
setTerminalIdentifier(gate.settings.letterIdentifier ?? "");
|
|
setGateIdentifier(gate.settings.numberIdentifier ?? "");
|
|
setHourlyAPU(gate.settings.hourlyApuCost ?? 0);
|
|
setHourlyPCA(gate.settings.hourlyPcaCost ?? 0);
|
|
} else {
|
|
setGateName("");
|
|
setTerminalKey("");
|
|
}
|
|
}, [gate]);
|
|
|
|
const confirm = () => {
|
|
if (gate) {
|
|
let settings = gate.settings;
|
|
settings.terminal = terminalKey;
|
|
settings.ductDiameter = ductProps.diameter;
|
|
settings.ductLength = ductProps.length;
|
|
settings.frictionFactor = ductProps.frictionFactor;
|
|
settings.thermalConductivity = ductProps.thermalConductivity;
|
|
settings.thermalResistance = ductProps.thermalResistance;
|
|
settings.lowerFlow = lowerFlowBound;
|
|
settings.upperFlow = upperFlowBound;
|
|
settings.idleFlow = idleLimit;
|
|
settings.ductName = ductName;
|
|
settings.pcaType = pcaUnit;
|
|
settings.letterIdentifier = terminalIdentifier;
|
|
settings.numberIdentifier = gateIdentifier.toString();
|
|
settings.hourlyApuCost = hourlyAPU;
|
|
settings.hourlyPcaCost = hourlyPCA;
|
|
gateAPI
|
|
.updateGate(gate.key, gateName, settings, as)
|
|
.then(resp => {
|
|
openSnack("Gate update");
|
|
close();
|
|
})
|
|
.catch(err => {
|
|
openSnack("Failed to update gate");
|
|
});
|
|
} else {
|
|
let settings: pond.GateSettings = pond.GateSettings.create({
|
|
longitude: long,
|
|
latitude: lat,
|
|
terminal: terminalKey,
|
|
ductDiameter: ductProps.diameter,
|
|
ductLength: ductProps.length,
|
|
frictionFactor: ductProps.frictionFactor,
|
|
thermalConductivity: ductProps.thermalConductivity,
|
|
thermalResistance: ductProps.thermalResistance,
|
|
lowerFlow: lowerFlowBound,
|
|
upperFlow: upperFlowBound,
|
|
idleFlow: idleLimit,
|
|
ductName: ductName,
|
|
pcaType: pcaUnit,
|
|
letterIdentifier: terminalIdentifier,
|
|
numberIdentifier: gateIdentifier.toString(),
|
|
hourlyApuCost: hourlyAPU,
|
|
hourlyPcaCost: hourlyPCA
|
|
});
|
|
gateAPI
|
|
.addGate(gateName, settings, as)
|
|
.then(resp => {
|
|
openSnack("New gate added");
|
|
let newGate = Gate.create(
|
|
pond.Gate.create({ key: resp.data.key, name: gateName, settings: settings })
|
|
);
|
|
close(newGate);
|
|
})
|
|
.catch(err => {
|
|
openSnack("There was a problem adding your gate");
|
|
});
|
|
}
|
|
};
|
|
|
|
const remove = () => {
|
|
if (gate) {
|
|
gateAPI
|
|
.removeGate(gate.key, as)
|
|
.then(resp => {
|
|
openSnack("Gate Deleted");
|
|
})
|
|
.catch(err => {
|
|
openSnack("Failed to delete gate");
|
|
})
|
|
.finally(() => {
|
|
navigate("/terminals");
|
|
});
|
|
}
|
|
};
|
|
|
|
const stepper = () => {
|
|
if (gate === undefined) {
|
|
return (
|
|
<Stepper activeStep={activeStep}>
|
|
{steps.map(label => {
|
|
const stepProps: { completed?: boolean } = {};
|
|
const labelProps: {
|
|
optional?: React.ReactNode;
|
|
} = {};
|
|
return (
|
|
<Step key={label} {...stepProps}>
|
|
<StepLabel {...labelProps}>{label}</StepLabel>
|
|
</Step>
|
|
);
|
|
})}
|
|
</Stepper>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Box display="flex" justifyContent="center" width="100%">
|
|
<Tabs
|
|
value={activeStep}
|
|
onChange={(_, value) => setActiveStep(value)}
|
|
indicatorColor="primary"
|
|
textColor="primary"
|
|
variant="fullWidth"
|
|
aria-label="bin tabs"
|
|
//classes={{ root: classes.tabs }}
|
|
>
|
|
{steps.map((step, i) => (
|
|
<Tab key={i} label={step} aria-label={step} />
|
|
))}
|
|
</Tabs>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const stepperContent = (step: number) => {
|
|
switch (step) {
|
|
case 1:
|
|
return ductProperties();
|
|
default:
|
|
return gateProperties();
|
|
}
|
|
};
|
|
|
|
const gateProperties = () => {
|
|
//build list of letters for select box
|
|
let tiOptions: JSX.Element[] = [];
|
|
//adds the letters to the terminal id options
|
|
for (let l = 65; l <= 90; l++) {
|
|
tiOptions.push(
|
|
<MenuItem key={String.fromCharCode(l)} value={String.fromCharCode(l)}>
|
|
{String.fromCharCode(l)}
|
|
</MenuItem>
|
|
);
|
|
}
|
|
//add numbers 1-9 for terminal id options
|
|
for (let n = 1; n <= 9; n++) {
|
|
tiOptions.push(
|
|
<MenuItem key={n} value={n}>
|
|
{n}
|
|
</MenuItem>
|
|
);
|
|
}
|
|
let numberOptions: JSX.Element[] = [];
|
|
for (let n = 0; n <= 99; n++) {
|
|
numberOptions.push(
|
|
<MenuItem key={n} value={n}>
|
|
{n}
|
|
</MenuItem>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<React.Fragment>
|
|
<TextField
|
|
margin="dense"
|
|
label="Gate Name"
|
|
fullWidth
|
|
value={gateName}
|
|
onChange={e => setGateName(e.target.value)}
|
|
/>
|
|
<TextField
|
|
margin="dense"
|
|
id="terminal"
|
|
label="Terminal"
|
|
fullWidth
|
|
select
|
|
value={terminalKey}
|
|
onChange={e => {
|
|
setTerminalKey(e.target.value as string);
|
|
}}>
|
|
<MenuItem key="new" value="">
|
|
Select Terminal
|
|
</MenuItem>
|
|
{terminalOptions?.map(terminal => (
|
|
<MenuItem key={terminal.key} value={terminal.key}>
|
|
{terminal.name}
|
|
</MenuItem>
|
|
))}
|
|
</TextField>
|
|
<TextField
|
|
margin="dense"
|
|
label="Low Mass Air Flow"
|
|
fullWidth
|
|
type="number"
|
|
value={lowerFlowBound}
|
|
onChange={e => setLowerFlowBound(+e.target.value)}
|
|
/>
|
|
<TextField
|
|
margin="dense"
|
|
label="High Mass Air Flow"
|
|
fullWidth
|
|
type="dense"
|
|
value={upperFlowBound}
|
|
onChange={e => setUpperFlowBound(+e.target.value)}
|
|
/>
|
|
<TextField
|
|
margin="dense"
|
|
label="Idle Mass Air Flow"
|
|
fullWidth
|
|
type="number"
|
|
value={idleLimit}
|
|
onChange={e => setIdleLimit(+e.target.value)}
|
|
/>
|
|
<TextField
|
|
margin="dense"
|
|
label="PCA Unit"
|
|
fullWidth
|
|
value={pcaUnit}
|
|
onChange={e => setPcaUnit(e.target.value)}
|
|
/>
|
|
<TextField
|
|
margin="dense"
|
|
style={{ width: "45%" }}
|
|
select
|
|
label="Terminal Identifier"
|
|
value={terminalIdentifier}
|
|
onChange={e => {
|
|
setTerminalIdentifier(e.target.value as string);
|
|
}}>
|
|
<MenuItem key="new" value="">
|
|
Gate Letter
|
|
</MenuItem>
|
|
{tiOptions}
|
|
</TextField>
|
|
<TextField
|
|
margin="dense"
|
|
style={{ width: "45%", marginLeft: "10%" }}
|
|
label="Gate Identifier"
|
|
select
|
|
value={gateIdentifier}
|
|
onChange={e => setGateIdentifier(e.target.value)}>
|
|
<MenuItem key="new" value="">
|
|
Gate Number
|
|
</MenuItem>
|
|
{numberOptions}
|
|
</TextField>
|
|
<TextField
|
|
margin="dense"
|
|
label="Hourly PCA Cost"
|
|
fullWidth
|
|
type="number"
|
|
value={hourlyPCA}
|
|
InputProps={{
|
|
endAdornment: <InputAdornment position="end">$/hr</InputAdornment>
|
|
}}
|
|
onChange={e => setHourlyPCA(+e.target.value)}
|
|
/>
|
|
<TextField
|
|
margin="dense"
|
|
label="Hourly APU Cost"
|
|
fullWidth
|
|
type="number"
|
|
value={hourlyAPU}
|
|
InputProps={{
|
|
endAdornment: <InputAdornment position="end">$/hr</InputAdornment>
|
|
}}
|
|
onChange={e => setHourlyAPU(+e.target.value)}
|
|
/>
|
|
</React.Fragment>
|
|
);
|
|
};
|
|
|
|
const ductProperties = () => {
|
|
return (
|
|
<React.Fragment>
|
|
<TextField
|
|
id="ductType"
|
|
label="Duct Type"
|
|
fullWidth
|
|
value={ductType}
|
|
select
|
|
onChange={e => {
|
|
let type = +e.target.value;
|
|
setDuctType(type);
|
|
setDuctName("custom");
|
|
if (type > 1) {
|
|
let describer = DuctDescriber(type);
|
|
setDuctName(describer.name);
|
|
setDuctProps({
|
|
diameter: ductProps.diameter,
|
|
frictionFactor: describer.frictionFactor,
|
|
length: ductProps.length,
|
|
thermalConductivity: describer.thermalConductivity,
|
|
thermalResistance: describer.thermalResistance
|
|
});
|
|
}
|
|
}}>
|
|
<MenuItem key="new" value={pond.DuctType.DUCT_TYPE_INVALID}>
|
|
Select Ducting Type
|
|
</MenuItem>
|
|
{ductOptions.map(duct => (
|
|
<MenuItem key={duct.value} value={duct.value}>
|
|
{duct.label}
|
|
</MenuItem>
|
|
))}
|
|
<MenuItem key="custom" value={pond.DuctType.DUCT_TYPE_NONE}>
|
|
Custom
|
|
</MenuItem>
|
|
</TextField>
|
|
<TextField
|
|
margin="normal"
|
|
id="ductDiameter"
|
|
label="Duct Diameter(mm)"
|
|
fullWidth
|
|
type="text"
|
|
value={isNaN(ductProps.diameter) ? "" : ductProps.diameter}
|
|
error={isNaN(+ductProps.diameter)}
|
|
InputProps={{
|
|
endAdornment: <InputAdornment position="end">mm</InputAdornment>
|
|
}}
|
|
onChange={e => {
|
|
setDuctProps({ ...ductProps, diameter: parseFloat(e.target.value) });
|
|
}}
|
|
/>
|
|
<TextField
|
|
margin="normal"
|
|
id="ductLength"
|
|
label="Duct Length(m)"
|
|
fullWidth
|
|
type="text"
|
|
value={isNaN(ductProps.length) ? "" : ductProps.length}
|
|
error={isNaN(+ductProps.length)}
|
|
InputProps={{
|
|
endAdornment: <InputAdornment position="end">m</InputAdornment>
|
|
}}
|
|
onChange={e => {
|
|
setDuctProps({ ...ductProps, length: parseFloat(e.target.value) });
|
|
}}
|
|
/>
|
|
<TextField
|
|
margin="normal"
|
|
id="thermalCond"
|
|
label="Thermal Conductivity"
|
|
fullWidth
|
|
type="text"
|
|
value={isNaN(ductProps.thermalConductivity) ? "" : ductProps.thermalConductivity}
|
|
error={isNaN(+ductProps.thermalConductivity)}
|
|
InputProps={{
|
|
endAdornment: <InputAdornment position="end">W/mK</InputAdornment>
|
|
}}
|
|
onChange={e => {
|
|
setDuctProps({ ...ductProps, thermalConductivity: parseFloat(e.target.value) });
|
|
setDuctType(1);
|
|
setDuctName("custom");
|
|
}}
|
|
/>
|
|
<TextField
|
|
margin="normal"
|
|
id="friction"
|
|
label="Friction Factor"
|
|
fullWidth
|
|
type="text"
|
|
value={isNaN(ductProps.frictionFactor) ? "" : ductProps.frictionFactor}
|
|
error={isNaN(+ductProps.frictionFactor)}
|
|
onChange={e => {
|
|
setDuctProps({ ...ductProps, frictionFactor: parseFloat(e.target.value) });
|
|
setDuctType(1);
|
|
setDuctName("custom");
|
|
}}
|
|
/>
|
|
<TextField
|
|
margin="normal"
|
|
id="thermalRes"
|
|
label="Thermal Resistance"
|
|
fullWidth
|
|
type="text"
|
|
value={isNaN(ductProps.thermalResistance) ? "" : ductProps.thermalResistance}
|
|
error={isNaN(+ductProps.thermalResistance)}
|
|
InputProps={{
|
|
endAdornment: <InputAdornment position="end">K/W</InputAdornment>
|
|
}}
|
|
onChange={e => {
|
|
setDuctProps({ ...ductProps, thermalResistance: parseFloat(e.target.value) });
|
|
setDuctType(1);
|
|
setDuctName("custom");
|
|
}}
|
|
/>
|
|
</React.Fragment>
|
|
);
|
|
};
|
|
|
|
const removeConfirmation = () => {
|
|
return (
|
|
<ResponsiveDialog open={removeDialog} onClose={() => setRemoveDialog(false)}>
|
|
<DialogTitle>Delete Gate</DialogTitle>
|
|
<DialogContent>Are you sure you wish to delete this gate?</DialogContent>
|
|
<DialogActions>
|
|
<Button
|
|
onClick={() => {
|
|
setRemoveDialog(false);
|
|
}}>
|
|
Cancel
|
|
</Button>
|
|
<Button onClick={remove}>Confirm</Button>
|
|
</DialogActions>
|
|
</ResponsiveDialog>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<ResponsiveDialog open={open} onClose={() => close()}>
|
|
{removeConfirmation()}
|
|
<DialogTitle></DialogTitle>
|
|
<DialogContent>
|
|
{stepper()}
|
|
{stepperContent(activeStep)}
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Grid container direction="row">
|
|
<Grid>
|
|
{gate && (
|
|
<Button
|
|
variant="contained"
|
|
style={{
|
|
backgroundColor: "red"
|
|
}}
|
|
onClick={() => {
|
|
setRemoveDialog(true);
|
|
}}>
|
|
Delete
|
|
</Button>
|
|
)}
|
|
</Grid>
|
|
<Grid>
|
|
{(gate || activeStep === 0) && (
|
|
<Button
|
|
onClick={() => {
|
|
close();
|
|
}}>
|
|
Cancel
|
|
</Button>
|
|
)}
|
|
{!gate && activeStep > 0 && (
|
|
<Button
|
|
onClick={() => {
|
|
setActiveStep(activeStep - 1);
|
|
}}>
|
|
Back
|
|
</Button>
|
|
)}
|
|
{(gate || activeStep === 1) && (
|
|
<Button variant="contained" color="primary" onClick={confirm}>
|
|
Confirm
|
|
</Button>
|
|
)}
|
|
{!gate && activeStep === 0 && (
|
|
<Button
|
|
variant="contained"
|
|
color="primary"
|
|
onClick={() => {
|
|
setActiveStep(activeStep + 1);
|
|
}}>
|
|
Next
|
|
</Button>
|
|
)}
|
|
</Grid>
|
|
</Grid>
|
|
</DialogActions>
|
|
</ResponsiveDialog>
|
|
);
|
|
}
|