frontend/src/gate/GateSettings.tsx
2025-04-17 15:42:19 -06:00

575 lines
17 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 [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.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,
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
label="Gate Name"
fullWidth
value={gateName}
onChange={e => setGateName(e.target.value)}
/>
<Select
id="terminal"
label="Terminal"
fullWidth
displayEmpty
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>
))}
</Select>
<TextField
label="Low Mass Air Flow"
fullWidth
type="number"
value={lowerFlowBound}
onChange={e => setLowerFlowBound(+e.target.value)}
/>
<TextField
label="High Mass Air Flow"
fullWidth
type="number"
value={upperFlowBound}
onChange={e => setUpperFlowBound(+e.target.value)}
/>
<TextField
label="PCA Unit"
fullWidth
value={pcaUnit}
onChange={e => setPcaUnit(e.target.value)}
/>
<TextField
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
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
label="Hourly PCA Cost"
fullWidth
type="number"
value={hourlyPCA}
InputProps={{
endAdornment: <InputAdornment position="end">$/hr</InputAdornment>
}}
onChange={e => setHourlyPCA(+e.target.value)}
/>
<TextField
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>
);
}