bins page now has the add bin button

This commit is contained in:
Carter 2025-03-10 12:51:40 -06:00
parent 429f990b41
commit cd972409b2
15 changed files with 134331 additions and 7 deletions

59
src/bin/AddBinFab.tsx Normal file
View file

@ -0,0 +1,59 @@
import { Fab, Theme } from "@mui/material";
import { makeStyles } from "@mui/styles";
import AddBinIcon from "assets/products/bindapt/addBin.png";
import { ImgIcon } from "common/ImgIcon";
import { useMobile } from "hooks";
interface Props {
onClick: () => void;
pulse: boolean;
}
const useStyles = makeStyles((theme: Theme) => {
const yellow = "#ffea00";
return ({
"@keyframes pulsate": {
to: {
boxShadow: "0 0 0 16px" + yellow + "00"
}
},
fab: {
background: yellow,
"&:hover": {
background: yellow
},
"&:focus": {
background: yellow
},
position: "fixed",
bottom: theme.spacing(8), //for mobile navigator
right: theme.spacing(2),
[theme.breakpoints.up("sm")]: {
bottom: theme.spacing(2)
}
},
pulse: {
boxShadow: "0 0 0 0 " + yellow + "75",
animation: "$pulsate 1.75s infinite cubic-bezier(0.66, 0.33, 0, 1)"
}
});
});
export default function AddBinFab(props: Props) {
const { onClick, pulse } = props;
const classes = useStyles();
const isMobile = useMobile();
const pulseString = pulse ? classes.pulse : "";
const classString = classes.fab + " " + pulseString;
return (
<Fab
onClick={onClick}
aria-label="Create Bin"
className={classString}
size={isMobile ? "medium" : "large"}>
<ImgIcon alt="Create Bin" src={AddBinIcon} />
</Fab>
);
}

154
src/bin/BinSelector.tsx Normal file
View file

@ -0,0 +1,154 @@
import { Grid2 as Grid, TextField } from "@mui/material";
import { ImportBins, jsonBin } from "common/DataImports/BinCables/BinCableImporter";
import SearchSelect, { Option } from "common/SearchSelect";
import { useMobile } from "hooks";
import { pond } from "protobuf-ts/pond";
import React, { useEffect, useState } from "react";
import { getDistanceUnit } from "utils";
interface Props {
optionsChanged: (binOptions: jsonBin[]) => void;
vertical?: boolean;
}
export default function BinSelector(props: Props) {
const { optionsChanged, vertical } = props;
const isMobile = useMobile();
const [manufacturerOptions, SetManufacturerOptions] = useState<Option[]>([]);
const [manufacturer, SetManufacturer] = useState<Option | null>(null);
const [binStyle, setBinStyle] = useState<Option | null>(null); //flat bottom or hopper
const [minDiameter, setMinDiameter] = useState(0);
const [maxDiameter, setMaxDiameter] = useState(0);
//set the initial manufacturer options
useEffect(() => {
let stringOps: string[] = [];
let options: Option[] = [];
ImportBins().forEach((bin: jsonBin) => {
if (!stringOps.includes(bin.Manufacturer)) {
stringOps.push(bin.Manufacturer);
options.push({
label: bin.Manufacturer,
value: bin.Manufacturer
});
}
});
SetManufacturerOptions(options);
}, []);
useEffect(() => {
//start out with all the bins
let ops: jsonBin[] = [];
ImportBins().forEach((bin: jsonBin) => {
let addBin = true;
if (manufacturer) {
if (manufacturer.value !== bin.Manufacturer) {
addBin = false;
}
}
if (binStyle) {
if (binStyle.value !== bin.Bottom) {
addBin = false;
}
}
if (minDiameter !== 0) {
if (minDiameter > bin.Diameter) {
addBin = false;
}
}
if (maxDiameter !== 0) {
if (maxDiameter < bin.Diameter) {
addBin = false;
}
}
if (addBin) {
ops.push(bin);
}
});
optionsChanged(ops);
}, [manufacturer, binStyle, minDiameter, maxDiameter]); // eslint-disable-line react-hooks/exhaustive-deps
const binSelection = () => {
return (
<Grid container direction="row" spacing={2} alignContent="center" alignItems="center">
<Grid size={{ xs: isMobile || vertical ? 12 : 3 }}>
{/* manufacturer */}
<SearchSelect
selected={manufacturer}
changeSelection={(option: Option | null) => {
SetManufacturer(option);
}}
options={manufacturerOptions}
label="Bin Manufacturer"
loading={false}
/>
</Grid>
<Grid size={{ xs: isMobile || vertical ? 12 : 3 }}>
{/* binStyle */}
<SearchSelect
selected={binStyle}
changeSelection={(option: Option | null) => {
setBinStyle(option);
}}
options={[
{ label: "Flat", value: "Flat Bottom" },
{ label: "Hopper", value: "Hopper Bottom" }
]}
label="BinStyle"
loading={false}
/>
</Grid>
{/* bin use: at the moment all bins are farm/personal use so this is not needed yet */}
{/* <Grid item xs={3}>
<SearchSelect
selected={binUse}
changeSelection={(option: Option | null) => {
setBinUse(option);
}}
options={[
{label: "Farm", value: "Farm"},
{label: "Commercial", value: "Commercial"}
]}
label="BinStyle"
loading={false}
/>
</Grid> */}
<Grid size={{ xs: isMobile || vertical ? 12 : 3 }}>
{/* minD */}
<TextField
variant="outlined"
type="number"
value={minDiameter}
onChange={e => {
if (!isNaN(+e.target.value)) {
setMinDiameter(+e.target.value);
}
}}
label={
"Minimum Diameter " +
(getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)")
}
/>
</Grid>
<Grid size={{ xs: isMobile || vertical ? 12 : 3 }}>
{/* maxD */}
<TextField
variant="outlined"
type="number"
value={maxDiameter}
onChange={e => {
if (!isNaN(+e.target.value)) {
setMaxDiameter(+e.target.value);
}
}}
label={
"Maximum Diameter " +
(getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)")
}
/>
</Grid>
</Grid>
);
};
return <React.Fragment>{binSelection()}</React.Fragment>;
}

2366
src/bin/BinSettings.tsx Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,67 @@
import bins from "common/DataImports/BinCables/bin_cable_data_with_count.json";
import brackets from "common/DataImports/BinCables/bracketData.json";
//note all length measurements are in feet
export interface Cable {
Orbit: number; //the orbit the cable is on in the bin
Length: number; //length of the cable
Nodes: number; //number of nodes on the cable
Type: number; //cable being temp or moisture
Count: number; //the number of similar cables
}
export interface CableSum {
Orbit: number;
NumberOfCables: number;
DistanceFromCenter: number;
Bracket: number;
}
export interface jsonBin {
ID: number;
Manufacturer: string;
Type: string;
Bottom: string;
Diameter: number;
Name: string;
Model: string;
Capacity: number;
Rings: number;
Sidewall: number;
Peak: number;
EaveToPeak: number;
RoofAngle: number;
HopperAngle: number;
CableSums: CableSum[];
Cables: Cable[];
}
export interface CableBracket {
ID: number;
Name: string;
DealerPrice: number;
}
export function ImportBins() {
return bins;
}
export function ImportBinMap() {
return new Map(bins.map(bin => [bin.ID, bin]));
}
export function getBinModel(id: number) {
return ImportBinMap().get(id);
}
export function BracketMap() {
let bMap = new Map<number, CableBracket>();
brackets.forEach(b => {
bMap.set(b.ID, b);
});
return bMap;
}
export function getBracket(bracketModel: number) {
return BracketMap().get(bracketModel);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,17 @@
[
{
"ID": 1,
"Name": "CABL-BRK1",
"DealerPrice": 210
},
{
"ID": 2,
"Name": "CABL-BRK2",
"DealerPrice": 217.5
},
{
"ID": 3,
"Name": "CABL-BRK3",
"DealerPrice": 87
}
]

View file

@ -0,0 +1,47 @@
import { Box, BoxProps, Theme, InputLabel } from "@mui/material";
import { makeStyles } from "@mui/styles";
// import NotchedOutline from "@material-ui/core/OutlinedInput/NotchedOutline";
import React, { PropsWithChildren } from "react";
const useStyles = makeStyles((theme: Theme) => {
return ({
inputLabel: {
position: "absolute",
left: 0,
top: 0,
transform: "translate(0, 24px) scale(1)"
},
notchedOutline: {
borderRadius: theme.shape.borderRadius,
opacity: theme.palette.action.activatedOpacity
}
})
});
interface Props extends BoxProps {
label: string | React.ReactNode;
}
const OutlinedBox = (props: PropsWithChildren<Props>) => {
const { label, children } = props;
const classes = useStyles();
const [labelWidth, setLabelWidth] = React.useState(0);
const labelRef = React.useRef<HTMLLabelElement>(null);
React.useEffect(() => {
const labelNode = labelRef.current;
setLabelWidth(labelNode != null ? labelNode.offsetWidth : 0);
}, [label]);
return (
<Box position="relative" width="100%" {...props}>
<InputLabel ref={labelRef} variant="outlined" className={classes.inputLabel} shrink>
{label}
</InputLabel>
{/* TODO: Add the notched outline part??? it's no longer in mui */}
{/* <NotchedOutline notched labelWidth={labelWidth} classes={{ root: classes.notchedOutline }} /> */}
<Box position="relative">{children}</Box>
</Box>
);
};
export default OutlinedBox;

View file

@ -0,0 +1,47 @@
/**
* Converts degrees to radians
* @param angle angle in degrees
* @returns angle in radians
*/
export function DegreesToRadians(angle: number) {
return angle * (Math.PI / 180);
}
/**
* Converts radians to degrees
* @param angle angle in radians
* @returns angle in degrees
*/
export function RadiansToDegrees(angle: number) {
return angle * (180 / Math.PI);
}
/**
* Takes in the one side of a right triangle its adjacent angle to determine the length of the opposite line
* @param length the length of the known side
* @param angle in degrees of the angle adjacent to the know side
* @returns the length of the line opposite the angle
*/
export function TriangleOppositeLength(length: number, angle: number) {
return length * Math.tan(DegreesToRadians(angle));
}
/**
* calculates the cubic volume of a cone, NOTE: radius and height must have the same units
* @param radius the radius of the base of the cone
* @param height the height at the peak of the cone
* @returns the cubic volume
*/
export function ConeVolume(radius: number, height: number) {
return Math.PI * Math.pow(radius, 2) * (height / 3);
}
/**
* calculates the cubic volume of a cylinder, NOTE: radius and height must have the same units
* @param radius the radius of the cylinder
* @param height the height of the cylinder
* @returns the cubic volume
*/
export function CylinderVolume(radius: number, height: number) {
return Math.PI * Math.pow(radius, 2) * height;
}

View file

@ -76,6 +76,8 @@ import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { green, yellow } from "@mui/material/colors";
import { makeStyles } from "@mui/styles";
import { useNavigate } from "react-router-dom";
import BinSettings from "bin/BinSettings";
import AddBinFab from "bin/AddBinFab";
// import { useHistory } from "react-router";
const useStyles = makeStyles((theme: Theme) => {
@ -736,12 +738,12 @@ export default function Bins(props: Props) {
setAddBinOpen(false);
}}
disableAutoFocusItem>
{/* <MenuItem onClick={() => setAddBinOpen(true)} aria-label="Create Bin" button dense>
<MenuItem onClick={() => setAddBinOpen(true)} aria-label="Create Bin" dense>
<ListItemIcon>
<LibraryAdd className={classes.green} />
</ListItemIcon>
<ListItemText secondary="Create Bin" />
</MenuItem> */}
</MenuItem>
</Menu>
);
};
@ -1362,7 +1364,7 @@ export default function Bins(props: Props) {
</Grid>
</Box>
</Box>
{/* <BinSettings
<BinSettings
mode="add"
canEdit={true}
open={addBinOpen}
@ -1376,7 +1378,7 @@ export default function Bins(props: Props) {
}
}}
/>
<GrainBagSettings
{/* <GrainBagSettings
open={addBagOpen}
close={() => {
setAddBagOpen(false);
@ -1391,7 +1393,7 @@ export default function Bins(props: Props) {
requiredUrlAffix={"bins"}
/> */}
{binMenu()}
{/* <AddBinFab onClick={() => setAddBinOpen(true)} pulse={binsTotal < 1 && !allLoading} /> */}
<AddBinFab onClick={() => setAddBinOpen(true)} pulse={binsTotal < 1 && !allLoading} />
</Box>
);
};

203
src/pbHelpers/Bin.ts Normal file
View file

@ -0,0 +1,203 @@
import { pond, quack } from "protobuf-ts/pond";
import { stringToComponentId } from "./Component";
import { capitalize } from "utils/strings";
import { or } from "utils";
interface BinShapeDescriber {
key: pond.BinShape;
label: string;
}
export const GetBinShapeDescribers = (): BinShapeDescriber[] => {
return [
{ key: pond.BinShape.BIN_SHAPE_UNKNOWN, label: "Unknown" },
{ key: pond.BinShape.BIN_SHAPE_FLAT_BOTTOM, label: "Flat Bottom" },
{ key: pond.BinShape.BIN_SHAPE_HOPPER_BOTTOM, label: "Hopper" }
];
};
export const HasGrainCable = (bin?: pond.BinSettings): boolean => {
if (bin && bin.deviceComponents) {
bin.deviceComponents.forEach(dc => {
dc.components.forEach(c => {
if (stringToComponentId(c).type === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE)
return true;
});
});
}
return false;
};
export const HasPlenumSensor = (bin?: pond.BinSettings): boolean => {
if (bin && bin.deviceComponents) {
bin.deviceComponents.forEach(dc => {
dc.components.forEach(c => {
let component = stringToComponentId(c);
if (component.type === quack.ComponentType.COMPONENT_TYPE_DHT) return true;
});
});
}
return false;
};
const keyTranslator = new Map<keyof pond.BinSettings, string>([
["name", "Name"],
["specs", "Specifications"],
["mode", "Mode of Operation"],
["fan", "Fan Type"],
["location", "Location"],
["inventory", "Inventory"],
["deviceComponents", "Device Components"]
]);
// Keys will be stringified by default if not found in the keyTranslator
export function TranslateKey(key: keyof pond.BinSettings): string {
let translatedKey = keyTranslator.get(key);
return translatedKey ? translatedKey : capitalize(key.toString());
}
const valueTranslator = new Map<keyof pond.BinSettings, (device: pond.BinSettings) => string>([
["name", device => device.name],
[
"specs",
device => {
let s: string = "";
if (device.specs) {
s = s + "Capacity: " + device.specs.bushelCapacity + " (bu)";
switch (device.specs.shape) {
case pond.BinShape.BIN_SHAPE_FLAT_BOTTOM:
s = s + "\nShape: Flat Bottom";
break;
case pond.BinShape.BIN_SHAPE_HOPPER_BOTTOM:
s = s + "\nShape: Hopper Bottom";
break;
default:
s = s + "Unknown";
}
s = s + "\nHeight: " + device.specs.heightCm + " (cm)";
s = s + "\nDiameter: " + device.specs.diameterCm + " (cm)";
}
return s;
}
],
[
"mode",
device => {
if (device.mode === undefined) {
return "None";
}
if (device.mode === null) {
return "None";
}
switch (device.mode) {
case pond.BinMode.BIN_MODE_COOLDOWN:
return "Cool Down";
case pond.BinMode.BIN_MODE_DRYING:
return "Drying";
case pond.BinMode.BIN_MODE_NONE:
return "None";
case pond.BinMode.BIN_MODE_STORAGE:
return "Storage";
case pond.BinMode.BIN_MODE_HYDRATING:
return "Hydrating";
default:
return "None";
}
}
],
[
"fan",
device => {
switch (device.fan?.type) {
case pond.FanType.FAN_TYPE_CENTRIFUGAL_HIGH_SPEED:
return "Centrifugal High Speed";
case pond.FanType.FAN_TYPE_CENTRIFUGAL_INLINE:
return "Centrifugal Inline";
case pond.FanType.FAN_TYPE_CENTRIFUGAL_LOW_SPEED:
return "Centrifugal Low Speed";
case pond.FanType.FAN_TYPE_UNKNOWN:
return "Unknown";
default:
return "None";
}
}
],
[
"location",
device => {
let long = device.location?.longitude;
let lat = device.location?.latitude;
if (long && lat) {
return "Long: " + Math.round(long * 100) / 100 + ", Lat: " + Math.round(lat * 100) / 100;
}
return "No coords to compare";
}
],
[
"inventory",
device => {
let s: string = "";
if (device.inventory) {
s = s + "Grain Type: ";
switch (device.inventory.grainType) {
case pond.Grain.GRAIN_BARLEY:
s = s + "Barley,\n";
break;
case pond.Grain.GRAIN_BUCKWHEAT:
s = s + "Buckwheat,\n";
break;
case pond.Grain.GRAIN_CANOLA:
s = s + "Canola,\n";
break;
default:
s = s + "Grain,\n";
}
s = s + "Use: ";
switch (device.inventory.grainUse) {
case pond.GrainUse.GRAIN_USE_COMMERCIAL:
s = s + "Commercial,\n";
break;
case pond.GrainUse.GRAIN_USE_FEED:
s = s + "Feed,\n";
break;
case pond.GrainUse.GRAIN_USE_SEED:
s = s + "Seed,\n";
break;
default:
s = s + "Unknown,\n";
}
s = s + "Amount: " + device.inventory.grainBushels + " (bu)";
}
return s;
}
],
[
"deviceComponents",
device => {
let s: string = "";
device.deviceComponents.forEach(dev => {
dev.components.forEach(comp => {
s = s + comp + "\n";
});
});
return s;
}
],
[
"theme",
device => {
return "Bin Theme";
}
]
]);
// Values will be stringified by default if its key is not found in the valueTranslator
export function TranslateValue(key: keyof pond.BinSettings, device: pond.BinSettings) {
let translatorFunc = valueTranslator.get(key);
let value: any = or(device[key], "");
let d: string;
d = or(value.toString(), "");
return translatorFunc ? translatorFunc(device) : device[key] ? capitalize(d) : "";
}

View file

@ -5,7 +5,7 @@ export { HTTPContext, useHTTP } from "./http";
export {
useBackpackAPI,
useBinAPI,
// useBinYardAPI,
useBinYardAPI,
useNoteAPI,
useNotificationAPI,
useComponentAPI,

View file

@ -0,0 +1,113 @@
import { useHTTP } from "hooks";
import { createContext, PropsWithChildren, useContext } from "react";
import { pond } from "protobuf-ts/pond";
import { or } from "utils/types";
import { pondURL } from "./pond";
import { AxiosResponse } from "axios";
import { useGlobalState } from "providers";
export interface IBinYardAPIContext {
addBinYard: (bin: pond.BinYardSettings) => Promise<AxiosResponse<pond.AddBinYardResponse>>;
updateBinYard: (
key: string,
binYard: pond.BinYardSettings,
asRoot?: true
) => Promise<AxiosResponse<pond.UpdateBinYardResponse>>;
removeBinYard: (key: string) => Promise<AxiosResponse<pond.RemoveBinYardResponse>>;
getBinYard: (key: string) => Promise<AxiosResponse<pond.BinYard>>;
listBinYards: (
limit: number,
offset: number,
order?: "asc" | "desc",
orderBy?: string,
search?: string,
asRoot?: boolean,
specificUser?: string
) => Promise<AxiosResponse<pond.ListBinYardsResponse>>;
}
export const BinYardAPIContext = createContext<IBinYardAPIContext>({} as IBinYardAPIContext);
interface Props {}
export default function BinYardProvider(props: PropsWithChildren<Props>) {
const { children } = props;
const { get, post, put, del } = useHTTP();
const [{ as }] = useGlobalState();
const addBinYard = (binYard: pond.BinYardSettings) => {
if (as) return post<pond.AddBinYardResponse>(pondURL(`/binyard?as=${as}`), binYard);
return post<pond.AddBinYardResponse>(pondURL("/binyard"), binYard);
};
const updateBinYard = (key: string, binYard: pond.BinYardSettings, asRoot?: boolean) => {
if (as) {
if (asRoot) {
return put<pond.UpdateBinYardResponse>(
pondURL(
"/binyards/" + key + (asRoot ? "?asRoot=" + asRoot.toString() : "") + "&as=" + as
),
binYard
);
}
return put<pond.UpdateBinYardResponse>(pondURL("/binyards/" + key + "?as=" + as), binYard);
}
return put<pond.UpdateBinYardResponse>(
pondURL("/binyards/" + key + (asRoot ? "?asRoot=" + asRoot.toString() : "")),
binYard
);
};
const removeBinYard = (key: string) => {
if (as) return del<pond.RemoveBinYardResponse>(pondURL("/binyard/" + key + "?as=" + as));
return del<pond.RemoveBinYardResponse>(pondURL("/binyard/" + key));
};
const getBinYard = (key: string) => {
if (as) return get<pond.BinYard>(pondURL("/binYard/" + key + "?as=" + as));
return get<pond.BinYard>(pondURL("/binYard/" + key));
};
const listBinYards = (
limit: number,
offset: number,
order?: "asc" | "desc",
orderBy?: string,
search?: string,
asRoot?: boolean,
specificUser?: string
) => {
let asText = "";
if (as) asText = "&as=" + as;
if (specificUser) asText = "&as=" + specificUser;
return get<pond.ListBinYardsResponse>(
pondURL(
"/binYards" +
"?limit=" +
limit +
"&offset=" +
offset +
("&order=" + or(order, "asc")) +
("&by=" + or(orderBy, "key")) +
(search ? "&search=" + search : "") +
(asRoot ? "&asRoot=" + asRoot.toString() : "") +
asText
)
);
};
return (
<BinYardAPIContext.Provider
value={{
addBinYard,
removeBinYard,
getBinYard,
listBinYards,
updateBinYard
}}>
{children}
</BinYardAPIContext.Provider>
);
}
export const useBinYardAPI = () => useContext(BinYardAPIContext);

View file

@ -15,6 +15,7 @@ import TagProvider, { useTagAPI } from "./tagAPI";
import FirmwareProvider, { useFirmwareAPI } from "./firmwareAPI";
import ComponentProvider, { useComponentAPI } from "./componentAPI";
import InteractionProvider, { useInteractionsAPI } from "./interactionsAPI";
import BinYardProvider, { useBinYardAPI } from "./binYardAPI";
// import NoteProvider from "providers/noteAPI";
export const pondURL = (partial: string, demo: boolean = false): string => {
@ -46,7 +47,9 @@ export default function PondProvider(props: PropsWithChildren<any>) {
<FirmwareProvider>
<ComponentProvider>
<InteractionProvider>
{children}
<BinYardProvider>
{children}
</BinYardProvider>
</InteractionProvider>
</ComponentProvider>
</FirmwareProvider>
@ -76,6 +79,7 @@ export {
useInteractionsAPI,
usePermissionAPI,
useBinAPI,
useBinYardAPI,
useGateAPI,
useGroupAPI,
useNoteAPI,

View file

@ -9,6 +9,7 @@
/* Bundler mode */
"moduleResolution": "node",
"resolveJsonModule": true,
//"allowImportingTsExtensions": true,
"isolatedModules": true,
//"moduleDetection": "force",

View file

@ -1,5 +1,6 @@
{
"compilerOptions": {
"baseUrl": "src",
"resolveJsonModule": true,
"esModuleInterop": true
},