merged staging and updated staging proto
37
README.md
|
|
@ -7,6 +7,43 @@ Currently, two official plugins are available:
|
||||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
### White-labeling
|
||||||
|
|
||||||
|
Steps to add a new white label
|
||||||
|
|
||||||
|
1. Client Instructions:
|
||||||
|
|
||||||
|
- supply assets and colours to be used for the white-label (favicon if necessary)
|
||||||
|
- determine a sub-domain to use as mask, dashboard is preferred (ex: dashboard.example.com)
|
||||||
|
- add a `CNAME` record pointing their custom subdomain (ex: dashboard.example.com) to brandxtech.ca
|
||||||
|
- add a login button on their website point to `/login` of their custom subdomain (ex: dashboard.example.com/login)
|
||||||
|
|
||||||
|
<!-- frontend steps -->
|
||||||
|
2. Create a PWA folder in `public` that is named after the client and generate PWA assets using [this](https://realfavicongenerator.net/) website (use the company logo but you might need to use a custom favicon depending on the result)
|
||||||
|
|
||||||
|
- https://maskable.app/ also create a maskable icon for PWA
|
||||||
|
- create a 512x512 png as well for PWA
|
||||||
|
|
||||||
|
3. Create a white-label branding folder in `src/assets/whitelabels` and create a `darkLogo.png` and `lightLogo.png` from the client's logo
|
||||||
|
4. In `src/services/whiteLabel.ts`, add a new `Whitelabel` instance with the required fields and add its hostname mapping to the `whitelabels` map (it is very similar to step one but had some constraints when dealing with the `public` folder)
|
||||||
|
5. If there are any pages specific to the whitelabel be sure to update the side and bottom navigator
|
||||||
|
|
||||||
|
<!-- backend steps -->
|
||||||
|
6. In `dynamic-config.prod.yml`, add a router entry for the new whitelabel
|
||||||
|
7. In `pond/http/server.go`, add the sub-domain to the `allowedOrigins`
|
||||||
|
8. In `pond/whitelabel.go`, add an entry for the new white label
|
||||||
|
|
||||||
|
7. Create a new `Auth0` application for the client
|
||||||
|
- use a image bucket like [postimg](https://postimages.org/) to store their company logo and favicon
|
||||||
|
- add a whitelabel logo
|
||||||
|
- allow necessary `origins`, `callback URLs`, and `CORS domains` to the application settings (see the default application as a guideline)
|
||||||
|
- customize the Universal Login page to handle the new whitelabel
|
||||||
|
- check if the client ID matches this client's Auth0 application
|
||||||
|
- change the Universal Login theme using white-label colours and images stored in a bucket (ex: [postimg](https://postimages.org/))
|
||||||
|
8. Enable the white-label sub-domain for CORS in the backend
|
||||||
|
|
||||||
|
- in [http.go](https://gitlab.com/brandx/backend/blob/master/pond/http.go), add the white-label sub-domain to the `AllowedOrigins` list
|
||||||
|
|
||||||
## Expanding the ESLint configuration
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||||
|
|
|
||||||
20
package-lock.json
generated
|
|
@ -43,7 +43,7 @@
|
||||||
"mui-tel-input": "^7.0.0",
|
"mui-tel-input": "^7.0.0",
|
||||||
"notistack": "^3.0.1",
|
"notistack": "^3.0.1",
|
||||||
"openweathermap-ts": "^1.2.10",
|
"openweathermap-ts": "^1.2.10",
|
||||||
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#master",
|
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging",
|
||||||
"query-string": "^9.2.1",
|
"query-string": "^9.2.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
|
|
@ -8306,10 +8306,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/date-fns-jalali": {
|
"node_modules/date-fns-jalali": {
|
||||||
"version": "4.1.0-0",
|
"version": "4.0.0-0",
|
||||||
"resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz",
|
"resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.0.0-0.tgz",
|
||||||
"integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==",
|
"integrity": "sha512-EczB+gWceuWCRlacE4T+WmdP+BV/IUQpjQW9aBa9DNcXkKuZFv3WBDqeP2Ew+6YFBtPRRcH5U22+C6gcpwgG8A==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/dayjs": {
|
"node_modules/dayjs": {
|
||||||
"version": "1.11.19",
|
"version": "1.11.19",
|
||||||
|
|
@ -11965,7 +11967,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/protobuf-ts": {
|
"node_modules/protobuf-ts": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#bcf085706791036841f7047a5b37f00c18e54574",
|
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#c9ef7906fd97cda8ef4bd149ec4a796159a7c067",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"protobufjs": "^6.8.8"
|
"protobufjs": "^6.8.8"
|
||||||
}
|
}
|
||||||
|
|
@ -12174,6 +12176,12 @@
|
||||||
"react": ">=16.8.0"
|
"react": ">=16.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-day-picker/node_modules/date-fns-jalali": {
|
||||||
|
"version": "4.1.0-0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz",
|
||||||
|
"integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@
|
||||||
"mui-tel-input": "^7.0.0",
|
"mui-tel-input": "^7.0.0",
|
||||||
"notistack": "^3.0.1",
|
"notistack": "^3.0.1",
|
||||||
"openweathermap-ts": "^1.2.10",
|
"openweathermap-ts": "^1.2.10",
|
||||||
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#master",
|
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging",
|
||||||
"query-string": "^9.2.1",
|
"query-string": "^9.2.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
|
|
|
||||||
BIN
public/MiPCA/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 4 KiB |
BIN
public/MiPCA/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/MiPCA/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
9
public/MiPCA/browserconfig.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig>
|
||||||
|
<msapplication>
|
||||||
|
<tile>
|
||||||
|
<square150x150logo src="/mstile-150x150.png"/>
|
||||||
|
<TileColor>#da532c</TileColor>
|
||||||
|
</tile>
|
||||||
|
</msapplication>
|
||||||
|
</browserconfig>
|
||||||
BIN
public/MiPCA/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 808 B |
BIN
public/MiPCA/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/MiPCA/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
33
public/MiPCA/manifest.json
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"name": "MiPCA",
|
||||||
|
"short_name": "MiPCA",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "48x48",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "android-chrome-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "maskable_icon.png",
|
||||||
|
"sizes": "196x196",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": "/",
|
||||||
|
"theme_color": "#272727",
|
||||||
|
"background_color": "#303030",
|
||||||
|
"display": "standalone",
|
||||||
|
"orientation": "any"
|
||||||
|
}
|
||||||
|
|
||||||
BIN
public/MiPCA/mstile-150x150.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
50
public/MiPCA/safari-pinned-tab.svg
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="1059.000000pt" height="1059.000000pt" viewBox="0 0 1059.000000 1059.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet">
|
||||||
|
<metadata>
|
||||||
|
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||||
|
</metadata>
|
||||||
|
<g transform="translate(0.000000,1059.000000) scale(0.100000,-0.100000)"
|
||||||
|
fill="#000000" stroke="none">
|
||||||
|
<path d="M7029 6666 c-2 -2 -31 -7 -64 -10 -426 -43 -727 -354 -781 -806 -10
|
||||||
|
-89 -11 -984 -1 -1090 27 -277 151 -517 340 -659 92 -69 256 -140 366 -157 18
|
||||||
|
-3 43 -8 55 -10 11 -3 80 -8 151 -11 408 -17 711 161 894 525 39 77 95 242
|
||||||
|
114 335 l14 67 -36 1 c-174 5 -484 0 -490 -7 -4 -5 -15 -36 -25 -69 -40 -134
|
||||||
|
-128 -250 -226 -300 -73 -37 -171 -51 -268 -38 -177 23 -289 121 -347 303 -16
|
||||||
|
49 -18 107 -20 522 -3 508 1 556 51 668 15 33 30 60 34 60 3 0 13 12 20 26 20
|
||||||
|
36 100 92 167 115 116 41 247 36 353 -14 52 -25 157 -127 182 -178 36 -73 56
|
||||||
|
-125 63 -161 4 -21 10 -38 14 -38 9 -1 494 -1 512 -1 15 1 15 7 -2 79 -37 158
|
||||||
|
-60 225 -114 332 -141 281 -357 448 -651 504 -62 11 -296 21 -305 12z"/>
|
||||||
|
<path d="M0 5298 c0 -1071 3 -1348 13 -1349 55 -4 439 -1 444 4 3 3 6 420 6
|
||||||
|
927 0 506 2 919 4 917 2 -3 62 -190 134 -418 72 -228 178 -565 236 -749 58
|
||||||
|
-184 108 -345 110 -357 3 -13 10 -23 17 -24 6 0 107 -1 225 -2 205 -2 214 -1
|
||||||
|
222 18 9 21 232 743 429 1390 29 94 55 173 59 177 3 4 6 -418 5 -938 -1 -861
|
||||||
|
0 -944 15 -945 93 -5 426 -2 434 3 9 5 12 346 12 1348 l0 1340 -35 1 c-46 0
|
||||||
|
-555 0 -601 0 l-35 -1 -28 -97 c-15 -54 -32 -111 -37 -128 -5 -16 -40 -140
|
||||||
|
-78 -275 -38 -135 -101 -360 -141 -500 -40 -140 -81 -284 -90 -320 -10 -36
|
||||||
|
-41 -146 -69 -245 -29 -99 -54 -191 -57 -205 -8 -39 -12 -36 -28 21 -8 30 -47
|
||||||
|
167 -86 304 -40 138 -82 288 -95 335 -33 121 -120 427 -221 780 -47 168 -87
|
||||||
|
311 -87 318 0 6 -7 12 -16 12 -9 1 -161 2 -339 3 l-322 2 0 -1347z"/>
|
||||||
|
<path d="M2890 6400 l0 -245 253 0 252 0 0 245 0 245 -252 0 -253 0 0 -245z"/>
|
||||||
|
<path d="M3948 6642 l-46 -3 0 -244 1 -244 591 -3 c575 -3 592 -4 636 -24 85
|
||||||
|
-39 141 -114 165 -219 43 -186 -50 -367 -212 -411 -38 -10 -170 -12 -610 -10
|
||||||
|
l-563 2 -1 -30 c-4 -176 0 -1499 5 -1503 4 -5 417 -8 486 -4 14 1 15 63 15
|
||||||
|
521 0 343 3 520 10 521 5 0 170 1 365 3 384 3 419 7 558 63 270 108 449 345
|
||||||
|
478 632 10 109 11 132 3 224 -22 244 -131 442 -322 581 -93 68 -273 131 -408
|
||||||
|
142 -69 6 -1070 11 -1151 6z"/>
|
||||||
|
<path d="M9279 6642 c-46 -3 -47 -4 -64 -50 -9 -26 -21 -56 -25 -67 -5 -11
|
||||||
|
-50 -128 -100 -260 -50 -132 -94 -249 -99 -260 -5 -11 -55 -141 -111 -290 -56
|
||||||
|
-148 -105 -279 -110 -290 -11 -24 2 10 -296 -770 -284 -741 -267 -697 -255
|
||||||
|
-704 11 -7 2358 -8 2364 -1 3 4 -59 172 -126 340 -3 8 -144 377 -312 820 -169
|
||||||
|
443 -322 843 -340 890 -18 47 -79 207 -135 355 -57 149 -106 274 -110 278 -7
|
||||||
|
8 -198 14 -281 9z m144 -932 c8 -25 45 -130 82 -235 37 -104 86 -246 110 -315
|
||||||
|
24 -69 71 -204 105 -300 34 -96 76 -218 95 -270 18 -52 38 -107 44 -122 6 -16
|
||||||
|
9 -29 8 -29 -1 -1 -213 -2 -470 -3 l-468 -1 6 23 c5 22 54 162 300 862 51 146
|
||||||
|
108 307 126 358 17 51 35 89 39 85 4 -4 14 -28 23 -53z"/>
|
||||||
|
<path d="M2890 4720 c0 -609 3 -770 13 -771 6 0 120 -1 252 -2 l240 0 3 771 2
|
||||||
|
772 -255 0 -255 0 0 -770z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.1 KiB |
BIN
src/assets/whitelabels/MiPCA/MiPCALogo.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
|
|
@ -200,25 +200,27 @@ export default function BinCard(props: Props) {
|
||||||
|
|
||||||
const typeDisplay = () => {
|
const typeDisplay = () => {
|
||||||
let grainType = bin.settings.inventory?.grainType ?? pond.Grain.GRAIN_NONE;
|
let grainType = bin.settings.inventory?.grainType ?? pond.Grain.GRAIN_NONE;
|
||||||
if (bin.storage() === pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN) {
|
switch (bin.storage()){
|
||||||
return (
|
case pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN:
|
||||||
<Box>
|
return (
|
||||||
<Typography variant="subtitle2" style={{ fontSize: "0.7rem", fontWeight: 700 }}>
|
<Box>
|
||||||
{GrainDescriber(grainType).name}
|
<Typography variant="subtitle2" style={{ fontSize: "0.7rem", fontWeight: 700 }}>
|
||||||
</Typography>
|
{GrainDescriber(grainType).name}
|
||||||
<Typography variant="subtitle2" style={{ fontSize: "0.7rem", fontWeight: 700 }}>
|
</Typography>
|
||||||
{bin.settings.inventory?.grainSubtype}
|
<Typography variant="subtitle2" style={{ fontSize: "0.7rem", fontWeight: 700 }}>
|
||||||
</Typography>
|
{bin.settings.inventory?.grainSubtype}
|
||||||
</Box>
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle2" style={{ fontSize: "0.7rem", fontWeight: 700 }}>
|
||||||
|
{bin.grainName()}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Typography variant="subtitle2" style={{ fontSize: "0.7rem", fontWeight: 700 }}>
|
|
||||||
{bin.settings.inventory?.customTypeName}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const tempDisplay = () => {
|
const tempDisplay = () => {
|
||||||
|
|
@ -425,6 +427,7 @@ export default function BinCard(props: Props) {
|
||||||
hottestNodeTemp={valDisplay === "high" ? hotNode?.temp : undefined}
|
hottestNodeTemp={valDisplay === "high" ? hotNode?.temp : undefined}
|
||||||
coldestNodeTemp={valDisplay === "low" ? coldNode?.temp : undefined}
|
coldestNodeTemp={valDisplay === "low" ? coldNode?.temp : undefined}
|
||||||
inventoryControl={bin.inventoryControl()}
|
inventoryControl={bin.inventoryControl()}
|
||||||
|
customGrain={bin.customGrain()}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -39,11 +39,12 @@ interface Props {
|
||||||
heaters?: Controller[];
|
heaters?: Controller[];
|
||||||
fans?: Controller[];
|
fans?: Controller[];
|
||||||
grain?: pond.Grain;
|
grain?: pond.Grain;
|
||||||
|
customGrain?: pond.GrainSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BinConditioningCard(props: Props) {
|
export default function BinConditioningCard(props: Props) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const { interactions, interactionDevices, plenums, ambients, heaters, fans, grain, mode } = props;
|
const { interactions, interactionDevices, plenums, ambients, heaters, fans, grain, mode, customGrain } = props;
|
||||||
const [sourceMap, setSourceMap] = useState<Map<string, Component>>(new Map());
|
const [sourceMap, setSourceMap] = useState<Map<string, Component>>(new Map());
|
||||||
const [sinkMap, setSinkMap] = useState<Map<string, Component>>(new Map());
|
const [sinkMap, setSinkMap] = useState<Map<string, Component>>(new Map());
|
||||||
|
|
||||||
|
|
@ -87,6 +88,7 @@ export default function BinConditioningCard(props: Props) {
|
||||||
sink={sink}
|
sink={sink}
|
||||||
source={source}
|
source={source}
|
||||||
grain={grain}
|
grain={grain}
|
||||||
|
customGrain={customGrain}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,10 +84,11 @@ interface Props {
|
||||||
source: Component;
|
source: Component;
|
||||||
sink: Component;
|
sink: Component;
|
||||||
grain?: pond.Grain;
|
grain?: pond.Grain;
|
||||||
|
customGrain?: pond.GrainSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BinConditioningInteraction(props: Props) {
|
export default function BinConditioningInteraction(props: Props) {
|
||||||
const { interaction, source, sink, grain, deviceId } = props;
|
const { interaction, source, sink, grain, deviceId, customGrain } = props;
|
||||||
const [sliderVals, setSliderVals] = useState<Map<quack.MeasurementType, number>>(new Map());
|
const [sliderVals, setSliderVals] = useState<Map<quack.MeasurementType, number>>(new Map());
|
||||||
const [sliderMarks, setSliderMarks] = useState<Map<quack.MeasurementType, number>>(new Map());
|
const [sliderMarks, setSliderMarks] = useState<Map<quack.MeasurementType, number>>(new Map());
|
||||||
//this is the emc value calculated from the interactions temp and humidity conditions
|
//this is the emc value calculated from the interactions temp and humidity conditions
|
||||||
|
|
@ -127,7 +128,6 @@ export default function BinConditioningInteraction(props: Props) {
|
||||||
if (
|
if (
|
||||||
grain !== undefined &&
|
grain !== undefined &&
|
||||||
grain !== pond.Grain.GRAIN_INVALID &&
|
grain !== pond.Grain.GRAIN_INVALID &&
|
||||||
grain !== pond.Grain.GRAIN_CUSTOM &&
|
|
||||||
temp &&
|
temp &&
|
||||||
hum
|
hum
|
||||||
) {
|
) {
|
||||||
|
|
@ -135,8 +135,9 @@ export default function BinConditioningInteraction(props: Props) {
|
||||||
//the emc calc needs the temp to be in celsius
|
//the emc calc needs the temp to be in celsius
|
||||||
temp = fahrenheitToCelsius(temp);
|
temp = fahrenheitToCelsius(temp);
|
||||||
}
|
}
|
||||||
let emc = ExtractMoisture(grain, temp, hum);
|
let emc = ExtractMoisture(grain, temp, hum, customGrain)
|
||||||
setBaseEMC(emc);
|
setBaseEMC(emc === hum ? undefined : emc);
|
||||||
|
|
||||||
}
|
}
|
||||||
}, [sliderVals, grain]);
|
}, [sliderVals, grain]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,7 @@ interface Props {
|
||||||
hottestNodeTemp?: number;
|
hottestNodeTemp?: number;
|
||||||
coldestNodeTemp?: number;
|
coldestNodeTemp?: number;
|
||||||
inventoryControl?: pond.BinInventoryControl;
|
inventoryControl?: pond.BinInventoryControl;
|
||||||
|
customGrain?: pond.GrainSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GrainNodePoint {
|
interface GrainNodePoint {
|
||||||
|
|
@ -211,7 +212,8 @@ export default function BinSVGV2(props: Props) {
|
||||||
hottestNodeTemp,
|
hottestNodeTemp,
|
||||||
coldestNodeTemp,
|
coldestNodeTemp,
|
||||||
inventoryControl,
|
inventoryControl,
|
||||||
co2Sensors
|
co2Sensors,
|
||||||
|
customGrain
|
||||||
} = props;
|
} = props;
|
||||||
const [{ user }] = useGlobalState();
|
const [{ user }] = useGlobalState();
|
||||||
const [extraDetails, setExtraDetails] = useState<"temps" | "hums">("temps");
|
const [extraDetails, setExtraDetails] = useState<"temps" | "hums">("temps");
|
||||||
|
|
@ -683,7 +685,7 @@ export default function BinSVGV2(props: Props) {
|
||||||
x: cablePos,
|
x: cablePos,
|
||||||
y: nodeY + 10,
|
y: nodeY + 10,
|
||||||
stroke: "green",
|
stroke: "green",
|
||||||
value: ExtractMoisture(props.grainType, temp, cable.humidities[index] ?? 0).toFixed(1)+"%",
|
value: ExtractMoisture(props.grainType, temp, cable.humidities[index] ?? 0, customGrain).toFixed(1)+"%",
|
||||||
})
|
})
|
||||||
!showTempHum &&
|
!showTempHum &&
|
||||||
nodeClickData.push({
|
nodeClickData.push({
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ import { getDistanceUnit, or, getTemperatureUnit, getGrainUnit } from "utils";
|
||||||
import { makeStyles } from "@mui/styles";
|
import { makeStyles } from "@mui/styles";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import BinSelector from "./BinSelector";
|
import BinSelector from "./BinSelector";
|
||||||
|
import CustomGrainSelector from "grain/CustomGrainSelector";
|
||||||
// import BinSelector from "./BinSelector";
|
// import BinSelector from "./BinSelector";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => {
|
const useStyles = makeStyles((theme: Theme) => {
|
||||||
|
|
@ -205,6 +206,7 @@ export default function BinSettings(props: Props) {
|
||||||
const libracartAPI = useLibraCartProxyAPI()
|
const libracartAPI = useLibraCartProxyAPI()
|
||||||
const [lcDestination, setlcDestination] = useState<Option | null>()
|
const [lcDestination, setlcDestination] = useState<Option | null>()
|
||||||
const [lcDestinationOptions, setlcDestinationOptions] = useState<Option[]>([])
|
const [lcDestinationOptions, setlcDestinationOptions] = useState<Option[]>([])
|
||||||
|
const [customGrain, setCustomGrain] = useState<pond.GrainSettings>()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
|
|
@ -236,6 +238,9 @@ export default function BinSettings(props: Props) {
|
||||||
initForm.inventory.grainType
|
initForm.inventory.grainType
|
||||||
).bushelsPerTonne;
|
).bushelsPerTonne;
|
||||||
}
|
}
|
||||||
|
if (initForm.inventory.customGrain){
|
||||||
|
setCustomGrain(initForm.inventory.customGrain)
|
||||||
|
}
|
||||||
//if the target temp is not set (older bins) make it the midpoint of the high and low temps assuming they are set otherwise make it 15
|
//if the target temp is not set (older bins) make it the midpoint of the high and low temps assuming they are set otherwise make it 15
|
||||||
if (!initForm.inventory.targetTemperature || initForm.inventory.targetTemperature) {
|
if (!initForm.inventory.targetTemperature || initForm.inventory.targetTemperature) {
|
||||||
if (initForm.highTemp && initForm.lowTemp) {
|
if (initForm.highTemp && initForm.lowTemp) {
|
||||||
|
|
@ -444,6 +449,11 @@ export default function BinSettings(props: Props) {
|
||||||
form.inventory.inventoryControl = inventoryControl
|
form.inventory.inventoryControl = inventoryControl
|
||||||
form.inventory.autoThreshold = autoFillThreshold
|
form.inventory.autoThreshold = autoFillThreshold
|
||||||
form.inventory.lidarDropCm = getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? lidarDropDistance * 30.48 : lidarDropDistance
|
form.inventory.lidarDropCm = getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? lidarDropDistance * 30.48 : lidarDropDistance
|
||||||
|
if(form.inventory.grainType !== pond.Grain.GRAIN_CUSTOM){
|
||||||
|
form.inventory.customGrain = undefined
|
||||||
|
}else{
|
||||||
|
form.inventory.customGrain = customGrain
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binAPI
|
binAPI
|
||||||
|
|
@ -493,6 +503,11 @@ export default function BinSettings(props: Props) {
|
||||||
form.inventory.autoThreshold = autoFillThreshold
|
form.inventory.autoThreshold = autoFillThreshold
|
||||||
//if the users preferences are in feet convert the distance to cm otherwise it was entered as cm so use that
|
//if the users preferences are in feet convert the distance to cm otherwise it was entered as cm so use that
|
||||||
form.inventory.lidarDropCm = getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? lidarDropDistance * 30.48 : lidarDropDistance
|
form.inventory.lidarDropCm = getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? lidarDropDistance * 30.48 : lidarDropDistance
|
||||||
|
if(form.inventory.grainType !== pond.Grain.GRAIN_CUSTOM){
|
||||||
|
form.inventory.customGrain = undefined
|
||||||
|
}else{
|
||||||
|
form.inventory.customGrain = customGrain
|
||||||
|
}
|
||||||
}
|
}
|
||||||
binAPI
|
binAPI
|
||||||
.updateBin(bin.key(), form, as)
|
.updateBin(bin.key(), form, as)
|
||||||
|
|
@ -1014,7 +1029,37 @@ export default function BinSettings(props: Props) {
|
||||||
{isCustom ? "Custom" : "Grain"}
|
{isCustom ? "Custom" : "Grain"}
|
||||||
</Typography> */}
|
</Typography> */}
|
||||||
<Box>
|
<Box>
|
||||||
{!isCustomInventory && (
|
{isCustomInventory ?
|
||||||
|
<React.Fragment>
|
||||||
|
{storageType === pond.BinStorage.BIN_STORAGE_FERTILIZER ?
|
||||||
|
<React.Fragment>
|
||||||
|
<TextField
|
||||||
|
label="Type"
|
||||||
|
value={customTypeName}
|
||||||
|
type="text"
|
||||||
|
onChange={event => {
|
||||||
|
updateForm(
|
||||||
|
"inventory",
|
||||||
|
pond.BinInventory.create({
|
||||||
|
...form.inventory,
|
||||||
|
customTypeName: event.target.value
|
||||||
|
})
|
||||||
|
);
|
||||||
|
updateFormExtension("customTypeName", event.target.value);
|
||||||
|
}}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
className={classes.bottomSpacing}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
:
|
||||||
|
<Box sx={{border: "1px solid white", borderRadius: 2, padding: 2, marginBottom: 2}}>
|
||||||
|
<CustomGrainSelector initialGrain={customGrain} onGrainSettingsChange={(newGrainSettings) => {setCustomGrain(newGrainSettings)}}/>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
</React.Fragment>
|
||||||
|
:
|
||||||
|
<React.Fragment>
|
||||||
<Box className={classes.bottomSpacing}>
|
<Box className={classes.bottomSpacing}>
|
||||||
<SearchSelect
|
<SearchSelect
|
||||||
label="Type"
|
label="Type"
|
||||||
|
|
@ -1040,49 +1085,6 @@ export default function BinSettings(props: Props) {
|
||||||
options={grainOptions}
|
options={grainOptions}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
|
||||||
{isCustomInventory ? (
|
|
||||||
<React.Fragment>
|
|
||||||
<TextField
|
|
||||||
label="Type"
|
|
||||||
value={customTypeName}
|
|
||||||
type="text"
|
|
||||||
onChange={event => {
|
|
||||||
updateForm(
|
|
||||||
"inventory",
|
|
||||||
pond.BinInventory.create({
|
|
||||||
...form.inventory,
|
|
||||||
customTypeName: event.target.value
|
|
||||||
})
|
|
||||||
);
|
|
||||||
updateFormExtension("customTypeName", event.target.value);
|
|
||||||
}}
|
|
||||||
fullWidth
|
|
||||||
variant="outlined"
|
|
||||||
className={classes.bottomSpacing}
|
|
||||||
/>
|
|
||||||
{storageType !== pond.BinStorage.BIN_STORAGE_FERTILIZER && (
|
|
||||||
<TextField
|
|
||||||
label="Bushels Per Tonne"
|
|
||||||
value={bushPerTonne}
|
|
||||||
type="number"
|
|
||||||
onChange={event => {
|
|
||||||
updateForm(
|
|
||||||
"inventory",
|
|
||||||
pond.BinInventory.create({
|
|
||||||
...form.inventory,
|
|
||||||
bushelsPerTonne: +event.target.value
|
|
||||||
})
|
|
||||||
);
|
|
||||||
updateFormExtension("bushelsPerTonne", event.target.value);
|
|
||||||
}}
|
|
||||||
fullWidth
|
|
||||||
variant="outlined"
|
|
||||||
className={classes.bottomSpacing}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
|
||||||
) : (
|
|
||||||
<TextField
|
<TextField
|
||||||
label="Grain Variant"
|
label="Grain Variant"
|
||||||
value={grainSubtype}
|
value={grainSubtype}
|
||||||
|
|
@ -1102,7 +1104,8 @@ export default function BinSettings(props: Props) {
|
||||||
disabled={!grainType}
|
disabled={!grainType}
|
||||||
className={classes.bottomSpacing}
|
className={classes.bottomSpacing}
|
||||||
/>
|
/>
|
||||||
)}
|
</React.Fragment>
|
||||||
|
}
|
||||||
{getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_BUSHELS ||
|
{getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_BUSHELS ||
|
||||||
storageType === pond.BinStorage.BIN_STORAGE_FERTILIZER ||
|
storageType === pond.BinStorage.BIN_STORAGE_FERTILIZER ||
|
||||||
formExtension.bushelsPerTonne === "0" ||
|
formExtension.bushelsPerTonne === "0" ||
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ import Edit from "@mui/icons-material/Edit";
|
||||||
import { makeStyles } from "@mui/styles";
|
import { makeStyles } from "@mui/styles";
|
||||||
import ButtonGroup from "common/ButtonGroup";
|
import ButtonGroup from "common/ButtonGroup";
|
||||||
import ModeChangeDialog from "./conditioning/modeChangeDialog";
|
import ModeChangeDialog from "./conditioning/modeChangeDialog";
|
||||||
|
import CustomGrainSelector from "grain/CustomGrainSelector";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => {
|
const useStyles = makeStyles((theme: Theme) => {
|
||||||
return ({
|
return ({
|
||||||
|
|
@ -286,6 +287,7 @@ export default function BinVisualizer(props: Props) {
|
||||||
|
|
||||||
const [openModeChange, setOpenModeChange] = useState(false)
|
const [openModeChange, setOpenModeChange] = useState(false)
|
||||||
const [modeChangeInProgress, setModeChangeInProgress] = useState(false)
|
const [modeChangeInProgress, setModeChangeInProgress] = useState(false)
|
||||||
|
const [newGrainSettings, setNewGrainSettings] = useState<pond.GrainSettings>()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setModeTime(moment(bin.status.lastModeChange));
|
setModeTime(moment(bin.status.lastModeChange));
|
||||||
|
|
@ -327,16 +329,26 @@ export default function BinVisualizer(props: Props) {
|
||||||
setBushPerTonne(bin.settings.inventory.bushelsPerTonne.toFixed(2));
|
setBushPerTonne(bin.settings.inventory.bushelsPerTonne.toFixed(2));
|
||||||
setGrainSubtype(bin.subtype());
|
setGrainSubtype(bin.subtype());
|
||||||
setNewBinMode(bin.settings.mode);
|
setNewBinMode(bin.settings.mode);
|
||||||
|
setNewGrainSettings(bin.customGrain());
|
||||||
}
|
}
|
||||||
if (bin.settings) {
|
if (bin.settings) {
|
||||||
let t = bin.settings.outdoorTemp;
|
let t = 0
|
||||||
|
let h = 0
|
||||||
|
if(ambient){
|
||||||
|
t = ambient.temperature
|
||||||
|
h = ambient.humidity
|
||||||
|
}else{
|
||||||
|
t = bin.settings.outdoorTemp;//use what is already set in the bin settings
|
||||||
|
h = bin.settings.outdoorHumidity
|
||||||
|
}
|
||||||
|
|
||||||
if (user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
if (user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
||||||
t = CtoF(t);
|
t = CtoF(t);
|
||||||
}
|
}
|
||||||
setOutdoorTemp(t);
|
setOutdoorTemp(t);
|
||||||
|
setOutdoorHumidity(h)
|
||||||
}
|
}
|
||||||
if (bin.settings) setOutdoorHumidity(bin.settings.outdoorHumidity);
|
}, [bin, user, ambient]);
|
||||||
}, [bin, user]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
//if(loadingTrend) return
|
//if(loadingTrend) return
|
||||||
|
|
@ -515,18 +527,13 @@ export default function BinVisualizer(props: Props) {
|
||||||
const capacity = bin.settings.specs?.bushelCapacity ?? 0;
|
const capacity = bin.settings.specs?.bushelCapacity ?? 0;
|
||||||
const grainBushels = bin.bushels();
|
const grainBushels = bin.bushels();
|
||||||
const isEmpty = bin.settings.inventory?.empty === true || !grainBushels || grainBushels <= 0;
|
const isEmpty = bin.settings.inventory?.empty === true || !grainBushels || grainBushels <= 0;
|
||||||
const grainType = bin.settings.inventory?.grainType;
|
|
||||||
const grainTypeName = isEmpty || !grainType ? "" : GrainDescriber(grainType).name;
|
|
||||||
const customTypeName = bin.settings.inventory?.customTypeName;
|
|
||||||
const grainSubtype = bin.settings.inventory?.grainSubtype;
|
const grainSubtype = bin.settings.inventory?.grainSubtype;
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
{/* grain display */}
|
{/* grain display */}
|
||||||
<Box display="flex" justifyContent="space-between">
|
<Box display="flex" justifyContent="space-between">
|
||||||
<Typography variant="subtitle2" color="textPrimary" style={{ fontWeight: 800 }}>
|
<Typography variant="subtitle2" color="textPrimary" style={{ fontWeight: 800 }}>
|
||||||
{bin.storage() === pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN
|
{isEmpty ? "" : bin.grainName()}
|
||||||
? grainTypeName
|
|
||||||
: customTypeName}
|
|
||||||
{grainSubtype !== "" ? " - " + grainSubtype : ""}
|
{grainSubtype !== "" ? " - " + grainSubtype : ""}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box
|
<Box
|
||||||
|
|
@ -768,6 +775,7 @@ export default function BinVisualizer(props: Props) {
|
||||||
setGrainType(bin.settings.inventory.grainType);
|
setGrainType(bin.settings.inventory.grainType);
|
||||||
setGrainOption(ToGrainOption(bin.settings.inventory.grainType));
|
setGrainOption(ToGrainOption(bin.settings.inventory.grainType));
|
||||||
setBushPerTonne(bin.settings.inventory.bushelsPerTonne.toFixed(2));
|
setBushPerTonne(bin.settings.inventory.bushelsPerTonne.toFixed(2));
|
||||||
|
setNewGrainSettings(bin.customGrain())
|
||||||
setGrainSubtype(bin.subtype());
|
setGrainSubtype(bin.subtype());
|
||||||
}
|
}
|
||||||
setGrainChangeDialog(false);
|
setGrainChangeDialog(false);
|
||||||
|
|
@ -799,11 +807,13 @@ export default function BinVisualizer(props: Props) {
|
||||||
setCustomTypeName("");
|
setCustomTypeName("");
|
||||||
setGrainOption(ToGrainOption(pond.Grain.GRAIN_NONE));
|
setGrainOption(ToGrainOption(pond.Grain.GRAIN_NONE));
|
||||||
if (checked) {
|
if (checked) {
|
||||||
setStorageType(pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN);
|
|
||||||
setGrainType(pond.Grain.GRAIN_CUSTOM);
|
|
||||||
} else {
|
|
||||||
setStorageType(pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN);
|
setStorageType(pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN);
|
||||||
|
setGrainType(pond.Grain.GRAIN_CUSTOM);
|
||||||
|
setNewGrainSettings(bin.customGrain())
|
||||||
|
} else {
|
||||||
|
setStorageType(pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN);
|
||||||
setGrainType(pond.Grain.GRAIN_NONE);
|
setGrainType(pond.Grain.GRAIN_NONE);
|
||||||
|
setNewGrainSettings(undefined)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
name="storage"
|
name="storage"
|
||||||
|
|
@ -834,7 +844,7 @@ export default function BinVisualizer(props: Props) {
|
||||||
)}
|
)}
|
||||||
{isCustomInventory ? (
|
{isCustomInventory ? (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<TextField
|
{/* <TextField
|
||||||
label="Type"
|
label="Type"
|
||||||
value={customTypeName}
|
value={customTypeName}
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -859,7 +869,8 @@ export default function BinVisualizer(props: Props) {
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
className={classes.bottomSpacing}
|
className={classes.bottomSpacing}
|
||||||
/>
|
/>
|
||||||
)}
|
)} */}
|
||||||
|
<CustomGrainSelector initialGrain={bin.customGrain()} onGrainSettingsChange={settings => {setNewGrainSettings(settings)}}/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
) : (
|
) : (
|
||||||
<TextField
|
<TextField
|
||||||
|
|
@ -1404,6 +1415,7 @@ export default function BinVisualizer(props: Props) {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
inventoryControl={bin.inventoryControl()}
|
inventoryControl={bin.inventoryControl()}
|
||||||
|
customGrain={bin.customGrain()}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -1574,6 +1586,12 @@ export default function BinVisualizer(props: Props) {
|
||||||
const fanPerformance = () => {
|
const fanPerformance = () => {
|
||||||
let totalCFM = bin.status.fanCfm;
|
let totalCFM = bin.status.fanCfm;
|
||||||
let bushelCFM = bin.status.cfmPerBushel;
|
let bushelCFM = bin.status.cfmPerBushel;
|
||||||
|
let emc = ExtractMoisture(
|
||||||
|
bin.grain(),
|
||||||
|
activePlenum?.tempHumidity?.temperature ?? 0,
|
||||||
|
activePlenum?.tempHumidity?.humidity ?? 0,
|
||||||
|
bin.customGrain()
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Typography style={{ fontSize: isMobile ? "0.85rem" : "1.0rem", fontWeight: 650 }}>
|
<Typography style={{ fontSize: isMobile ? "0.85rem" : "1.0rem", fontWeight: 650 }}>
|
||||||
|
|
@ -1616,20 +1634,18 @@ export default function BinVisualizer(props: Props) {
|
||||||
{cfmDryWarning(bushelCFM)}
|
{cfmDryWarning(bushelCFM)}
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid >
|
<Grid>
|
||||||
<Box className={classes.lightBox}>
|
{emc !== activePlenum?.tempHumidity?.humidity &&
|
||||||
<Typography align="center" style={{ fontWeight: 650, color: emcColour }}>
|
<Box className={classes.lightBox}>
|
||||||
{ExtractMoisture(
|
<Typography align="center" style={{ fontWeight: 650, color: emcColour }}>
|
||||||
bin.grain(),
|
{emc.toFixed(2)}
|
||||||
activePlenum?.tempHumidity?.temperature ?? 0,
|
%
|
||||||
activePlenum?.tempHumidity?.humidity ?? 0
|
</Typography>
|
||||||
).toFixed(2)}
|
<Typography align="center" style={{ fontWeight: 650 }}>
|
||||||
%
|
Plenum EMC
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography align="center" style={{ fontWeight: 650 }}>
|
</Box>
|
||||||
Plenum EMC
|
}
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -1638,6 +1654,12 @@ export default function BinVisualizer(props: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const ambientDisplay = () => {
|
const ambientDisplay = () => {
|
||||||
|
let emc = ExtractMoisture(
|
||||||
|
bin.grain(),
|
||||||
|
ambient?.temperature ?? 0,
|
||||||
|
ambient?.humidity ?? 0,
|
||||||
|
bin.customGrain()
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Typography style={{ fontSize: isMobile ? "0.85rem" : "1.0rem", fontWeight: 650 }}>
|
<Typography style={{ fontSize: isMobile ? "0.85rem" : "1.0rem", fontWeight: 650 }}>
|
||||||
|
|
@ -1677,19 +1699,17 @@ export default function BinVisualizer(props: Props) {
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid >
|
<Grid >
|
||||||
<Box className={classes.lightBox}>
|
{emc !== ambient?.humidity &&
|
||||||
<Typography align="center" style={{ fontWeight: 650, color: emcColour }}>
|
<Box className={classes.lightBox}>
|
||||||
{ExtractMoisture(
|
<Typography align="center" style={{ fontWeight: 650, color: emcColour }}>
|
||||||
bin.grain(),
|
{emc.toFixed(2)}
|
||||||
ambient?.temperature ?? 0,
|
%
|
||||||
ambient?.humidity ?? 0
|
</Typography>
|
||||||
).toFixed(2)}
|
<Typography align="center" style={{ fontWeight: 650 }}>
|
||||||
%
|
Ambient EMC
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography align="center" style={{ fontWeight: 650 }}>
|
</Box>
|
||||||
Ambient EMC
|
}
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -1968,6 +1988,7 @@ export default function BinVisualizer(props: Props) {
|
||||||
? 0
|
? 0
|
||||||
: parseFloat(bushPerTonne);
|
: parseFloat(bushPerTonne);
|
||||||
b.settings.inventory.grainSubtype = grainSubtype;
|
b.settings.inventory.grainSubtype = grainSubtype;
|
||||||
|
b.settings.inventory.customGrain = newGrainSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
if (b.settings.outdoorTemp !== undefined) {
|
if (b.settings.outdoorTemp !== undefined) {
|
||||||
|
|
@ -1986,7 +2007,7 @@ export default function BinVisualizer(props: Props) {
|
||||||
}
|
}
|
||||||
b.settings.mode = newBinMode;
|
b.settings.mode = newBinMode;
|
||||||
}
|
}
|
||||||
|
console.log(b.settings)
|
||||||
binAPI.updateBin(bin.key(), b.settings, as).then(resp => {
|
binAPI.updateBin(bin.key(), b.settings, as).then(resp => {
|
||||||
refresh();
|
refresh();
|
||||||
});
|
});
|
||||||
|
|
@ -2013,8 +2034,8 @@ export default function BinVisualizer(props: Props) {
|
||||||
if(refresh) updateBin();
|
if(refresh) updateBin();
|
||||||
}}
|
}}
|
||||||
defaultTargetMoisture={bin.settings.inventory?.initialMoisture}
|
defaultTargetMoisture={bin.settings.inventory?.initialMoisture}
|
||||||
// defaultOutdoorTemp={get this from any ambient sensors}
|
defaultOutdoorTemp={outdoorTemp}
|
||||||
// defaultOutdoorHumidity={get this from any ambient sensors}
|
defaultOutdoorHumidity={outdoorHumidity}
|
||||||
changeTargetMoisture={newMoisture => {
|
changeTargetMoisture={newMoisture => {
|
||||||
setTargetMoisture(newMoisture)
|
setTargetMoisture(newMoisture)
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -108,9 +108,9 @@ export default function ModeChangeDialog(props: Props){
|
||||||
const [deviceOption, setDeviceOption] = useState<Option>()
|
const [deviceOption, setDeviceOption] = useState<Option>()
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
setMoistureInput(defaultTargetMoisture?.toFixed(2) ?? "0")
|
setMoistureInput(defaultTargetMoisture?.toString() ?? "0")
|
||||||
setOutdoorTempInput(defaultOutdoorTemp?.toFixed(2) ?? "0")
|
setOutdoorTempInput(defaultOutdoorTemp?.toString() ?? "0")
|
||||||
setOutdoorHumidityInput(defaultOutdoorHumidity?.toFixed(2) ?? "0")
|
setOutdoorHumidityInput(defaultOutdoorHumidity?.toString() ?? "0")
|
||||||
},[defaultTargetMoisture, defaultOutdoorTemp, defaultOutdoorHumidity])
|
},[defaultTargetMoisture, defaultOutdoorTemp, defaultOutdoorHumidity])
|
||||||
|
|
||||||
//get the interactions and components for the device when it is selected from the dropdown
|
//get the interactions and components for the device when it is selected from the dropdown
|
||||||
|
|
@ -436,7 +436,9 @@ if (!selectedDevice) return;
|
||||||
}
|
}
|
||||||
stage1.steps.push(newStep);
|
stage1.steps.push(newStep);
|
||||||
});
|
});
|
||||||
stages.push(stage1)
|
if(stage1.steps.length > 0){
|
||||||
|
stages.push(stage1)
|
||||||
|
}
|
||||||
//stage two is to add the new interactions
|
//stage two is to add the new interactions
|
||||||
let stage2: Stage = {
|
let stage2: Stage = {
|
||||||
title: "Add New Interactions",
|
title: "Add New Interactions",
|
||||||
|
|
@ -447,7 +449,9 @@ if (!selectedDevice) return;
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
stages.push(stage2)
|
if(stage2.steps.length > 0){
|
||||||
|
stages.push(stage2)
|
||||||
|
}
|
||||||
|
|
||||||
//stage three is to update the controller components
|
//stage three is to update the controller components
|
||||||
let stage3: Stage = {
|
let stage3: Stage = {
|
||||||
|
|
@ -463,7 +467,9 @@ if (!selectedDevice) return;
|
||||||
stage3.steps.push(newStep)
|
stage3.steps.push(newStep)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
stages.push(stage3)
|
if(stage3.steps.length > 0){
|
||||||
|
stages.push(stage3)
|
||||||
|
}
|
||||||
//set those stages to a state variable
|
//set those stages to a state variable
|
||||||
setPromiseStages(stages)
|
setPromiseStages(stages)
|
||||||
}
|
}
|
||||||
|
|
@ -618,11 +624,7 @@ if (!selectedDevice) return;
|
||||||
onChange={event => {
|
onChange={event => {
|
||||||
setOutdoorTempInput(event.target.value)
|
setOutdoorTempInput(event.target.value)
|
||||||
if(changeOutdoorTemp && !isNaN(parseFloat(event.target.value))){
|
if(changeOutdoorTemp && !isNaN(parseFloat(event.target.value))){
|
||||||
//if the users prefs are F will need to convert it to C when sending it back
|
|
||||||
let val = parseFloat(event.target.value)
|
let val = parseFloat(event.target.value)
|
||||||
if(getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT){
|
|
||||||
val = fahrenheitToCelsius(val)
|
|
||||||
}
|
|
||||||
changeOutdoorTemp(val)
|
changeOutdoorTemp(val)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
@ -721,7 +723,7 @@ if (!selectedDevice) return;
|
||||||
<Box>
|
<Box>
|
||||||
<PromiseProgress
|
<PromiseProgress
|
||||||
stages={promiseStages}
|
stages={promiseStages}
|
||||||
automaticStart
|
description="These are the changes that will occur in order to change your bin mode with the given options. Press start to begin."
|
||||||
failFast
|
failFast
|
||||||
onStart={() => {
|
onStart={() => {
|
||||||
startChange()
|
startChange()
|
||||||
|
|
|
||||||
|
|
@ -170,18 +170,18 @@ export default function BinGraphs(props: Props) {
|
||||||
setEndDate(newEndDate);
|
setEndDate(newEndDate);
|
||||||
};
|
};
|
||||||
|
|
||||||
const determineGrainColour = () => {
|
// const determineGrainColour = () => {
|
||||||
let col = "yellow";
|
// let col = "yellow";
|
||||||
let binInv = bin.settings.inventory;
|
// let binInv = bin.settings.inventory;
|
||||||
if (binInv) {
|
// if (binInv) {
|
||||||
if (binInv.grainType === pond.Grain.GRAIN_CUSTOM) {
|
// if (binInv.grainType === pond.Grain.GRAIN_CUSTOM) {
|
||||||
col = stringToMaterialColour(binInv.customTypeName);
|
// col = stringToMaterialColour(binInv.customTypeName);
|
||||||
} else if (binInv.grainType !== pond.Grain.GRAIN_NONE) {
|
// } else if (binInv.grainType !== pond.Grain.GRAIN_NONE) {
|
||||||
col = GrainDescriber(binInv.grainType).colour;
|
// col = GrainDescriber(binInv.grainType).colour;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
return col;
|
// return col;
|
||||||
};
|
// };
|
||||||
|
|
||||||
const inventoryGraph = () => {
|
const inventoryGraph = () => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -193,7 +193,7 @@ export default function BinGraphs(props: Props) {
|
||||||
endDate={endDate}
|
endDate={endDate}
|
||||||
binLoading={binLoading}
|
binLoading={binLoading}
|
||||||
bin={bin}
|
bin={bin}
|
||||||
colour={determineGrainColour()}
|
colour={bin.grainColour()}
|
||||||
fertilizerBin={bin.settings.storage === pond.BinStorage.BIN_STORAGE_FERTILIZER}
|
fertilizerBin={bin.settings.storage === pond.BinStorage.BIN_STORAGE_FERTILIZER}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,8 @@ export default function BinGraphsTrending(props: Props) {
|
||||||
trend: ExtractMoisture(
|
trend: ExtractMoisture(
|
||||||
bin.grain(),
|
bin.grain(),
|
||||||
val.values[0],
|
val.values[0],
|
||||||
plenumHumidity.values[i].values[0]
|
plenumHumidity.values[i].values[0],
|
||||||
|
bin.customGrain()
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
trendData.push(trendPoint);
|
trendData.push(trendPoint);
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import { pond } from "protobuf-ts/pond";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { makeStyles } from "@mui/styles";
|
import { makeStyles } from "@mui/styles";
|
||||||
import { useGlobalState } from "providers";
|
import { useGlobalState } from "providers";
|
||||||
|
import { getContextKeys, getContextTypes } from "pbHelpers/Context";
|
||||||
|
|
||||||
interface TabPanelProps {
|
interface TabPanelProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
|
@ -116,13 +117,7 @@ export default function DeviceLinkDrawer(props: Props) {
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
true,
|
true
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
as
|
|
||||||
)
|
)
|
||||||
.then(resp => {
|
.then(resp => {
|
||||||
let devMap = new Map<string, pond.ComprehensiveDevice>();
|
let devMap = new Map<string, pond.ComprehensiveDevice>();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { AxiosResponse } from "axios"
|
import { AxiosResponse } from "axios"
|
||||||
import { Box, Button, CircularProgress, DialogActions, DialogContent, DialogTitle, Typography } from "@mui/material"
|
import { Box, Button, CircularProgress, DialogActions, DialogContent, DialogTitle, Divider, Typography } from "@mui/material"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { CheckCircle, Error, Pending } from "@mui/icons-material"
|
import { CheckCircle, Error, Pending } from "@mui/icons-material"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
@ -18,7 +18,7 @@ export interface Step {
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
stages: Stage[]
|
stages: Stage[]
|
||||||
automaticStart: boolean
|
description?: string
|
||||||
failFast?: boolean
|
failFast?: boolean
|
||||||
onStart?: () => void
|
onStart?: () => void
|
||||||
onComplete?: () => void
|
onComplete?: () => void
|
||||||
|
|
@ -32,7 +32,7 @@ enum progress {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PromiseProgress(props: Props){
|
export function PromiseProgress(props: Props){
|
||||||
const {stages, failFast, onStart, onComplete, automaticStart} = props
|
const {stages, failFast, onStart, onComplete, description } = props
|
||||||
const [completion, setCompletion] = useState<Map<string, progress>>(new Map())
|
const [completion, setCompletion] = useState<Map<string, progress>>(new Map())
|
||||||
|
|
||||||
const execute = () => {
|
const execute = () => {
|
||||||
|
|
@ -84,12 +84,6 @@ export function PromiseProgress(props: Props){
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if(automaticStart){
|
|
||||||
execute()
|
|
||||||
}
|
|
||||||
},[automaticStart, execute])
|
|
||||||
|
|
||||||
const getCompletion = (completion?: progress) => {
|
const getCompletion = (completion?: progress) => {
|
||||||
switch(completion){
|
switch(completion){
|
||||||
case progress.InProgress:
|
case progress.InProgress:
|
||||||
|
|
@ -123,16 +117,17 @@ export function PromiseProgress(props: Props){
|
||||||
Progress
|
Progress
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
{stages.map((stage, number) => displayStage(stage, number))}
|
<Typography>{description}</Typography>
|
||||||
|
<Box marginTop={2}>
|
||||||
|
{stages.map((stage, number) => displayStage(stage, number))}
|
||||||
|
</Box>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
{!automaticStart &&
|
<Button variant="contained" color="primary" onClick={() => {
|
||||||
<Button onClick={() => {
|
execute()
|
||||||
execute()
|
}}>
|
||||||
}}>
|
Start
|
||||||
Start
|
</Button>
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import {
|
||||||
AccordionSummary,
|
AccordionSummary,
|
||||||
Alert,
|
Alert,
|
||||||
AlertTitle,
|
AlertTitle,
|
||||||
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Collapse,
|
Collapse,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
|
@ -47,6 +48,7 @@ import { getDistanceUnit } from "utils";
|
||||||
import { GrainOptions } from "grain";
|
import { GrainOptions } from "grain";
|
||||||
import CompModes from "component/ComponentMode.json";
|
import CompModes from "component/ComponentMode.json";
|
||||||
import FanPicker from "fans/fanPicker";
|
import FanPicker from "fans/fanPicker";
|
||||||
|
import CustomGrainSelector from "grain/CustomGrainSelector";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => {
|
const useStyles = makeStyles((theme: Theme) => {
|
||||||
return ({
|
return ({
|
||||||
|
|
@ -136,6 +138,7 @@ export default function ComponentForm(props: Props) {
|
||||||
sensorDistance: "0",
|
sensorDistance: "0",
|
||||||
});
|
});
|
||||||
const [compMode, setCompMode] = useState<any>();
|
const [compMode, setCompMode] = useState<any>();
|
||||||
|
const [useCustomGrain, setUseCustomGrain] = useState<boolean>(false)
|
||||||
//const [numCalibrations, setNumCalibrations] = useState(0)
|
//const [numCalibrations, setNumCalibrations] = useState(0)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -192,6 +195,10 @@ export default function ComponentForm(props: Props) {
|
||||||
sensorDistance = sensorDistance / 30.48;
|
sensorDistance = sensorDistance / 30.48;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(formComponent.settings.customGrain){
|
||||||
|
setUseCustomGrain(true)
|
||||||
|
}
|
||||||
|
|
||||||
setForm({
|
setForm({
|
||||||
component: formComponent,
|
component: formComponent,
|
||||||
measure: formComponent.settings.measurementPeriodMs > 0,
|
measure: formComponent.settings.measurementPeriodMs > 0,
|
||||||
|
|
@ -383,6 +390,13 @@ export default function ComponentForm(props: Props) {
|
||||||
setForm(f);
|
setForm(f);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateCustomGrain = (grainSettings?: pond.GrainSettings) => {
|
||||||
|
let f = cloneDeep(form)
|
||||||
|
f.component.settings.customGrain = grainSettings
|
||||||
|
f.component.settings.grainType = pond.Grain.GRAIN_CUSTOM
|
||||||
|
setForm(f)
|
||||||
|
}
|
||||||
|
|
||||||
const updateGrainType = (option: Option | null) => {
|
const updateGrainType = (option: Option | null) => {
|
||||||
let f = cloneDeep(form);
|
let f = cloneDeep(form);
|
||||||
|
|
||||||
|
|
@ -622,13 +636,36 @@ export default function ComponentForm(props: Props) {
|
||||||
const grainSelect = () => {
|
const grainSelect = () => {
|
||||||
let selected = findSelectedGrain(grainOptions);
|
let selected = findSelectedGrain(grainOptions);
|
||||||
return (
|
return (
|
||||||
<SearchSelect
|
<React.Fragment>
|
||||||
selected={selected}
|
<FormControlLabel
|
||||||
changeSelection={updateGrainType}
|
control={
|
||||||
label="Grain Type"
|
<Switch
|
||||||
options={grainOptions}
|
value={useCustomGrain}
|
||||||
group
|
checked={useCustomGrain}
|
||||||
/>
|
title="Custom Grain"
|
||||||
|
onClick={() => {
|
||||||
|
setUseCustomGrain(!useCustomGrain);
|
||||||
|
updateCustomGrain()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Custom Grain"
|
||||||
|
labelPlacement="start"
|
||||||
|
/>
|
||||||
|
{useCustomGrain ?
|
||||||
|
<Box sx={{border: "1px solid white", borderRadius: 2, padding: 2, marginBottom: 2}}>
|
||||||
|
<CustomGrainSelector onGrainSettingsChange={updateCustomGrain} initialGrain={form.component.customGrainProps()}/>
|
||||||
|
</Box>
|
||||||
|
:
|
||||||
|
<SearchSelect
|
||||||
|
selected={selected}
|
||||||
|
changeSelection={updateGrainType}
|
||||||
|
label="Grain Type"
|
||||||
|
options={grainOptions}
|
||||||
|
group
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { Pressure } from "models/Pressure";
|
||||||
import { GrainCable } from "models/GrainCable";
|
import { GrainCable } from "models/GrainCable";
|
||||||
import { extension, Summary } from "pbHelpers/ComponentType";
|
import { extension, Summary } from "pbHelpers/ComponentType";
|
||||||
import { makeStyles } from "@mui/styles";
|
import { makeStyles } from "@mui/styles";
|
||||||
|
import { stringToMaterialColour } from "utils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
component: Component | Plenum | Pressure | GrainCable;
|
component: Component | Plenum | Pressure | GrainCable;
|
||||||
|
|
@ -100,13 +101,26 @@ export default function UnitMeasurementSummary(props: Props) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const grainName = () => {
|
||||||
|
if(component.settings.grainType === pond.Grain.GRAIN_CUSTOM && component.settings.customGrain?.name){
|
||||||
|
return component.settings.customGrain.name
|
||||||
|
}
|
||||||
|
return GrainDescriber(component.settings.grainType).name
|
||||||
|
}
|
||||||
|
|
||||||
|
const grainColor = () => {
|
||||||
|
if(component.settings.grainType === pond.Grain.GRAIN_CUSTOM && component.settings.customGrain?.name){
|
||||||
|
return stringToMaterialColour(component.settings.customGrain.name)
|
||||||
|
}
|
||||||
|
return GrainDescriber(component.settings.grainType).colour
|
||||||
|
}
|
||||||
|
|
||||||
const getSummaryComponent = (summaries: Summary[]): any => {
|
const getSummaryComponent = (summaries: Summary[]): any => {
|
||||||
if (summaries.length < 1) {
|
if (summaries.length < 1) {
|
||||||
return noSummary();
|
return noSummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
let overlays: JSX.Element[] = [];
|
let overlays: JSX.Element[] = [];
|
||||||
let grain = GrainDescriber(component.settings.grainType);
|
|
||||||
return (
|
return (
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid size={{ xs: 12 }}>
|
<Grid size={{ xs: 12 }}>
|
||||||
|
|
@ -170,7 +184,7 @@ export default function UnitMeasurementSummary(props: Props) {
|
||||||
component.settings.grainType !== pond.Grain.GRAIN_INVALID && (
|
component.settings.grainType !== pond.Grain.GRAIN_INVALID && (
|
||||||
<Typography variant={largeText ? "body1" : "caption"} color="textPrimary">
|
<Typography variant={largeText ? "body1" : "caption"} color="textPrimary">
|
||||||
<span key={"grainType"}>
|
<span key={"grainType"}>
|
||||||
Grain Type: <span style={{ color: grain.colour }}>{grain.name}</span>
|
Grain Type: <span style={{ color: grainColor() }}>{grainName()}</span>
|
||||||
</span>
|
</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Button, DialogActions, DialogContent, DialogTitle, Typography } from "@mui/material";
|
import { CheckBox } from "@mui/icons-material";
|
||||||
|
import { Button, Checkbox, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, FormLabel, InputAdornment, TextField, Typography } from "@mui/material";
|
||||||
import ResponsiveDialog from "common/ResponsiveDialog";
|
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||||
import { Component, Device } from "models";
|
import { Component, Device } from "models";
|
||||||
import { Gate } from "models/Gate";
|
import { Gate } from "models/Gate";
|
||||||
|
|
@ -6,6 +7,7 @@ import moment from "moment";
|
||||||
import { pond, quack } from "protobuf-ts/pond";
|
import { pond, quack } from "protobuf-ts/pond";
|
||||||
import { useGlobalState, useInteractionsAPI, useSnackbar } from "providers";
|
import { useGlobalState, useInteractionsAPI, useSnackbar } from "providers";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { getPressureUnit } from "utils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
@ -37,6 +39,7 @@ const densityMap = new Map<number, number>([
|
||||||
export default function GateDeviceInteraction(props: Props) {
|
export default function GateDeviceInteraction(props: Props) {
|
||||||
const { open, close, gate, compDevice, densityTemp } = props;
|
const { open, close, gate, compDevice, densityTemp } = props;
|
||||||
const [{ user, as }] = useGlobalState();
|
const [{ user, as }] = useGlobalState();
|
||||||
|
const [device, setDevice] = useState<Device>(Device.create())
|
||||||
const [lowDelta, setLowDelta] = useState(0);
|
const [lowDelta, setLowDelta] = useState(0);
|
||||||
const [highDelta, setHighDelta] = useState(0);
|
const [highDelta, setHighDelta] = useState(0);
|
||||||
const [greenComponent, setGreenComponent] = useState<Component>();
|
const [greenComponent, setGreenComponent] = useState<Component>();
|
||||||
|
|
@ -44,7 +47,12 @@ export default function GateDeviceInteraction(props: Props) {
|
||||||
const [pressureComponent, setPressureComponent] = useState<Component>();
|
const [pressureComponent, setPressureComponent] = useState<Component>();
|
||||||
const interactionsAPI = useInteractionsAPI();
|
const interactionsAPI = useInteractionsAPI();
|
||||||
const [adding, setAdding] = useState(false);
|
const [adding, setAdding] = useState(false);
|
||||||
|
//boolean to determine if a third condition should be put on the interaction for the red light, requires a V2 device running at least fromware version 2.1.9
|
||||||
|
const [useRedOffCondition, setUseRedOffCondition] = useState(false)
|
||||||
|
//if the pressure is under this value then the red light should be off
|
||||||
|
const [redThreshold, setRedThreshold] = useState("0")
|
||||||
const { openSnack } = useSnackbar();
|
const { openSnack } = useSnackbar();
|
||||||
|
const [pressureSource, setPressureSource] = useState<quack.ComponentID>(quack.ComponentID.create())
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
//math to determine what the delta pressures to set will be
|
//math to determine what the delta pressures to set will be
|
||||||
|
|
@ -95,46 +103,64 @@ export default function GateDeviceInteraction(props: Props) {
|
||||||
gate.preferences[component.key()] === pond.GateComponentType.GATE_COMPONENT_TYPE_PRESSURE
|
gate.preferences[component.key()] === pond.GateComponentType.GATE_COMPONENT_TYPE_PRESSURE
|
||||||
) {
|
) {
|
||||||
setPressureComponent(component);
|
setPressureComponent(component);
|
||||||
|
setPressureSource(
|
||||||
|
quack.ComponentID.create({
|
||||||
|
type: component.type(),
|
||||||
|
address: component.settings.address,
|
||||||
|
addressType: component.settings.addressType
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if(compDevice.device){
|
||||||
|
setDevice(Device.create(compDevice.device))
|
||||||
|
}
|
||||||
}, [gate, densityTemp, user, compDevice]);
|
}, [gate, densityTemp, user, compDevice]);
|
||||||
|
|
||||||
// useEffect(() => {
|
const buttonDisabled = () => {
|
||||||
// //load current interactions for the device
|
if (greenComponent === undefined) return true
|
||||||
// let deviceID = Device.any(compDevice.device).id();
|
if (redComponent === undefined) return true
|
||||||
// interactionsAPI.listInteractionsByDevice(deviceID).then(resp => {
|
if (adding) return true
|
||||||
// setCurrentInteractions(resp);
|
if (useRedOffCondition && isNaN(parseFloat(redThreshold))) return true
|
||||||
// });
|
return false
|
||||||
// }, [compDevice.device, interactionsAPI]);
|
}
|
||||||
|
|
||||||
// const removeCurrentInteractions = () => {
|
|
||||||
// let deviceID = Device.any(compDevice.device).id();
|
|
||||||
// currentInteractions.forEach(interaction => {
|
|
||||||
// interactionsAPI.removeInteraction(deviceID, interaction.key());
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
|
|
||||||
const createInteractions = async () => {
|
const createInteractions = async () => {
|
||||||
//the interactions to be made
|
//the interactions to be made
|
||||||
|
|
||||||
//TOGGLE green ON when pressure within range of upper and lower
|
|
||||||
if (
|
if (
|
||||||
greenComponent !== undefined &&
|
greenComponent !== undefined &&
|
||||||
redComponent !== undefined &&
|
redComponent !== undefined &&
|
||||||
pressureComponent !== undefined
|
pressureComponent !== undefined
|
||||||
) {
|
) {
|
||||||
let greenToggle: pond.InteractionSettings = pond.InteractionSettings.create({
|
|
||||||
sink: quack.ComponentID.create({
|
let lightInteractions: pond.InteractionSettings[] = []
|
||||||
type: greenComponent.type(),
|
//making variables for the parameters of the interactions
|
||||||
address: greenComponent.settings.address,
|
const redSink = quack.ComponentID.create({
|
||||||
addressType: greenComponent.settings.addressType
|
type: redComponent.type(),
|
||||||
}),
|
address: redComponent.settings.address,
|
||||||
source: quack.ComponentID.create({
|
addressType: redComponent.settings.addressType
|
||||||
type: pressureComponent.type(),
|
})
|
||||||
address: pressureComponent.settings.address,
|
|
||||||
addressType: pressureComponent.settings.addressType
|
const greenSink = quack.ComponentID.create({
|
||||||
}),
|
type: greenComponent.type(),
|
||||||
|
address: greenComponent.settings.address,
|
||||||
|
addressType: greenComponent.settings.addressType
|
||||||
|
})
|
||||||
|
|
||||||
|
const lightSchedule = pond.InteractionSchedule.create({
|
||||||
|
timezone: moment.tz.guess(),
|
||||||
|
weekdays: ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"],
|
||||||
|
timeOfDayStart: "00:00",
|
||||||
|
timeOfDayEnd: "24:00"
|
||||||
|
})
|
||||||
|
|
||||||
|
//TOGGLE green ON when pressure within range of upper and lower, this will always be the same regardless
|
||||||
|
lightInteractions.push(pond.InteractionSettings.create({
|
||||||
|
sink: greenSink,
|
||||||
|
source: pressureSource,
|
||||||
conditions: [
|
conditions: [
|
||||||
pond.InteractionCondition.create({
|
pond.InteractionCondition.create({
|
||||||
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
|
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
|
||||||
|
|
@ -150,61 +176,146 @@ export default function GateDeviceInteraction(props: Props) {
|
||||||
nodeOne: 2,
|
nodeOne: 2,
|
||||||
nodeTwo: 1,
|
nodeTwo: 1,
|
||||||
subtype: 18,
|
subtype: 18,
|
||||||
schedule: pond.InteractionSchedule.create({
|
schedule: lightSchedule,
|
||||||
timezone: moment.tz.guess(),
|
|
||||||
weekdays: ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"],
|
|
||||||
timeOfDayStart: "00:00",
|
|
||||||
timeOfDayEnd: "24:00"
|
|
||||||
}),
|
|
||||||
result: pond.InteractionResult.create({
|
result: pond.InteractionResult.create({
|
||||||
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE,
|
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE,
|
||||||
value: 1
|
value: 1
|
||||||
})
|
})
|
||||||
});
|
}))
|
||||||
|
|
||||||
//TOGGLE red OFF when pressure within range of upper and lower
|
//if they want the red light off when below a certain pressure, it will need 4 seperate SET interactions
|
||||||
let redToggle: pond.InteractionSettings = pond.InteractionSettings.create({
|
if(useRedOffCondition){
|
||||||
sink: quack.ComponentID.create({
|
//need to make sure the redthreshold is in pascals
|
||||||
type: redComponent.type(),
|
let thresholdPascals = 0
|
||||||
address: redComponent.settings.address,
|
if(!isNaN(parseFloat(redThreshold))){
|
||||||
addressType: redComponent.settings.addressType
|
if(getPressureUnit() === pond.PressureUnit.PRESSURE_UNIT_KILOPASCALS){
|
||||||
}),
|
thresholdPascals = parseFloat(redThreshold)*1000
|
||||||
source: quack.ComponentID.create({
|
}else if (getPressureUnit() === pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER){
|
||||||
type: pressureComponent.type(),
|
thresholdPascals = parseFloat(redThreshold)*249
|
||||||
address: pressureComponent.settings.address,
|
}
|
||||||
addressType: pressureComponent.settings.addressType
|
}
|
||||||
}),
|
|
||||||
conditions: [
|
//SET red on when above high
|
||||||
pond.InteractionCondition.create({
|
lightInteractions.push(pond.InteractionSettings.create({
|
||||||
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
|
sink: redSink,
|
||||||
value: -highDelta,
|
source: pressureSource,
|
||||||
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
|
conditions: [
|
||||||
}),
|
pond.InteractionCondition.create({
|
||||||
pond.InteractionCondition.create({
|
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
|
||||||
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
|
value: -highDelta,
|
||||||
value: -lowDelta,
|
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
|
||||||
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
|
})
|
||||||
|
],
|
||||||
|
nodeOne: 2,
|
||||||
|
nodeTwo: 1,
|
||||||
|
subtype: 18,
|
||||||
|
schedule: lightSchedule,
|
||||||
|
result: pond.InteractionResult.create({
|
||||||
|
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET,
|
||||||
|
value: 1
|
||||||
})
|
})
|
||||||
],
|
}))
|
||||||
nodeOne: 2,
|
//SET red off when below high and above low
|
||||||
nodeTwo: 1,
|
lightInteractions.push(pond.InteractionSettings.create({
|
||||||
subtype: 18,
|
sink: redSink,
|
||||||
schedule: pond.InteractionSchedule.create({
|
source: pressureSource,
|
||||||
timezone: moment.tz.guess(),
|
conditions: [
|
||||||
weekdays: ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"],
|
pond.InteractionCondition.create({
|
||||||
timeOfDayStart: "00:00",
|
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
|
||||||
timeOfDayEnd: "24:00"
|
value: -highDelta,
|
||||||
}),
|
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
|
||||||
result: pond.InteractionResult.create({
|
}),
|
||||||
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE,
|
pond.InteractionCondition.create({
|
||||||
value: 0
|
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
|
||||||
})
|
value: -lowDelta,
|
||||||
});
|
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
|
||||||
|
})
|
||||||
|
],
|
||||||
|
nodeOne: 2,
|
||||||
|
nodeTwo: 1,
|
||||||
|
subtype: 18,
|
||||||
|
schedule: lightSchedule,
|
||||||
|
result: pond.InteractionResult.create({
|
||||||
|
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET,
|
||||||
|
value: 0
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
//SET red on when below low and above threshold
|
||||||
|
lightInteractions.push(pond.InteractionSettings.create({
|
||||||
|
sink: redSink,
|
||||||
|
source: pressureSource,
|
||||||
|
conditions: [
|
||||||
|
pond.InteractionCondition.create({
|
||||||
|
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
|
||||||
|
value: -lowDelta,
|
||||||
|
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
|
||||||
|
}),
|
||||||
|
pond.InteractionCondition.create({
|
||||||
|
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
|
||||||
|
value: -thresholdPascals,
|
||||||
|
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
nodeOne: 2,
|
||||||
|
nodeTwo: 1,
|
||||||
|
subtype: 18,
|
||||||
|
schedule: lightSchedule,
|
||||||
|
result: pond.InteractionResult.create({
|
||||||
|
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET,
|
||||||
|
value: 1
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
//SET red off when below threshold
|
||||||
|
lightInteractions.push(pond.InteractionSettings.create({
|
||||||
|
sink: redSink,
|
||||||
|
source: pressureSource,
|
||||||
|
conditions: [
|
||||||
|
pond.InteractionCondition.create({
|
||||||
|
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
|
||||||
|
value: thresholdPascals,
|
||||||
|
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
subtype: 1,
|
||||||
|
schedule: lightSchedule,
|
||||||
|
result: pond.InteractionResult.create({
|
||||||
|
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET,
|
||||||
|
value: 0
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}else{
|
||||||
|
//otherwise use the regular single TOGGLE interaction
|
||||||
|
//TOGGLE red OFF when pressure within range of upper and lower
|
||||||
|
lightInteractions.push(pond.InteractionSettings.create({
|
||||||
|
sink: redSink,
|
||||||
|
source: pressureSource,
|
||||||
|
conditions: [
|
||||||
|
pond.InteractionCondition.create({
|
||||||
|
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
|
||||||
|
value: -highDelta,
|
||||||
|
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
|
||||||
|
}),
|
||||||
|
pond.InteractionCondition.create({
|
||||||
|
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
|
||||||
|
value: -lowDelta,
|
||||||
|
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
|
||||||
|
})
|
||||||
|
],
|
||||||
|
nodeOne: 2,
|
||||||
|
nodeTwo: 1,
|
||||||
|
subtype: 18,
|
||||||
|
schedule: lightSchedule,
|
||||||
|
result: pond.InteractionResult.create({
|
||||||
|
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE,
|
||||||
|
value: 0
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
let deviceID = Device.any(compDevice.device).id();
|
let deviceID = Device.any(compDevice.device).id();
|
||||||
if (deviceID !== undefined) {
|
if (deviceID !== undefined) {
|
||||||
let multi = pond.MultiInteractionSettings.create({
|
let multi = pond.MultiInteractionSettings.create({
|
||||||
interactions: [greenToggle, redToggle]
|
interactions: lightInteractions
|
||||||
});
|
});
|
||||||
interactionsAPI
|
interactionsAPI
|
||||||
.addMultiInteractions(deviceID, multi, as)
|
.addMultiInteractions(deviceID, multi, as)
|
||||||
|
|
@ -230,11 +341,51 @@ export default function GateDeviceInteraction(props: Props) {
|
||||||
}}>
|
}}>
|
||||||
<DialogTitle>Set Interaction For Light Toggle</DialogTitle>
|
<DialogTitle>Set Interaction For Light Toggle</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
Your Delta Pressures, in pascals, will be:
|
{/* Your Delta Pressures, in pascals, will be:
|
||||||
<Typography>low = {lowDelta}</Typography>
|
<Typography>low = {lowDelta}</Typography>
|
||||||
<Typography>high = {highDelta}</Typography>
|
<Typography>high = {highDelta}</Typography>
|
||||||
<Typography>{greenComponent ? "" : "Green LED Component not found"}</Typography>
|
<Typography>{greenComponent ? "" : "Green LED Component not found"}</Typography>
|
||||||
<Typography>{redComponent ? "" : "Red LED Component not found"}</Typography>
|
<Typography>{redComponent ? "" : "Red LED Component not found"}</Typography> */}
|
||||||
|
<Typography>
|
||||||
|
This will clear existing interactions on the pressure chain and set new ones to toggle the green light on and the red light off
|
||||||
|
when the difference between the pressures falls within this range and vice versa:
|
||||||
|
</Typography>
|
||||||
|
<br />
|
||||||
|
<Typography>Upper Limit: {getPressureUnit() === pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER ? (highDelta/249.089).toFixed(2) + " iwg" : highDelta/1000 + " kPa"}</Typography>
|
||||||
|
<Typography>Lower Limit: {getPressureUnit() === pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER ? (lowDelta/249.089).toFixed(2) + " iwg" : lowDelta/1000 + " kPa"}</Typography>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<Typography>If you would like to have the interactions set in such a way that if the average pressure falls below a given value set the use off condition and set the value to use</Typography>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={useRedOffCondition}
|
||||||
|
onChange={(e, checked) => {
|
||||||
|
setUseRedOffCondition(checked)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={<Typography>Use Off Condition</Typography>}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
type="number"
|
||||||
|
fullWidth
|
||||||
|
disabled={!useRedOffCondition}
|
||||||
|
value={redThreshold}
|
||||||
|
error={isNaN(parseFloat(redThreshold))}
|
||||||
|
helperText={isNaN(parseFloat(redThreshold)) ? "Must be a valid number" : ""}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
{getPressureUnit() === pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER ? "iwg" : "kPa"}
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
onChange={e => {
|
||||||
|
setRedThreshold(e.target.value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -244,9 +395,13 @@ export default function GateDeviceInteraction(props: Props) {
|
||||||
//TODO: Once we figure out how to fix the backend to handle rapid addition/removal of interactions
|
//TODO: Once we figure out how to fix the backend to handle rapid addition/removal of interactions
|
||||||
//removeCurrentInteractions();
|
//removeCurrentInteractions();
|
||||||
setAdding(true);
|
setAdding(true);
|
||||||
createInteractions();
|
interactionsAPI.clearInteractions(device.id(), [pressureSource]).then(resp => {
|
||||||
|
createInteractions();
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
disabled={greenComponent === undefined || redComponent === undefined || adding}>
|
disabled={buttonDisabled()}>
|
||||||
{adding ? "Adding Interaction" : "Create Interaction"}
|
{adding ? "Adding Interaction" : "Create Interaction"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|
|
||||||
|
|
@ -70,13 +70,19 @@ export default function GateFlowGraph(props: Props) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const [flowEvents, setFlowEvents] = useState<pond.AirFlowEvent[]>([]);
|
const [flowEvents, setFlowEvents] = useState<pond.AirFlowEvent[]>([]);
|
||||||
const [eventsLoading, setEventsLoading] = useState(false);
|
const [eventsLoading, setEventsLoading] = useState(false);
|
||||||
//these two constants could be entered by the user at time of retrieval
|
//these two constants could be entered by the user at time of retrieval or set in the gates settings
|
||||||
const eventThreshold = 5; //the threshold that if crossed from one measurement to the next during a flow event will end the current flow event and start a new one
|
const eventThreshold = 5; //the threshold that if crossed from one measurement to the next during a flow event will end the current flow event and start a new one
|
||||||
const idleFlow = 3.5; //the mass air flow that must be crossed in order to start a flow event, anything below this will not start an event but must go below this to end an event
|
//const idleFlow = 3.5; //the mass air flow that must be crossed in order to start a flow event, anything below this will not start an event but must go below this to end an event
|
||||||
|
const [idleFlow, setIdleFlow] = useState(3.5)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loadingChartData) return;
|
if (loadingChartData) return;
|
||||||
if (ambient && pressureComponent) {
|
if (ambient && pressureComponent) {
|
||||||
|
let gateIdle = idleFlow
|
||||||
|
if(gate.settings.idleFlow){
|
||||||
|
gateIdle = gate.settings.idleFlow
|
||||||
|
}
|
||||||
|
|
||||||
let recent: SSAreaDataPoint | undefined;
|
let recent: SSAreaDataPoint | undefined;
|
||||||
setLoadingChartData(true);
|
setLoadingChartData(true);
|
||||||
gateAPI
|
gateAPI
|
||||||
|
|
@ -110,11 +116,11 @@ export default function GateFlowGraph(props: Props) {
|
||||||
|
|
||||||
/** determine runtime */
|
/** determine runtime */
|
||||||
// set the start time if the values is greater than the idleFlow and start is not already set
|
// set the start time if the values is greater than the idleFlow and start is not already set
|
||||||
if (val.airFlow >= idleFlow && !start) {
|
if (val.airFlow >= gateIdle && !start) {
|
||||||
start = time;
|
start = time;
|
||||||
}
|
}
|
||||||
// set the stop time when off or at the last measurements and the start time is set
|
// set the stop time when off or at the last measurements and the start time is set
|
||||||
if ((val.airFlow < idleFlow || i === resp.data.values.length - idleFlow) && start) {
|
if ((val.airFlow < gateIdle || i === resp.data.values.length - gateIdle) && start) {
|
||||||
stop = time;
|
stop = time;
|
||||||
}
|
}
|
||||||
// if both start and stop are set calculate add the timeframe to the total runtime
|
// if both start and stop are set calculate add the timeframe to the total runtime
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,10 @@ import ResponsiveTable, { Column } from "common/ResponsiveTable";
|
||||||
import AddGateFab from "./AddGateFab";
|
import AddGateFab from "./AddGateFab";
|
||||||
import GateSettings from "./GateSettings";
|
import GateSettings from "./GateSettings";
|
||||||
import { useGateAPI, useGlobalState } from "providers";
|
import { useGateAPI, useGlobalState } from "providers";
|
||||||
import { Settings } from "@mui/icons-material";
|
import { CheckCircleOutline, DoNotDisturb, ErrorOutline, HelpOutlineOutlined, Settings } from "@mui/icons-material";
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
|
import moment from "moment";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
//gates: Gate[];
|
//gates: Gate[];
|
||||||
|
|
@ -89,6 +91,19 @@ export default function GateList(props: Props) {
|
||||||
setPageSize(event.target.value);
|
setPageSize(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const displayPCAStatus = (state: pond.PCAState) => {
|
||||||
|
switch(state){
|
||||||
|
case pond.PCAState.PCA_STATE_IN_BOUNDS:
|
||||||
|
return <CheckCircleOutline sx={{color: "green"}}/>
|
||||||
|
case pond.PCAState.PCA_STATE_OUT_BOUNDS:
|
||||||
|
return <ErrorOutline sx={{color: "red"}} />
|
||||||
|
case pond.PCAState.PCA_STATE_OFF:
|
||||||
|
return <DoNotDisturb />
|
||||||
|
default:
|
||||||
|
return <HelpOutlineOutlined />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const desktopCols = (): Column<Gate>[] => {
|
const desktopCols = (): Column<Gate>[] => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
@ -151,6 +166,29 @@ export default function GateList(props: Props) {
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "PCA Status",
|
||||||
|
render: gate => (
|
||||||
|
<Box padding={2}>
|
||||||
|
{displayPCAStatus(gate.status.pcaState)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Last FLow",
|
||||||
|
render: gate => (
|
||||||
|
<Box padding={2}>
|
||||||
|
<Box display="flex" justifyContent="space-between">
|
||||||
|
<Typography>Last Flow:</Typography>
|
||||||
|
<Typography>{gate.status.lastMassAirflow.toFixed(2)}</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box display="flex" justifyContent="space-between">
|
||||||
|
<Typography>Last Reading:</Typography>
|
||||||
|
<Typography>{gate.status.lastUpdate !== "" ? moment(gate.status.lastUpdate).fromNow() : "No Reading Yet"}</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
const mobileCols = (): Column<Gate>[] => {
|
const mobileCols = (): Column<Gate>[] => {
|
||||||
|
|
@ -162,13 +200,16 @@ export default function GateList(props: Props) {
|
||||||
<Typography margin="auto" marginLeft={0} fontWeight={650} fontSize={20}>
|
<Typography margin="auto" marginLeft={0} fontWeight={650} fontSize={20}>
|
||||||
{gate.name}
|
{gate.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
<IconButton onClick={(event) => {
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
event.stopPropagation()
|
{displayPCAStatus(gate.status.pcaState)}
|
||||||
setSelectedGate(gate)
|
<IconButton onClick={(event) => {
|
||||||
setGateDialog(true)
|
event.stopPropagation()
|
||||||
}}>
|
setSelectedGate(gate)
|
||||||
<Settings />
|
setGateDialog(true)
|
||||||
</IconButton>
|
}}>
|
||||||
|
<Settings />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -198,6 +239,14 @@ export default function GateList(props: Props) {
|
||||||
<Typography>PCA Unit:</Typography>
|
<Typography>PCA Unit:</Typography>
|
||||||
<Typography>{gate.settings.pcaType !== "" ? gate.settings.pcaType : "None"}</Typography>
|
<Typography>{gate.settings.pcaType !== "" ? gate.settings.pcaType : "None"}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
<Box padding={2} display="flex" flexDirection="row" justifyContent="space-between">
|
||||||
|
<Typography>Last Flow:</Typography>
|
||||||
|
<Typography>{gate.status.lastMassAirflow}</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box padding={2} display="flex" flexDirection="row" justifyContent="space-between">
|
||||||
|
<Typography>Last Reading:</Typography>
|
||||||
|
<Typography>{gate.status.lastUpdate !== "" ? moment(gate.status.lastUpdate).fromNow() : "No PCA reading yet"}</Typography>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
// <Typography variant="body2" color="textSecondary" sx={{ padding: 1 }}>
|
// <Typography variant="body2" color="textSecondary" sx={{ padding: 1 }}>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ export default function GateSettings(props: Props) {
|
||||||
//user set identifier to be shown on the marker on the map
|
//user set identifier to be shown on the marker on the map
|
||||||
const [terminalIdentifier, setTerminalIdentifier] = useState("");
|
const [terminalIdentifier, setTerminalIdentifier] = useState("");
|
||||||
const [gateIdentifier, setGateIdentifier] = useState("");
|
const [gateIdentifier, setGateIdentifier] = useState("");
|
||||||
|
const [idleLimit, setIdleLimit] = useState<number>(0);
|
||||||
const [hourlyPCA, setHourlyPCA] = useState<number>(0);
|
const [hourlyPCA, setHourlyPCA] = useState<number>(0);
|
||||||
const [hourlyAPU, setHourlyAPU] = useState<number>(0);
|
const [hourlyAPU, setHourlyAPU] = useState<number>(0);
|
||||||
|
|
||||||
|
|
@ -140,6 +141,7 @@ export default function GateSettings(props: Props) {
|
||||||
settings.thermalResistance = ductProps.thermalResistance;
|
settings.thermalResistance = ductProps.thermalResistance;
|
||||||
settings.lowerFlow = lowerFlowBound;
|
settings.lowerFlow = lowerFlowBound;
|
||||||
settings.upperFlow = upperFlowBound;
|
settings.upperFlow = upperFlowBound;
|
||||||
|
settings.idleFlow = idleLimit;
|
||||||
settings.ductName = ductName;
|
settings.ductName = ductName;
|
||||||
settings.pcaType = pcaUnit;
|
settings.pcaType = pcaUnit;
|
||||||
settings.letterIdentifier = terminalIdentifier;
|
settings.letterIdentifier = terminalIdentifier;
|
||||||
|
|
@ -167,6 +169,7 @@ export default function GateSettings(props: Props) {
|
||||||
thermalResistance: ductProps.thermalResistance,
|
thermalResistance: ductProps.thermalResistance,
|
||||||
lowerFlow: lowerFlowBound,
|
lowerFlow: lowerFlowBound,
|
||||||
upperFlow: upperFlowBound,
|
upperFlow: upperFlowBound,
|
||||||
|
idleFlow: idleLimit,
|
||||||
ductName: ductName,
|
ductName: ductName,
|
||||||
pcaType: pcaUnit,
|
pcaType: pcaUnit,
|
||||||
letterIdentifier: terminalIdentifier,
|
letterIdentifier: terminalIdentifier,
|
||||||
|
|
@ -283,16 +286,18 @@ export default function GateSettings(props: Props) {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<TextField
|
<TextField
|
||||||
|
margin="dense"
|
||||||
label="Gate Name"
|
label="Gate Name"
|
||||||
fullWidth
|
fullWidth
|
||||||
value={gateName}
|
value={gateName}
|
||||||
onChange={e => setGateName(e.target.value)}
|
onChange={e => setGateName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<Select
|
<TextField
|
||||||
|
margin="dense"
|
||||||
id="terminal"
|
id="terminal"
|
||||||
label="Terminal"
|
label="Terminal"
|
||||||
fullWidth
|
fullWidth
|
||||||
displayEmpty
|
select
|
||||||
value={terminalKey}
|
value={terminalKey}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setTerminalKey(e.target.value as string);
|
setTerminalKey(e.target.value as string);
|
||||||
|
|
@ -305,8 +310,9 @@ export default function GateSettings(props: Props) {
|
||||||
{terminal.name}
|
{terminal.name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</TextField>
|
||||||
<TextField
|
<TextField
|
||||||
|
margin="dense"
|
||||||
label="Low Mass Air Flow"
|
label="Low Mass Air Flow"
|
||||||
fullWidth
|
fullWidth
|
||||||
type="number"
|
type="number"
|
||||||
|
|
@ -314,19 +320,30 @@ export default function GateSettings(props: Props) {
|
||||||
onChange={e => setLowerFlowBound(+e.target.value)}
|
onChange={e => setLowerFlowBound(+e.target.value)}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
|
margin="dense"
|
||||||
label="High Mass Air Flow"
|
label="High Mass Air Flow"
|
||||||
fullWidth
|
fullWidth
|
||||||
type="number"
|
type="dense"
|
||||||
value={upperFlowBound}
|
value={upperFlowBound}
|
||||||
onChange={e => setUpperFlowBound(+e.target.value)}
|
onChange={e => setUpperFlowBound(+e.target.value)}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<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"
|
label="PCA Unit"
|
||||||
fullWidth
|
fullWidth
|
||||||
value={pcaUnit}
|
value={pcaUnit}
|
||||||
onChange={e => setPcaUnit(e.target.value)}
|
onChange={e => setPcaUnit(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
|
margin="dense"
|
||||||
style={{ width: "45%" }}
|
style={{ width: "45%" }}
|
||||||
select
|
select
|
||||||
label="Terminal Identifier"
|
label="Terminal Identifier"
|
||||||
|
|
@ -340,6 +357,7 @@ export default function GateSettings(props: Props) {
|
||||||
{tiOptions}
|
{tiOptions}
|
||||||
</TextField>
|
</TextField>
|
||||||
<TextField
|
<TextField
|
||||||
|
margin="dense"
|
||||||
style={{ width: "45%", marginLeft: "10%" }}
|
style={{ width: "45%", marginLeft: "10%" }}
|
||||||
label="Gate Identifier"
|
label="Gate Identifier"
|
||||||
select
|
select
|
||||||
|
|
@ -351,6 +369,7 @@ export default function GateSettings(props: Props) {
|
||||||
{numberOptions}
|
{numberOptions}
|
||||||
</TextField>
|
</TextField>
|
||||||
<TextField
|
<TextField
|
||||||
|
margin="dense"
|
||||||
label="Hourly PCA Cost"
|
label="Hourly PCA Cost"
|
||||||
fullWidth
|
fullWidth
|
||||||
type="number"
|
type="number"
|
||||||
|
|
@ -361,6 +380,7 @@ export default function GateSettings(props: Props) {
|
||||||
onChange={e => setHourlyPCA(+e.target.value)}
|
onChange={e => setHourlyPCA(+e.target.value)}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
|
margin="dense"
|
||||||
label="Hourly APU Cost"
|
label="Hourly APU Cost"
|
||||||
fullWidth
|
fullWidth
|
||||||
type="number"
|
type="number"
|
||||||
|
|
|
||||||
213
src/grain/CustomGrainForm.tsx
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
import {MenuItem, TextField } from "@mui/material"
|
||||||
|
import { cloneDeep } from "lodash"
|
||||||
|
import { pond } from "protobuf-ts/pond"
|
||||||
|
import React, { useEffect, useRef, useState } from "react"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
grainSettings?: pond.GrainSettings
|
||||||
|
onGrainSettingsChange: (settings: pond.GrainSettings, formValid: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CustomGrainForm(props: Props) {
|
||||||
|
const {grainSettings, onGrainSettingsChange} = props
|
||||||
|
const [newGrainSettings, setNewGrainSettings] = useState(pond.GrainSettings.create())
|
||||||
|
const [name, setName] = useState("")
|
||||||
|
const [group, setGroup] = useState("")
|
||||||
|
const [equation, setEquation] = useState<pond.MoistureEquation>(pond.MoistureEquation.MOISTURE_EQUATION_NONE)
|
||||||
|
const [constantA, setConstantA] = useState("0")
|
||||||
|
const [constantB, setConstantB] = useState("0")
|
||||||
|
const [constantC, setConstantC] = useState("0")
|
||||||
|
const [kgPerBushel, setKgPerBushel] = useState("0")
|
||||||
|
const [bushelsPerTonne, setBushelsPerTonne] = useState("0")
|
||||||
|
const valid = useRef(false)
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
if(grainSettings){
|
||||||
|
setName(grainSettings.name)
|
||||||
|
setGroup(grainSettings.group)
|
||||||
|
setEquation(grainSettings.equation)
|
||||||
|
setConstantA(grainSettings.a.toString())
|
||||||
|
setConstantB(grainSettings.b.toString())
|
||||||
|
setConstantC(grainSettings.c.toString())
|
||||||
|
setKgPerBushel(grainSettings.kgPerBushel.toString())
|
||||||
|
setBushelsPerTonne(grainSettings.bushelsPerTonne.toString())
|
||||||
|
setNewGrainSettings(grainSettings)
|
||||||
|
}
|
||||||
|
},[grainSettings])
|
||||||
|
|
||||||
|
const validate = (name: string, group: string, constA: string, constB: string, constC: string, kgPerBushel: string, bushelsPerTonne: string) => {
|
||||||
|
if (name === "") return false
|
||||||
|
if (group === "") return false
|
||||||
|
if (isNaN(parseFloat(constA))) return false
|
||||||
|
if (isNaN(parseFloat(constB))) return false
|
||||||
|
if (isNaN(parseFloat(constC))) return false
|
||||||
|
if (isNaN(parseFloat(kgPerBushel))) return false
|
||||||
|
if (isNaN(parseFloat(bushelsPerTonne))) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsChanged = (newSettings: pond.GrainSettings) => {
|
||||||
|
setNewGrainSettings(newSettings)
|
||||||
|
onGrainSettingsChange(newSettings, valid.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
const grainForm = () => {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<TextField
|
||||||
|
sx={{marginBottom: 2}}
|
||||||
|
fullWidth
|
||||||
|
label="Name*"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => {
|
||||||
|
let name = e.target.value
|
||||||
|
setName(name)
|
||||||
|
let settings = cloneDeep(newGrainSettings)
|
||||||
|
settings.name = name
|
||||||
|
valid.current = validate(name, group, constantA, constantB, constantC, kgPerBushel, bushelsPerTonne)
|
||||||
|
settingsChanged(settings)
|
||||||
|
}}/>
|
||||||
|
<TextField
|
||||||
|
sx={{marginBottom: 2}}
|
||||||
|
fullWidth
|
||||||
|
label="Group*"
|
||||||
|
value={group}
|
||||||
|
onChange={(e) => {
|
||||||
|
let group = e.target.value
|
||||||
|
setGroup(group)
|
||||||
|
let settings = cloneDeep(newGrainSettings)
|
||||||
|
settings.group = group
|
||||||
|
valid.current = validate(name, group, constantA, constantB, constantC, kgPerBushel, bushelsPerTonne)
|
||||||
|
settingsChanged(settings)
|
||||||
|
}}/>
|
||||||
|
<TextField
|
||||||
|
sx={{marginBottom: 2}}
|
||||||
|
fullWidth
|
||||||
|
value={equation}
|
||||||
|
helperText={"Not selecting an EMC equation to use will return the humidity instead of EMC"}
|
||||||
|
label="EMC Equation"
|
||||||
|
onChange={(e) => {
|
||||||
|
let enumVal = parseFloat(e.target.value)
|
||||||
|
setEquation(enumVal)
|
||||||
|
let settings = cloneDeep(newGrainSettings)
|
||||||
|
settings.equation = enumVal
|
||||||
|
valid.current = validate(name, group, constantA, constantB, constantC, kgPerBushel, bushelsPerTonne)
|
||||||
|
settingsChanged(settings)
|
||||||
|
}}
|
||||||
|
select>
|
||||||
|
<MenuItem key={pond.MoistureEquation.MOISTURE_EQUATION_NONE} value={pond.MoistureEquation.MOISTURE_EQUATION_NONE}>
|
||||||
|
None
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem key={pond.MoistureEquation.MOISTURE_EQUATION_CHUNG_PFOST} value={pond.MoistureEquation.MOISTURE_EQUATION_CHUNG_PFOST}>
|
||||||
|
Chung-Pfost
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem key={pond.MoistureEquation.MOISTURE_EQUATION_HENDERSON} value={pond.MoistureEquation.MOISTURE_EQUATION_HENDERSON}>
|
||||||
|
Henderson
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem key={pond.MoistureEquation.MOISTURE_EQUATION_HALSEY} value={pond.MoistureEquation.MOISTURE_EQUATION_HALSEY}>
|
||||||
|
Halsey
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem key={pond.MoistureEquation.MOISTURE_EQUATION_OSWIN} value={pond.MoistureEquation.MOISTURE_EQUATION_OSWIN}>
|
||||||
|
Oswin
|
||||||
|
</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
sx={{marginBottom: 2}}
|
||||||
|
fullWidth
|
||||||
|
type="number"
|
||||||
|
value={constantA}
|
||||||
|
error={isNaN(parseFloat(constantA))}
|
||||||
|
helperText={isNaN(parseFloat(constantA)) ? "Must be a valid number" : ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
let val = e.target.value
|
||||||
|
setConstantA(val)
|
||||||
|
valid.current = validate(name, group, val, constantB, constantC, kgPerBushel, bushelsPerTonne)
|
||||||
|
let settings = cloneDeep(newGrainSettings)
|
||||||
|
if(!isNaN(parseFloat(val))){
|
||||||
|
settings.a = parseFloat(val)
|
||||||
|
}
|
||||||
|
settingsChanged(settings)
|
||||||
|
}}
|
||||||
|
label="Constant A"/>
|
||||||
|
<TextField
|
||||||
|
sx={{marginBottom: 2}}
|
||||||
|
fullWidth
|
||||||
|
type="number"
|
||||||
|
value={constantB}
|
||||||
|
error={isNaN(parseFloat(constantB))}
|
||||||
|
helperText={isNaN(parseFloat(constantB)) ? "Must be a valid number" : ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
let val = e.target.value
|
||||||
|
setConstantB(val)
|
||||||
|
valid.current = validate(name, group, constantA, val, constantC, kgPerBushel, bushelsPerTonne)
|
||||||
|
let settings = cloneDeep(newGrainSettings)
|
||||||
|
if(!isNaN(parseFloat(val))){
|
||||||
|
settings.b = parseFloat(val)
|
||||||
|
}
|
||||||
|
settingsChanged(settings)
|
||||||
|
}}
|
||||||
|
label="Constant B"/>
|
||||||
|
<TextField
|
||||||
|
sx={{marginBottom: 2}}
|
||||||
|
fullWidth
|
||||||
|
type="number"
|
||||||
|
value={constantC}
|
||||||
|
error={isNaN(parseFloat(constantC))}
|
||||||
|
helperText={isNaN(parseFloat(constantC)) ? "Must be a valid number" : ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
let val = e.target.value
|
||||||
|
setConstantC(val)
|
||||||
|
valid.current = validate(name, group, constantA, constantB, val, kgPerBushel, bushelsPerTonne)
|
||||||
|
let settings = cloneDeep(newGrainSettings)
|
||||||
|
if(!isNaN(parseFloat(val))){
|
||||||
|
settings.c = parseFloat(val)
|
||||||
|
}
|
||||||
|
settingsChanged(settings)
|
||||||
|
}}
|
||||||
|
label="Constant C"/>
|
||||||
|
<TextField
|
||||||
|
sx={{marginBottom: 2}}
|
||||||
|
fullWidth
|
||||||
|
type="number"
|
||||||
|
value={kgPerBushel}
|
||||||
|
error={isNaN(parseFloat(kgPerBushel))}
|
||||||
|
helperText={isNaN(parseFloat(kgPerBushel)) ? "Must be a valid number" : ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
let val = e.target.value
|
||||||
|
setKgPerBushel(val)
|
||||||
|
valid.current = validate(name, group, constantA, constantB, constantC, val, bushelsPerTonne)
|
||||||
|
let settings = cloneDeep(newGrainSettings)
|
||||||
|
if(!isNaN(parseFloat(val))){
|
||||||
|
settings.kgPerBushel = parseFloat(val)
|
||||||
|
}
|
||||||
|
settingsChanged(settings)
|
||||||
|
}}
|
||||||
|
label="kg per Bushel"/>
|
||||||
|
<TextField
|
||||||
|
sx={{marginBottom: 2}}
|
||||||
|
fullWidth
|
||||||
|
type="number"
|
||||||
|
value={bushelsPerTonne}
|
||||||
|
error={isNaN(parseFloat(bushelsPerTonne))}
|
||||||
|
helperText={isNaN(parseFloat(bushelsPerTonne)) ? "Must be a valid number" : ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
let val = e.target.value
|
||||||
|
setBushelsPerTonne(val)
|
||||||
|
valid.current = validate(name, group, constantA, constantB, constantC, kgPerBushel, val)
|
||||||
|
let settings = cloneDeep(newGrainSettings)
|
||||||
|
if(!isNaN(parseFloat(val))){
|
||||||
|
settings.bushelsPerTonne = parseFloat(val)
|
||||||
|
}
|
||||||
|
settingsChanged(settings)
|
||||||
|
}}
|
||||||
|
label="Bushels per Tonne"/>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{grainForm()}
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
155
src/grain/CustomGrainSelector.tsx
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
import { ExpandMore } from "@mui/icons-material"
|
||||||
|
import { Accordion, AccordionDetails, AccordionSummary, Box, Button, Typography } from "@mui/material"
|
||||||
|
import SearchSelect, { Option } from "common/SearchSelect"
|
||||||
|
import { pond } from "protobuf-ts/pond"
|
||||||
|
import { useGlobalState } from "providers"
|
||||||
|
import { useGrainAPI } from "providers/pond/grainAPI"
|
||||||
|
import React, { useEffect, useState } from "react"
|
||||||
|
import CustomGrainForm from "./CustomGrainForm"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
initialGrain?: pond.GrainSettings
|
||||||
|
onGrainSettingsChange: (settings: pond.GrainSettings) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CustomGrainSelector(props: Props) {
|
||||||
|
const {initialGrain, onGrainSettingsChange} = props
|
||||||
|
const grainAPI = useGrainAPI()
|
||||||
|
const [{as}] = useGlobalState()
|
||||||
|
const [newGrainSettings, setNewGrainSettings] = useState(initialGrain ?? pond.GrainSettings.create())
|
||||||
|
const [formValid, setFormValid] = useState(true)
|
||||||
|
const [grainMap, setGrainMap] = useState<Map<string, pond.GrainObject>>(
|
||||||
|
initialGrain ?
|
||||||
|
new Map([
|
||||||
|
["Active", pond.GrainObject.create({key: "Active", name: initialGrain.name, settings: initialGrain})]
|
||||||
|
])
|
||||||
|
:
|
||||||
|
new Map())
|
||||||
|
const [grainOptions, setGrainOptions] = useState<Option[]>(
|
||||||
|
initialGrain ?
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: initialGrain.name,
|
||||||
|
value: "Active",
|
||||||
|
group: "Active"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
:
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
const [isNew, setIsNew] = useState(false)
|
||||||
|
const [selectedOption, setSelectedOption] = useState<Option | null>(initialGrain ? {
|
||||||
|
label: initialGrain.name,
|
||||||
|
value: "Active",
|
||||||
|
group: "Active"
|
||||||
|
} : null)
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
grainAPI.listGrains(0, 0, "asc", "name", undefined, undefined, undefined, as).then(resp => {
|
||||||
|
let options: Option[] = grainOptions
|
||||||
|
let map: Map<string, pond.GrainObject> = grainMap
|
||||||
|
if (resp.data.grains){
|
||||||
|
resp.data.grains.forEach(grain => {
|
||||||
|
map.set(grain.key, grain)
|
||||||
|
options.push({
|
||||||
|
label: grain.name,
|
||||||
|
value: grain.key,
|
||||||
|
group: grain.settings?.group,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setGrainOptions([...options])
|
||||||
|
setGrainMap(map)
|
||||||
|
})
|
||||||
|
},[grainAPI])
|
||||||
|
|
||||||
|
const saveGrain = () => {
|
||||||
|
// console.log(newGrainSettings)
|
||||||
|
grainAPI.addGrain(newGrainSettings, as).then(resp => {
|
||||||
|
console.log("grain added")
|
||||||
|
}).catch(err => {
|
||||||
|
console.log("err")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsChanged = (newSettings: pond.GrainSettings, formValid: boolean) => {
|
||||||
|
setNewGrainSettings(newSettings)
|
||||||
|
setIsNew(true)
|
||||||
|
setFormValid(formValid)
|
||||||
|
onGrainSettingsChange(newSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
const grainAccordion = () => {
|
||||||
|
return (
|
||||||
|
<Accordion>
|
||||||
|
<AccordionSummary expandIcon={<ExpandMore />}>Custom Grain Properties</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<CustomGrainForm grainSettings={newGrainSettings} onGrainSettingsChange={settingsChanged}/>
|
||||||
|
{isNew &&
|
||||||
|
<Box display='flex' justifyContent="flex-end">
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
disabled={!formValid}
|
||||||
|
onClick={() => {
|
||||||
|
setIsNew(false)
|
||||||
|
saveGrain()
|
||||||
|
}}
|
||||||
|
color="primary">
|
||||||
|
Save Custom Grain
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{grainOptions.length === 0 ?
|
||||||
|
<Box>
|
||||||
|
<Typography sx={{marginBottom: 1}}>Custom Grain Properties</Typography>
|
||||||
|
<CustomGrainForm grainSettings={newGrainSettings} onGrainSettingsChange={settingsChanged}/>
|
||||||
|
{isNew &&
|
||||||
|
<Box display='flex' justifyContent="flex-end">
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
disabled={!formValid}
|
||||||
|
onClick={() => {
|
||||||
|
setIsNew(false)
|
||||||
|
}}
|
||||||
|
color="primary">
|
||||||
|
Save Custom Grain
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
:
|
||||||
|
<Box>
|
||||||
|
<Box sx={{marginBottom: 2}}>
|
||||||
|
<SearchSelect
|
||||||
|
label="My Grains"
|
||||||
|
selected={selectedOption}
|
||||||
|
changeSelection={option => {
|
||||||
|
setIsNew(false)
|
||||||
|
setSelectedOption(option)
|
||||||
|
//when an option is selected set all of the state variables controlling the form entries
|
||||||
|
let grain = grainMap.get(option?.value)
|
||||||
|
if(grain && grain.settings){
|
||||||
|
//the the new grain settings object
|
||||||
|
setNewGrainSettings(grain.settings)
|
||||||
|
//pass that settings object back up
|
||||||
|
settingsChanged(grain.settings, true)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
group
|
||||||
|
options={grainOptions}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
{grainAccordion()}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
}
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -641,6 +641,16 @@ export function GetGrainExtensionMap(): Map<pond.Grain, GrainExtension> {
|
||||||
return cloneDeep(GrainExtensions);
|
return cloneDeep(GrainExtensions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function GetValidGrainExtensions(): GrainExtension[] {
|
||||||
|
let validGrains: GrainExtension[] = []
|
||||||
|
GrainExtensions.forEach((ext, key) => {
|
||||||
|
if(key !== pond.Grain.GRAIN_NONE && key !== pond.Grain.GRAIN_INVALID && key !== pond.Grain.GRAIN_CUSTOM){
|
||||||
|
validGrains.push(ext)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return validGrains
|
||||||
|
}
|
||||||
|
|
||||||
export default function GrainDescriber(type: pond.Grain): GrainExtension {
|
export default function GrainDescriber(type: pond.Grain): GrainExtension {
|
||||||
let describer = GrainExtensions.get(type);
|
let describer = GrainExtensions.get(type);
|
||||||
//console.log(describer)
|
//console.log(describer)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,8 @@ const toERH = (humidity: number): number => {
|
||||||
export function ExtractMoisture(
|
export function ExtractMoisture(
|
||||||
type: pond.Grain | undefined,
|
type: pond.Grain | undefined,
|
||||||
celsius: number,
|
celsius: number,
|
||||||
humidity: number
|
humidity: number,
|
||||||
|
customGrain?: pond.GrainSettings
|
||||||
): number {
|
): number {
|
||||||
if (humidity <= 0) {
|
if (humidity <= 0) {
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -26,20 +27,43 @@ export function ExtractMoisture(
|
||||||
type === undefined ||
|
type === undefined ||
|
||||||
type === pond.Grain.GRAIN_NONE ||
|
type === pond.Grain.GRAIN_NONE ||
|
||||||
type === pond.Grain.GRAIN_INVALID ||
|
type === pond.Grain.GRAIN_INVALID ||
|
||||||
type === pond.Grain.GRAIN_CUSTOM
|
(type === pond.Grain.GRAIN_CUSTOM && customGrain === undefined)
|
||||||
) {
|
) {
|
||||||
return humidity;
|
return humidity;
|
||||||
}
|
}
|
||||||
|
|
||||||
let dry = humidity;
|
let dry = humidity;
|
||||||
let ctx = GrainDescriber(type);
|
let eq: pond.MoistureEquation
|
||||||
dry = toDryMoisture(ctx.equation, celsius, humidity, ctx.a, ctx.b, ctx.c);
|
let constA: number
|
||||||
|
let constB: number
|
||||||
|
let constC: number
|
||||||
|
if(type === pond.Grain.GRAIN_CUSTOM && customGrain){
|
||||||
|
if(customGrain.equation === pond.MoistureEquation.MOISTURE_EQUATION_NONE) return humidity
|
||||||
|
eq = customGrain.equation
|
||||||
|
constA = customGrain.a
|
||||||
|
constB = customGrain.b
|
||||||
|
constC = customGrain.c
|
||||||
|
}else{
|
||||||
|
let ctx = GrainDescriber(type);
|
||||||
|
eq = ctx.equation
|
||||||
|
constA = ctx.a
|
||||||
|
constB = ctx.b
|
||||||
|
constC = ctx.c
|
||||||
|
}
|
||||||
|
|
||||||
|
dry = toDryMoisture(eq, celsius, humidity, constA, constB, constC);
|
||||||
return dryToWet(dry);
|
return dryToWet(dry);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WaterContent(type: pond.Grain, bushels: number, moistureContent: number): number {
|
export function WaterContent(type: pond.Grain, bushels: number, moistureContent: number, customGrain?: pond.GrainSettings): number {
|
||||||
let grain = GrainDescriber(type);
|
//kg per bushel conversion
|
||||||
return bushels * grain.weightConversionKg * moistureContent;
|
let conversion = 0
|
||||||
|
if(type === pond.Grain.GRAIN_CUSTOM && customGrain){
|
||||||
|
conversion = customGrain.kgPerBushel
|
||||||
|
}else{
|
||||||
|
conversion = GrainDescriber(type).weightConversionKg;
|
||||||
|
}
|
||||||
|
return bushels * conversion * moistureContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MoistureToHumidity(
|
export function MoistureToHumidity(
|
||||||
|
|
@ -69,7 +93,7 @@ function wetToDry(wetMC: number): number {
|
||||||
}
|
}
|
||||||
|
|
||||||
function toDryMoisture(
|
function toDryMoisture(
|
||||||
eq: Equation,
|
eq: pond.MoistureEquation,
|
||||||
T: number,
|
T: number,
|
||||||
RH: number,
|
RH: number,
|
||||||
a: number,
|
a: number,
|
||||||
|
|
@ -78,13 +102,13 @@ function toDryMoisture(
|
||||||
): number {
|
): number {
|
||||||
const ERH: number = toERH(RH);
|
const ERH: number = toERH(RH);
|
||||||
switch (eq) {
|
switch (eq) {
|
||||||
case Equation.chungPfost:
|
case pond.MoistureEquation.MOISTURE_EQUATION_CHUNG_PFOST:
|
||||||
return chungPfost(T, ERH, a, b, c);
|
return chungPfost(T, ERH, a, b, c);
|
||||||
case Equation.halsey:
|
case pond.MoistureEquation.MOISTURE_EQUATION_HALSEY:
|
||||||
return halsey(T, ERH, a, b, c);
|
return halsey(T, ERH, a, b, c);
|
||||||
case Equation.henderson:
|
case pond.MoistureEquation.MOISTURE_EQUATION_HENDERSON:
|
||||||
return henderson(T, ERH, a, b, c);
|
return henderson(T, ERH, a, b, c);
|
||||||
case Equation.oswin:
|
case pond.MoistureEquation.MOISTURE_EQUATION_OSWIN:
|
||||||
return oswin(T, ERH, a, b, c);
|
return oswin(T, ERH, a, b, c);
|
||||||
default:
|
default:
|
||||||
return RH;
|
return RH;
|
||||||
|
|
@ -92,7 +116,7 @@ function toDryMoisture(
|
||||||
}
|
}
|
||||||
|
|
||||||
function dryMoistureToHumidity(
|
function dryMoistureToHumidity(
|
||||||
eq: Equation,
|
eq: pond.MoistureEquation,
|
||||||
celsius: number,
|
celsius: number,
|
||||||
dryMC: number,
|
dryMC: number,
|
||||||
a: number,
|
a: number,
|
||||||
|
|
@ -101,16 +125,16 @@ function dryMoistureToHumidity(
|
||||||
): number {
|
): number {
|
||||||
let ERH = 0;
|
let ERH = 0;
|
||||||
switch (eq) {
|
switch (eq) {
|
||||||
case Equation.chungPfost:
|
case pond.MoistureEquation.MOISTURE_EQUATION_CHUNG_PFOST:
|
||||||
ERH = chungPfostInverse(celsius, dryMC, a, b, c);
|
ERH = chungPfostInverse(celsius, dryMC, a, b, c);
|
||||||
break;
|
break;
|
||||||
case Equation.halsey:
|
case pond.MoistureEquation.MOISTURE_EQUATION_HALSEY:
|
||||||
ERH = halseyInverse(celsius, dryMC, a, b, c);
|
ERH = halseyInverse(celsius, dryMC, a, b, c);
|
||||||
break;
|
break;
|
||||||
case Equation.henderson:
|
case pond.MoistureEquation.MOISTURE_EQUATION_HENDERSON:
|
||||||
ERH = hendersonInverse(celsius, dryMC, a, b, c);
|
ERH = hendersonInverse(celsius, dryMC, a, b, c);
|
||||||
break;
|
break;
|
||||||
case Equation.oswin:
|
case pond.MoistureEquation.MOISTURE_EQUATION_OSWIN:
|
||||||
ERH = oswinInverse(celsius, dryMC, a, b, c);
|
ERH = oswinInverse(celsius, dryMC, a, b, c);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -125,17 +125,19 @@ export default function GrainTransaction(props: Props) {
|
||||||
.then(resp => {
|
.then(resp => {
|
||||||
//let sourceOps: Option[] = sourceOptions
|
//let sourceOps: Option[] = sourceOptions
|
||||||
let binOps: Option[] = [];
|
let binOps: Option[] = [];
|
||||||
resp.data.bins.forEach(bin => {
|
if(resp.data.bins){
|
||||||
let b = Bin.create(bin);
|
resp.data.bins.forEach(bin => {
|
||||||
if (mainObject.key() !== b.key()) {
|
let b = Bin.create(bin);
|
||||||
let op: Option = {
|
if (mainObject.key() !== b.key()) {
|
||||||
label: b.name(),
|
let op: Option = {
|
||||||
value: b,
|
label: b.name(),
|
||||||
group: "Bins"
|
value: b,
|
||||||
};
|
group: "Bins"
|
||||||
(!restrictMatching || grainTypesMatch(op.value, mainObject)) && binOps.push(op);
|
};
|
||||||
}
|
(!restrictMatching || grainTypesMatch(op.value, mainObject) || op.value.grainName() === mainObject.grainName()) && binOps.push(op);
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
setBinOptions([...binOps]);
|
setBinOptions([...binOps]);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|
@ -151,17 +153,19 @@ export default function GrainTransaction(props: Props) {
|
||||||
.then(resp => {
|
.then(resp => {
|
||||||
//let sourceOps: Option[] = sourceOptions
|
//let sourceOps: Option[] = sourceOptions
|
||||||
let bagOps: Option[] = [];
|
let bagOps: Option[] = [];
|
||||||
resp.data.grainBags.forEach(bag => {
|
if(resp.data.grainBags){
|
||||||
let b = GrainBag.create(bag);
|
resp.data.grainBags.forEach(bag => {
|
||||||
if (mainObject.key() !== b.key()) {
|
let b = GrainBag.create(bag);
|
||||||
let op: Option = {
|
if (mainObject.key() !== b.key()) {
|
||||||
label: b.name(),
|
let op: Option = {
|
||||||
value: b,
|
label: b.name(),
|
||||||
group: "Grain Bags"
|
value: b,
|
||||||
};
|
group: "Grain Bags"
|
||||||
(!restrictMatching || grainTypesMatch(op.value, mainObject)) && bagOps.push(op);
|
};
|
||||||
}
|
(!restrictMatching || grainTypesMatch(op.value, mainObject) || op.value.grainName() === mainObject.grainName()) && bagOps.push(op);
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
setBagOptions([...bagOps]);
|
setBagOptions([...bagOps]);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|
@ -184,7 +188,7 @@ export default function GrainTransaction(props: Props) {
|
||||||
value: f,
|
value: f,
|
||||||
group: "Fields"
|
group: "Fields"
|
||||||
};
|
};
|
||||||
(!restrictMatching || grainTypesMatch(op.value, mainObject)) && fieldOps.push(op);
|
(!restrictMatching || grainTypesMatch(op.value, mainObject) || op.value.grainName() === mainObject.grainName()) && fieldOps.push(op);
|
||||||
});
|
});
|
||||||
setFieldOptions([...fieldOps]);
|
setFieldOptions([...fieldOps]);
|
||||||
})
|
})
|
||||||
|
|
@ -356,13 +360,6 @@ export default function GrainTransaction(props: Props) {
|
||||||
if (source.grain() === destination.grain()) {
|
if (source.grain() === destination.grain()) {
|
||||||
matching = true;
|
matching = true;
|
||||||
}
|
}
|
||||||
} else if (source.storage() === pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN) {
|
|
||||||
if (
|
|
||||||
source.customType().toLowerCase() === destination.customType().toLowerCase() &&
|
|
||||||
source.bushelsPerTonne() === destination.bushelsPerTonne()
|
|
||||||
) {
|
|
||||||
matching = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return matching;
|
return matching;
|
||||||
|
|
@ -373,7 +370,7 @@ export default function GrainTransaction(props: Props) {
|
||||||
if (grainTypesMatch(selectedSource.value, selectedDestination.value)) {
|
if (grainTypesMatch(selectedSource.value, selectedDestination.value)) {
|
||||||
updateInventory();
|
updateInventory();
|
||||||
} else {
|
} else {
|
||||||
//open dialog saying that the destinations grain type will change from this action
|
//open dialog saying that the grain types do not match or because it is a custom type
|
||||||
setGrainChangeDialog(true);
|
setGrainChangeDialog(true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -381,6 +378,36 @@ export default function GrainTransaction(props: Props) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the function is used to determine what text to display to the user when the grain types dont match
|
||||||
|
* or when it sees any of the objects involved as having a custom grain type
|
||||||
|
* @param source the source of the grain
|
||||||
|
* @param destination the destination of the grain
|
||||||
|
* @returns the text describing the situation
|
||||||
|
*/
|
||||||
|
const confirmationText = () => {
|
||||||
|
if(selectedSource && selectedDestination){
|
||||||
|
|
||||||
|
let source = selectedSource?.value
|
||||||
|
let destination = selectedDestination?.value
|
||||||
|
//both are custom
|
||||||
|
if(source.storage() === pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN && destination.storage() === pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN){
|
||||||
|
return "Both source and destination are using custom grain types, they may not match. Would you like to continue with the transaction?"
|
||||||
|
}
|
||||||
|
//source is custom destination is not
|
||||||
|
if(source.storage() === pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN){
|
||||||
|
return "The source is using a custom grain type and may not match the destination. Would you like to continue with the transaction?"
|
||||||
|
}
|
||||||
|
//destination is custom source is not
|
||||||
|
if(destination.storage() === pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN){
|
||||||
|
return "The destination is using a custom grain type and may not match the source. Would you like to continue with the transaction?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//neither are custom
|
||||||
|
return "The Grain types do not match. Are you sure you would like to perform this transaction?"
|
||||||
|
}
|
||||||
|
|
||||||
const grainChange = () => {
|
const grainChange = () => {
|
||||||
return (
|
return (
|
||||||
<ResponsiveDialog
|
<ResponsiveDialog
|
||||||
|
|
@ -389,7 +416,7 @@ export default function GrainTransaction(props: Props) {
|
||||||
setGrainChangeDialog(false);
|
setGrainChangeDialog(false);
|
||||||
}}>
|
}}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
The grain type of the destination may change as a result of this action.
|
{confirmationText()}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -90,11 +90,19 @@ export class Bin {
|
||||||
public customType(): string {
|
public customType(): string {
|
||||||
let c = "";
|
let c = "";
|
||||||
if (this.settings.inventory) {
|
if (this.settings.inventory) {
|
||||||
c = this.settings.inventory.customTypeName;
|
if (this.settings.inventory.customGrain){
|
||||||
|
c = this.settings.inventory.customGrain.name
|
||||||
|
}else{
|
||||||
|
c = this.settings.inventory.customTypeName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public customGrain(): pond.GrainSettings | undefined {
|
||||||
|
return this.settings.inventory?.customGrain ?? undefined
|
||||||
|
}
|
||||||
|
|
||||||
public empty(): boolean {
|
public empty(): boolean {
|
||||||
return this.settings.inventory?.empty || (this.settings.inventory !== undefined && this.settings.inventory !== null && this.bushels() < 5);
|
return this.settings.inventory?.empty || (this.settings.inventory !== undefined && this.settings.inventory !== null && this.bushels() < 5);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -142,4 +142,9 @@ export class Component {
|
||||||
return getFriendlyAddressTypeName(this.settings.addressType);
|
return getFriendlyAddressTypeName(this.settings.addressType);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public customGrainProps(): pond.GrainSettings | undefined {
|
||||||
|
if(this.settings.customGrain) return this.settings.customGrain
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -176,12 +176,23 @@ export class Device {
|
||||||
case pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_BLUE:
|
case pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_BLUE:
|
||||||
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLUE:
|
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLUE:
|
||||||
case pond.DevicePlatform.DEVICE_PLATFORM_V2_ETHERNET_BLUE:
|
case pond.DevicePlatform.DEVICE_PLATFORM_V2_ETHERNET_BLUE:
|
||||||
max = this.status.firmwareVersion >= "2.1.9" ? 4 : 2;
|
max = this.versionComparison(this.status.firmwareVersion, "2.1.9") ? 4 : 2;
|
||||||
}
|
}
|
||||||
return max
|
return max
|
||||||
}
|
}
|
||||||
|
|
||||||
private versionComparison(deviceVersion: string, requiredVersion: string): boolean {
|
private versionComparison(deviceVersion: string, requiredVersion: string): boolean {
|
||||||
|
// Feature explicitly not supported on this platform
|
||||||
|
if (!requiredVersion || requiredVersion === "N/A") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Device firmware missing / unset
|
||||||
|
if (!deviceVersion || deviceVersion.trim() === "") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//compare the versions
|
||||||
const parse = (v: string) => {
|
const parse = (v: string) => {
|
||||||
const [core, pre] = v.split("-");
|
const [core, pre] = v.split("-");
|
||||||
const parts = core.split(".").map(n => parseInt(n, 10));
|
const parts = core.split(".").map(n => parseInt(n, 10));
|
||||||
|
|
|
||||||
|
|
@ -5,22 +5,24 @@ import { or } from "utils/types";
|
||||||
|
|
||||||
export class Gate {
|
export class Gate {
|
||||||
public settings: pond.GateSettings = pond.GateSettings.create();
|
public settings: pond.GateSettings = pond.GateSettings.create();
|
||||||
|
public status: pond.GateStatus = pond.GateStatus.create();
|
||||||
public name: string = "Gate";
|
public name: string = "Gate";
|
||||||
public key: string = "";
|
public key: string = "";
|
||||||
public preferences: any = {};
|
public preferences: any = {};
|
||||||
public gateMutations: any = {};
|
public gateMutations: any = {};
|
||||||
public pcaState: pond.PCAState = pond.PCAState.PCA_STATE_UNKNOWN;
|
// public pcaState: pond.PCAState = pond.PCAState.PCA_STATE_UNKNOWN;
|
||||||
|
|
||||||
public static create(pb?: pond.Gate): Gate {
|
public static create(pb?: pond.Gate): Gate {
|
||||||
let my = new Gate();
|
let my = new Gate();
|
||||||
if (pb) {
|
if (pb) {
|
||||||
let g = pond.Gate.fromObject(pb);
|
let g = pond.Gate.fromObject(pb);
|
||||||
my.settings = pond.GateSettings.fromObject(cloneDeep(or(g.settings, {})));
|
my.settings = pond.GateSettings.fromObject(cloneDeep(or(g.settings, {})));
|
||||||
|
my.status = pond.GateStatus.fromObject(cloneDeep(or(g.status, {})));
|
||||||
my.name = g.name;
|
my.name = g.name;
|
||||||
my.key = g.key;
|
my.key = g.key;
|
||||||
my.preferences = g.componentPreferences;
|
my.preferences = g.componentPreferences;
|
||||||
my.gateMutations = g.gateMutations;
|
my.gateMutations = g.gateMutations;
|
||||||
my.pcaState = g.pcaState;
|
// my.pcaState = g.pcaState;
|
||||||
}
|
}
|
||||||
return my;
|
return my;
|
||||||
}
|
}
|
||||||
|
|
@ -77,7 +79,7 @@ export class Gate {
|
||||||
}
|
}
|
||||||
|
|
||||||
public gateMarkerColour(): string {
|
public gateMarkerColour(): string {
|
||||||
switch (this.pcaState) {
|
switch (this.status.pcaState) {
|
||||||
case pond.PCAState.PCA_STATE_OFF:
|
case pond.PCAState.PCA_STATE_OFF:
|
||||||
return "grey";
|
return "grey";
|
||||||
case pond.PCAState.PCA_STATE_IN_BOUNDS:
|
case pond.PCAState.PCA_STATE_IN_BOUNDS:
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import BinsIcon from "products/Bindapt/BinsIcon";
|
||||||
import { useGlobalState } from "providers";
|
import { useGlobalState } from "providers";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useNavigate, useLocation } from "react-router-dom";
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
import { IsAdaptiveAgriculture, isBXT, IsMiVent, IsAdCon, IsOmniAir } from "services/whiteLabel";
|
import { IsAdaptiveAgriculture, isBXT, IsMiVent, IsAdCon, IsOmniAir, IsMiPCA } from "services/whiteLabel";
|
||||||
import FieldsIcon from "products/AgIcons/FieldsIcon";
|
import FieldsIcon from "products/AgIcons/FieldsIcon";
|
||||||
import NexusSTIcon from "products/Construction/NexusSTIcon";
|
import NexusSTIcon from "products/Construction/NexusSTIcon";
|
||||||
import OmniAirDeviceIcon from "products/AviationIcons/OmniAirDeviceIcon";
|
import OmniAirDeviceIcon from "products/AviationIcons/OmniAirDeviceIcon";
|
||||||
|
|
@ -40,6 +40,7 @@ export default function BottomNavigator(props: Props) {
|
||||||
const isMiVent = IsMiVent();
|
const isMiVent = IsMiVent();
|
||||||
const isAdCon = IsAdCon();
|
const isAdCon = IsAdCon();
|
||||||
const isOmni = IsOmniAir();
|
const isOmni = IsOmniAir();
|
||||||
|
const isMiPCA = IsMiPCA();
|
||||||
|
|
||||||
const reRoute = useCallback(
|
const reRoute = useCallback(
|
||||||
(path: string) => {
|
(path: string) => {
|
||||||
|
|
@ -117,14 +118,14 @@ export default function BottomNavigator(props: Props) {
|
||||||
value="constructionMap"
|
value="constructionMap"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isOmni && (
|
{(isOmni || isMiPCA) && (
|
||||||
<BottomNavigationAction
|
<BottomNavigationAction
|
||||||
label="Map"
|
label="Map"
|
||||||
icon={<AirportMapIcon type={getType()} />}
|
icon={<AirportMapIcon type={getType()} />}
|
||||||
value="aviationMap"
|
value="aviationMap"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isOmni && (
|
{(isOmni || isMiPCA) && (
|
||||||
<BottomNavigationAction
|
<BottomNavigationAction
|
||||||
label="Terminals"
|
label="Terminals"
|
||||||
icon={<PlaneIcon type={getType()} />}
|
icon={<PlaneIcon type={getType()} />}
|
||||||
|
|
@ -139,7 +140,7 @@ export default function BottomNavigator(props: Props) {
|
||||||
<BindaptIcon type={getType()} />
|
<BindaptIcon type={getType()} />
|
||||||
) : isAdCon ? (
|
) : isAdCon ? (
|
||||||
<NexusSTIcon type={getType()} />
|
<NexusSTIcon type={getType()} />
|
||||||
) : isOmni ? (
|
) : (isOmni || isMiPCA) ? (
|
||||||
<OmniAirDeviceIcon type={getType()} />
|
<OmniAirDeviceIcon type={getType()} />
|
||||||
) : (
|
) : (
|
||||||
<DevicesIcon />
|
<DevicesIcon />
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ const JohnDeere = lazy(() => import("pages/JohnDeere"));
|
||||||
const CNHi = lazy(() => import("pages/CNHi"));
|
const CNHi = lazy(() => import("pages/CNHi"));
|
||||||
const LibraCart = lazy(() => import("pages/LibraCart"));
|
const LibraCart = lazy(() => import("pages/LibraCart"));
|
||||||
const FieldPage = lazy(()=>import("pages/Field"));
|
const FieldPage = lazy(()=>import("pages/Field"));
|
||||||
|
const GrainsPage = lazy(()=>import("pages/Grains"));
|
||||||
|
|
||||||
export const appendToUrl = (appendage: number | string) => {
|
export const appendToUrl = (appendage: number | string) => {
|
||||||
const basePath = location.pathname.replace(/\/$/, "");
|
const basePath = location.pathname.replace(/\/$/, "");
|
||||||
|
|
@ -360,6 +361,7 @@ export default function Router() {
|
||||||
{user.hasFeature("libra-cart") &&
|
{user.hasFeature("libra-cart") &&
|
||||||
<Route path="libracart" element={<LibraCart />} />
|
<Route path="libracart" element={<LibraCart />} />
|
||||||
}
|
}
|
||||||
|
<Route path="grains" element={<GrainsPage />} />
|
||||||
{/* Map routes */}
|
{/* Map routes */}
|
||||||
<Route path="visualFarm" element={<FieldMap />} />
|
<Route path="visualFarm" element={<FieldMap />} />
|
||||||
<Route path="aviationMap" element={<AviationMap />} />
|
<Route path="aviationMap" element={<AviationMap />} />
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { ChevronRight, Code, Memory, People, Person, SyncAlt, TapAndPlay } from "@mui/icons-material";
|
import { ChevronRight, Code, Grain, Memory, People, Person, SyncAlt, TapAndPlay } from "@mui/icons-material";
|
||||||
import ChevronLeft from "@mui/icons-material/ChevronLeft";
|
import ChevronLeft from "@mui/icons-material/ChevronLeft";
|
||||||
import {
|
import {
|
||||||
darken,
|
darken,
|
||||||
|
|
@ -27,6 +27,7 @@ import {
|
||||||
IsAdaptiveAgriculture,
|
IsAdaptiveAgriculture,
|
||||||
// hasTutorialPlaylist,
|
// hasTutorialPlaylist,
|
||||||
IsAdCon,
|
IsAdCon,
|
||||||
|
IsMiPCA,
|
||||||
// isBXT,
|
// isBXT,
|
||||||
IsMiVent,
|
IsMiVent,
|
||||||
IsOmniAir,
|
IsOmniAir,
|
||||||
|
|
@ -166,7 +167,8 @@ export default function SideNavigator(props: Props) {
|
||||||
const isMiVent = IsMiVent();
|
const isMiVent = IsMiVent();
|
||||||
const isAg = IsAdaptiveAgriculture()
|
const isAg = IsAdaptiveAgriculture()
|
||||||
const isStreamline = IsStreamline()
|
const isStreamline = IsStreamline()
|
||||||
const isMiPCA = IsOmniAir()
|
const isOmni = IsOmniAir()
|
||||||
|
const isMiPCA = IsMiPCA()
|
||||||
const isAdCon = IsAdCon()
|
const isAdCon = IsAdCon()
|
||||||
return (
|
return (
|
||||||
<List className={classes.list} component="nav">
|
<List className={classes.list} component="nav">
|
||||||
|
|
@ -184,7 +186,7 @@ export default function SideNavigator(props: Props) {
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{(isMiPCA || user.hasFeature("admin")) && (
|
{((isOmni || isMiPCA) || user.hasFeature("admin")) && (
|
||||||
<Tooltip title="Aviation Map" placement="right">
|
<Tooltip title="Aviation Map" placement="right">
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
id="tour-aviation-map"
|
id="tour-aviation-map"
|
||||||
|
|
@ -240,7 +242,7 @@ export default function SideNavigator(props: Props) {
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{(isMiPCA || user.hasFeature("admin")) && (
|
{((isOmni || isMiPCA) || user.hasFeature("admin")) && (
|
||||||
<Tooltip title="Terminals" placement="right">
|
<Tooltip title="Terminals" placement="right">
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
id="tour-terminals"
|
id="tour-terminals"
|
||||||
|
|
@ -374,6 +376,20 @@ export default function SideNavigator(props: Props) {
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
{(isAg || isStreamline || user.hasFeature("admin")) && (
|
||||||
|
<Tooltip title="Grain Types" placement="right">
|
||||||
|
<ListItemButton
|
||||||
|
id="tour-grain-types"
|
||||||
|
onClick={() => goTo("/grains")}
|
||||||
|
classes={getClasses("/grains")}
|
||||||
|
>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Grain />
|
||||||
|
</ListItemIcon>
|
||||||
|
{open && <ListItemText primary="Grain Types" />}
|
||||||
|
</ListItemButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
{(isAdCon || user.hasFeature("admin")) && (
|
{(isAdCon || user.hasFeature("admin")) && (
|
||||||
<Tooltip title="Jobsites" placement="right">
|
<Tooltip title="Jobsites" placement="right">
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ export default function ObjectHeaterCard(props: Props) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (heaterData.devices[0]) {
|
if (heaterData.devices && heaterData.devices[0]) {
|
||||||
setConnectedDevice(Device.any(heaterData.devices[0].device));
|
setConnectedDevice(Device.any(heaterData.devices[0].device));
|
||||||
heaterData.devices[0].components.forEach(c => {
|
heaterData.devices[0].components.forEach(c => {
|
||||||
//loop through the components and assign them accordingly
|
//loop through the components and assign them accordingly
|
||||||
|
|
@ -171,7 +171,7 @@ export default function ObjectHeaterCard(props: Props) {
|
||||||
|
|
||||||
const goToHeater = () => {
|
const goToHeater = () => {
|
||||||
//history.push("/objectHeaters/" + heater.key);
|
//history.push("/objectHeaters/" + heater.key);
|
||||||
let path = "/objectHeaters/" + heater.key;
|
let path = "/heaters/" + heater.key;
|
||||||
navigate(path, { state: {heater: heater} });
|
navigate(path, { state: {heater: heater} });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,6 @@ export default function NewObjectInteraction(props: Props){
|
||||||
setNewAlertComponentType(quack.ComponentType.COMPONENT_TYPE_INVALID);
|
setNewAlertComponentType(quack.ComponentType.COMPONENT_TYPE_INVALID);
|
||||||
setConditions([]);
|
setConditions([]);
|
||||||
setSelectedAlertComponents([]);
|
setSelectedAlertComponents([]);
|
||||||
setValidOptions([])
|
|
||||||
setValidComponents([])
|
setValidComponents([])
|
||||||
setValStrings(["0", "0"]);
|
setValStrings(["0", "0"]);
|
||||||
setNumConditions(0)
|
setNumConditions(0)
|
||||||
|
|
|
||||||
|
|
@ -819,6 +819,7 @@ export default function Bin(props: Props) {
|
||||||
fans={fans}
|
fans={fans}
|
||||||
heaters={heaters}
|
heaters={heaters}
|
||||||
grain={bin.grain()}
|
grain={bin.grain()}
|
||||||
|
customGrain={bin.customGrain()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Box marginTop={2}>
|
<Box marginTop={2}>
|
||||||
|
|
@ -968,6 +969,7 @@ export default function Bin(props: Props) {
|
||||||
fans={fans}
|
fans={fans}
|
||||||
heaters={heaters}
|
heaters={heaters}
|
||||||
grain={bin.grain()}
|
grain={bin.grain()}
|
||||||
|
customGrain={bin.customGrain()}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -128,10 +128,9 @@ export default function DevicePage() {
|
||||||
interaction.settings.nodeOne > interaction.settings.nodeTwo &&
|
interaction.settings.nodeOne > interaction.settings.nodeTwo &&
|
||||||
interaction.settings.nodeTwo !== 0
|
interaction.settings.nodeTwo !== 0
|
||||||
) {
|
) {
|
||||||
//flip operator and send negative comparitor to save
|
//flip operator to display what is actually happening
|
||||||
interaction.settings.conditions.forEach(condition => {
|
interaction.settings.conditions.forEach(condition => {
|
||||||
//coming back from the backend as a string for some reason
|
if (condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN) {
|
||||||
if (condition.comparison.toString() === "RELATIONAL_OPERATOR_GREATER_THAN") {
|
|
||||||
condition.comparison = quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN;
|
condition.comparison = quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN;
|
||||||
} else {
|
} else {
|
||||||
condition.comparison = quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN;
|
condition.comparison = quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN;
|
||||||
|
|
|
||||||
|
|
@ -192,7 +192,7 @@ export default function Gate(props: Props) {
|
||||||
const deviceDrawer = () => {
|
const deviceDrawer = () => {
|
||||||
return (
|
return (
|
||||||
<DeviceLinkDrawer
|
<DeviceLinkDrawer
|
||||||
deviceTags={["omniair"]}
|
deviceTags={["omniair", "mipca"]}
|
||||||
devicePrefMap={devPrefs}
|
devicePrefMap={devPrefs}
|
||||||
prefOptions={[
|
prefOptions={[
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
|
|
||||||
474
src/pages/Grains.tsx
Normal file
|
|
@ -0,0 +1,474 @@
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import PageContainer from "./PageContainer";
|
||||||
|
import CustomGrainForm from "grain/CustomGrainForm";
|
||||||
|
import { Accordion, AccordionDetails, AccordionSummary, Box, Button, DialogActions, DialogContent, DialogTitle, Divider, Grid2, IconButton, Typography } from "@mui/material";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
import ButtonGroup from "common/ButtonGroup";
|
||||||
|
import { useMobile, useSnackbar, useUserAPI } from "hooks";
|
||||||
|
import { useGrainAPI } from "providers/pond/grainAPI";
|
||||||
|
import ResponsiveTable from "common/ResponsiveTable";
|
||||||
|
import { GetGrainExtensionMap, GetValidGrainExtensions, GrainExtension } from "grain";
|
||||||
|
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||||
|
import { useGlobalState } from "providers";
|
||||||
|
import { Scope } from "models";
|
||||||
|
import React from "react";
|
||||||
|
import { AddCircle, ExpandMore } from "@mui/icons-material";
|
||||||
|
|
||||||
|
export default function Grains() {
|
||||||
|
const [currentCustomGrain, setCurrentCustomGrain] = useState<pond.GrainSettings>()
|
||||||
|
const [formValid, setFormValid] = useState(false)
|
||||||
|
const [selectedGrain, setSelectedGrain] = useState<pond.GrainObject | undefined>()
|
||||||
|
const [displayCustom, setDisplayCustom] = useState(false)
|
||||||
|
const { openSnack } = useSnackbar()
|
||||||
|
const [tableSize, setTableSize] = useState(10)
|
||||||
|
const [tablePage, setTablePage] = useState(0)
|
||||||
|
const [tableTotal, setTableTotal] = useState(0)
|
||||||
|
const grainAPI = useGrainAPI()
|
||||||
|
const userAPI = useUserAPI()
|
||||||
|
const [{as, user}] = useGlobalState();
|
||||||
|
const [permissions, setPermissions] = useState<pond.Permission[]>([])
|
||||||
|
const [openRemoveConfirmation, setOpenRemoveConfirmation] = useState(false)
|
||||||
|
const [openUpdateConfirmation, setOpenUpdateConfirmation] = useState(false)
|
||||||
|
const [grainObjectTableData, setGrainObjectTableData] = useState<pond.GrainObject[]>([])
|
||||||
|
const [supportedGrainTableData, setSupportedGrainTableData] = useState<GrainExtension[]>([])
|
||||||
|
// const [selectedGrainExtension, setSelectedGrainExtension] = useState<GrainExtension | undefined>()
|
||||||
|
const [accordionKey, setAccordionKey] = useState("allClosed")
|
||||||
|
const [openNewGrainDialog, setOpenNewGrainDialog] = useState(false)
|
||||||
|
const isMobile = useMobile()
|
||||||
|
const extraLoad = 10 //the amount of more to load on the mobile view when load more is clicked
|
||||||
|
|
||||||
|
//function to load the first set of custom grains
|
||||||
|
const loadCustomGrain = useCallback(() => {
|
||||||
|
grainAPI.listGrains(tableSize, tablePage * tableSize, "asc", "name").then(resp => {
|
||||||
|
setGrainObjectTableData(resp.data.grains)
|
||||||
|
setTableTotal(resp.data.total)
|
||||||
|
}).catch(err => {
|
||||||
|
openSnack("There was an issue loading your custom grains")
|
||||||
|
})
|
||||||
|
},[tableSize, tablePage])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
//load the custom grains
|
||||||
|
if(displayCustom){
|
||||||
|
loadCustomGrain()
|
||||||
|
}else{
|
||||||
|
let displayedGrains: GrainExtension[] = []
|
||||||
|
let offset = tablePage * tableSize
|
||||||
|
let limit = tableSize
|
||||||
|
let grainList = GetValidGrainExtensions()
|
||||||
|
let sortedGrain = grainList.sort((a, b) => (a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1))
|
||||||
|
sortedGrain.forEach((ext, i) => {
|
||||||
|
if(i >= offset && displayedGrains.length < limit){
|
||||||
|
displayedGrains.push(ext)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setSupportedGrainTableData(displayedGrains)
|
||||||
|
setTableTotal(sortedGrain.length)
|
||||||
|
}
|
||||||
|
},[loadCustomGrain, tableSize, tablePage, displayCustom, as])
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
//if the user is viewing as a team get their permissions to the team, if they are not then we will get their permission to the grain type individually when a type is selected
|
||||||
|
if(as){
|
||||||
|
userAPI.getUser(user.id(), { key: as, kind: "team" } as Scope).then(resp => {
|
||||||
|
setPermissions(resp.permissions);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},[as, userAPI])
|
||||||
|
|
||||||
|
const equationName = (enumVal: number) => {
|
||||||
|
switch(enumVal){
|
||||||
|
case pond.MoistureEquation.MOISTURE_EQUATION_CHUNG_PFOST:
|
||||||
|
return "Chung-Pfost"
|
||||||
|
case pond.MoistureEquation.MOISTURE_EQUATION_HALSEY:
|
||||||
|
return "Halsey"
|
||||||
|
case pond.MoistureEquation.MOISTURE_EQUATION_HENDERSON:
|
||||||
|
return "Henderson"
|
||||||
|
case pond.MoistureEquation.MOISTURE_EQUATION_OSWIN:
|
||||||
|
return "Oswin"
|
||||||
|
default:
|
||||||
|
return "None"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//function to update
|
||||||
|
|
||||||
|
//table that will display the custom grains created by the user
|
||||||
|
const grainObjectTable = () => {
|
||||||
|
return (
|
||||||
|
<ResponsiveTable<pond.GrainObject>
|
||||||
|
rows={grainObjectTableData}
|
||||||
|
noDataMessage="No Grain Types Found. Add a new custom grain to see them here"
|
||||||
|
loadMore={() => {
|
||||||
|
setTableSize(tableSize + extraLoad)
|
||||||
|
}}
|
||||||
|
renderMobile={(row) => <Accordion expanded={accordionKey === row.key} onChange={() => {
|
||||||
|
//close it if it is already open
|
||||||
|
if(accordionKey === row.key){
|
||||||
|
setAccordionKey("allClosed")
|
||||||
|
}else{
|
||||||
|
setAccordionKey(row.key)
|
||||||
|
}
|
||||||
|
setCurrentCustomGrain(row.settings ?? pond.GrainSettings.create())
|
||||||
|
}}>
|
||||||
|
<AccordionSummary expandIcon={<ExpandMore />}>{row.name}</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
{customGrainDisplay()}
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>}
|
||||||
|
onRowClick={(grainObj) => {
|
||||||
|
setSelectedGrain(grainObj)
|
||||||
|
setFormValid(true)
|
||||||
|
/** if they are viewin as a team leave the permissions alone, if not then use the users permissions to the grain type,
|
||||||
|
* for the moment grain types are not shareable so this will be unused, but this will cover our bases if we want to implement sharing in the future
|
||||||
|
* */
|
||||||
|
if (!as) {
|
||||||
|
userAPI.getUser(user.id(), { key: grainObj.key, kind: "grain" } as Scope).then(resp => {
|
||||||
|
setPermissions(resp.permissions);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
title: "Name",
|
||||||
|
render: row => <Box padding={2}>
|
||||||
|
<Typography>
|
||||||
|
{row.name}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Group",
|
||||||
|
render: row => <Box padding={2}>
|
||||||
|
<Typography>
|
||||||
|
{row.settings?.group}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Equation",
|
||||||
|
render: row => <Box padding={2}>
|
||||||
|
<Typography>
|
||||||
|
{equationName(row.settings?.equation ?? 0)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
page={tablePage}
|
||||||
|
pageSize={tableSize}
|
||||||
|
handleRowsPerPageChange={(e) => {
|
||||||
|
setTableSize(e.target.value)
|
||||||
|
}}
|
||||||
|
setPage={(page) => {
|
||||||
|
setTablePage(page)
|
||||||
|
}}
|
||||||
|
total={tableTotal}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
//table that will display the grain extensions provided by us
|
||||||
|
const supportedGrainTable = () => {
|
||||||
|
return (
|
||||||
|
<ResponsiveTable<GrainExtension>
|
||||||
|
rows={supportedGrainTableData}
|
||||||
|
page={tablePage}
|
||||||
|
// onRowClick={(ext) => {setSelectedGrainExtension(ext)}}
|
||||||
|
loadMore={() => {
|
||||||
|
setTableSize(tableSize + extraLoad)
|
||||||
|
}}
|
||||||
|
renderMobile={(row) => <Accordion>
|
||||||
|
<AccordionSummary expandIcon={<ExpandMore />}>{row.name}</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
{supportedDisplay(row)}
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>}
|
||||||
|
pageSize={tableSize}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
title: "Name",
|
||||||
|
render: row => <Box padding={2}>
|
||||||
|
<Typography>
|
||||||
|
{row.name}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Group",
|
||||||
|
render: row => <Box padding={2}>
|
||||||
|
<Typography>
|
||||||
|
{row.group}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Equation",
|
||||||
|
render: row => <Box padding={2}>
|
||||||
|
<Typography>
|
||||||
|
{equationName(row.equation ?? 0)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Kg Per Bushel",
|
||||||
|
render: row => <Box padding={2}>
|
||||||
|
<Typography>
|
||||||
|
{row.weightConversionKg.toFixed(2)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Bushels Per Tonne",
|
||||||
|
render: row => <Box padding={2}>
|
||||||
|
<Typography>
|
||||||
|
{row.bushelsPerTonne.toFixed(2)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
handleRowsPerPageChange={(e) => {
|
||||||
|
setTableSize(e.target.value)
|
||||||
|
}}
|
||||||
|
setPage={(page) => {
|
||||||
|
setTablePage(page)
|
||||||
|
}}
|
||||||
|
total={tableTotal}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
//the grain display for supported grain type to show the rest of the data such as what constants are used in the formula
|
||||||
|
const supportedDisplay = (grain?: GrainExtension) => {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{grain ?
|
||||||
|
<React.Fragment>
|
||||||
|
<Typography>
|
||||||
|
Info
|
||||||
|
</Typography>
|
||||||
|
<Divider />
|
||||||
|
<Box padding={2}>
|
||||||
|
<Typography>Name: {grain.name}</Typography>
|
||||||
|
<Typography>Group: {grain.group}</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography>
|
||||||
|
Formula
|
||||||
|
</Typography>
|
||||||
|
<Divider />
|
||||||
|
<Box padding={2}>
|
||||||
|
<Typography>Equation: {equationName(grain.equation)}</Typography>
|
||||||
|
{/* <Typography>Constant A: {grain.a}</Typography>
|
||||||
|
<Typography>Constant B: {grain.b}</Typography>
|
||||||
|
<Typography>Constant C: {grain.c}</Typography> */}
|
||||||
|
</Box>
|
||||||
|
<Typography>
|
||||||
|
Conversions
|
||||||
|
</Typography>
|
||||||
|
<Divider />
|
||||||
|
<Box padding={2}>
|
||||||
|
<Typography>Kg per Bushel: {grain.weightConversionKg.toFixed(2)}</Typography>
|
||||||
|
<Typography>Bushels per Tonne: {grain.bushelsPerTonne.toFixed(2)}</Typography>
|
||||||
|
</Box>
|
||||||
|
</React.Fragment>
|
||||||
|
:
|
||||||
|
<Typography>Select a Grain from the table to view its details</Typography>
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateGrain = () => {
|
||||||
|
if(selectedGrain && currentCustomGrain){
|
||||||
|
grainAPI.updateGrain(selectedGrain.key, currentCustomGrain).then(resp => {
|
||||||
|
openSnack("Grain type updated")
|
||||||
|
}).catch(err => {
|
||||||
|
openSnack("There was an issue updating the selected grain type")
|
||||||
|
}).finally(() => {
|
||||||
|
setOpenUpdateConfirmation(false)
|
||||||
|
loadCustomGrain()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const updateGrainConfirmation = () => {
|
||||||
|
return (
|
||||||
|
<ResponsiveDialog open={openUpdateConfirmation} onClose={() => {setOpenUpdateConfirmation(false)}}>
|
||||||
|
<DialogTitle>Update Custom Grain</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
Update existing custom grain type?
|
||||||
|
<br/>
|
||||||
|
Note that updating the grain here will not change any bins or components this type was used for, those will retain the old settings.
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button variant="contained" color="primary" onClick={()=>{setOpenUpdateConfirmation(false)}}>Cancel</Button>
|
||||||
|
<Button variant="contained" color="primary" onClick={()=>{updateGrain()}}>Confirm</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</ResponsiveDialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
//because the form is embedded on desktop this dialog is specifically for mobile view
|
||||||
|
const newGrainDialog = () => {
|
||||||
|
return (
|
||||||
|
<ResponsiveDialog open={openNewGrainDialog} onClose={() => setOpenNewGrainDialog(false)}>
|
||||||
|
<DialogTitle>Add New Grain</DialogTitle>
|
||||||
|
<DialogActions>
|
||||||
|
{customGrainDisplay()}
|
||||||
|
|
||||||
|
</DialogActions>
|
||||||
|
</ResponsiveDialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addNewGrain = () => {
|
||||||
|
if(currentCustomGrain){
|
||||||
|
grainAPI.addGrain(currentCustomGrain).then(resp => {
|
||||||
|
//new grain added
|
||||||
|
openSnack("New grain type added")
|
||||||
|
}).catch(err => {
|
||||||
|
openSnack("There was a problem creating the new grain type")
|
||||||
|
}).finally(() => {
|
||||||
|
loadCustomGrain()
|
||||||
|
setOpenNewGrainDialog(false)//only for the mobile view since the desktop has it embedded
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeGrain = () => {
|
||||||
|
if(selectedGrain){
|
||||||
|
grainAPI.removeGrain(selectedGrain.key).then(resp => {
|
||||||
|
openSnack("Grain type removed")
|
||||||
|
}).catch(err => {
|
||||||
|
openSnack("There was a problem removing the selected grain type")
|
||||||
|
}).finally(() => {
|
||||||
|
setOpenRemoveConfirmation(false)
|
||||||
|
loadCustomGrain()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const removeConfirmation = () => {
|
||||||
|
return (
|
||||||
|
<ResponsiveDialog open={openRemoveConfirmation} onClose={() => {setOpenRemoveConfirmation(false)}}>
|
||||||
|
<DialogTitle>Remove Custom Grain</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
Remove custom grain type?
|
||||||
|
<br />
|
||||||
|
Note that removing the grain here will not change any bins or components this type was used for, those will retain the old settings.
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button variant="contained" color="primary" onClick={()=>{setOpenRemoveConfirmation(false)}}>Cancel</Button>
|
||||||
|
<Button variant="contained" color="primary" onClick={()=>{removeGrain()}}>Confirm</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</ResponsiveDialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const customGrainActions = () => {
|
||||||
|
return (
|
||||||
|
<Box display="flex" justifyContent="space-between">
|
||||||
|
<Box>
|
||||||
|
{selectedGrain &&
|
||||||
|
<Button disabled={!permissions.includes(pond.Permission.PERMISSION_WRITE)} variant="contained" color="error" onClick={() => {setOpenRemoveConfirmation(true)}}>Remove Grain</Button>
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
{selectedGrain &&
|
||||||
|
<Button sx={{marginRight: 1}} onClick={() => setOpenUpdateConfirmation(true)} disabled={!formValid || !permissions.includes(pond.Permission.PERMISSION_WRITE)} variant="contained" color="primary">Update</Button>
|
||||||
|
}
|
||||||
|
{isMobile && !selectedGrain && <Button onClick={() => {setOpenNewGrainDialog(false)}}>Cancel</Button>}
|
||||||
|
<Button onClick={() => addNewGrain()} disabled={!formValid || (as !== "" && !permissions.includes(pond.Permission.PERMISSION_WRITE))} variant="contained" color="primary">Save New</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
//the grain display for custom grain the user has defined
|
||||||
|
const customGrainDisplay = () => {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<CustomGrainForm
|
||||||
|
grainSettings={selectedGrain?.settings || undefined}
|
||||||
|
onGrainSettingsChange={(newSettings, formValid)=>{
|
||||||
|
setCurrentCustomGrain(newSettings)
|
||||||
|
setFormValid(formValid)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{customGrainActions()}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const desktopView = () => {
|
||||||
|
if(displayCustom){
|
||||||
|
return (
|
||||||
|
<Grid2 container>
|
||||||
|
<Grid2 size={7} padding={2}>
|
||||||
|
{grainObjectTable()}
|
||||||
|
</Grid2>
|
||||||
|
<Grid2 size={5} padding={2}>
|
||||||
|
{customGrainDisplay()}
|
||||||
|
</Grid2>
|
||||||
|
</Grid2>
|
||||||
|
)
|
||||||
|
}else{
|
||||||
|
return (
|
||||||
|
<Box marginTop={2}>
|
||||||
|
{supportedGrainTable()}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mobileView = () => {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{displayCustom ? grainObjectTable() : supportedGrainTable()}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<Box padding={2}>
|
||||||
|
<Typography variant="h4">
|
||||||
|
Adaptive Grains
|
||||||
|
</Typography>
|
||||||
|
<Box display="flex" marginTop={2} justifyContent="space-between">
|
||||||
|
<ButtonGroup
|
||||||
|
toggle
|
||||||
|
buttons={[
|
||||||
|
{
|
||||||
|
title: "Supported",
|
||||||
|
function: () => {
|
||||||
|
setDisplayCustom(false)
|
||||||
|
setTablePage(0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Custom",
|
||||||
|
function: () => {
|
||||||
|
setDisplayCustom(true)
|
||||||
|
setTablePage(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{isMobile && displayCustom &&
|
||||||
|
<IconButton sx={{padding: 0}} color="primary" onClick={()=>{
|
||||||
|
setSelectedGrain(undefined)
|
||||||
|
setCurrentCustomGrain(undefined)
|
||||||
|
setOpenNewGrainDialog(true)
|
||||||
|
}}>
|
||||||
|
<AddCircle />
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
{!isMobile ? desktopView() : mobileView()}
|
||||||
|
</Box>
|
||||||
|
{updateGrainConfirmation()}
|
||||||
|
{removeConfirmation()}
|
||||||
|
{newGrainDialog()}
|
||||||
|
</PageContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -56,8 +56,8 @@ export default function Site() {
|
||||||
siteAPI
|
siteAPI
|
||||||
.getSitePage(siteKey, as)
|
.getSitePage(siteKey, as)
|
||||||
.then(resp => {
|
.then(resp => {
|
||||||
setSite(ISite.any(resp.data.site));
|
if(resp.data.site) setSite(ISite.any(resp.data.site));
|
||||||
setHeaters(resp.data.heaterData);
|
if(resp.data.heaterData) setHeaters(resp.data.heaterData);
|
||||||
setLoadingSite(false);
|
setLoadingSite(false);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
|
|
||||||
|
|
@ -182,7 +182,6 @@ export default function Terminals(props: Props) {
|
||||||
if(loading) return
|
if(loading) return
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
terminalAPI.listTerminals(200, 0, "asc", "name", undefined, as).then(resp => {
|
terminalAPI.listTerminals(200, 0, "asc", "name", undefined, as).then(resp => {
|
||||||
console.log(resp.data)
|
|
||||||
if(resp.data.terminals){
|
if(resp.data.terminals){
|
||||||
setTerminals(resp.data.terminals.map(a => Terminal.any(a)));
|
setTerminals(resp.data.terminals.map(a => Terminal.any(a)));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,7 @@ export default function GateProvider(props: PropsWithChildren<Props>) {
|
||||||
return new Promise<AxiosResponse<pond.ListGatesResponse>>((resolve, reject) => {
|
return new Promise<AxiosResponse<pond.ListGatesResponse>>((resolve, reject) => {
|
||||||
get<pond.ListGatesResponse>(url).then(resp => {
|
get<pond.ListGatesResponse>(url).then(resp => {
|
||||||
resp.data = pond.ListGatesResponse.fromObject(resp.data)
|
resp.data = pond.ListGatesResponse.fromObject(resp.data)
|
||||||
|
console.log(resp)
|
||||||
return resolve(resp)
|
return resolve(resp)
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
return reject(err)
|
return reject(err)
|
||||||
|
|
|
||||||
137
src/providers/pond/grainAPI.tsx
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
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";
|
||||||
|
import { or } from "utils";
|
||||||
|
|
||||||
|
export interface IGrainInterface {
|
||||||
|
addGrain: (
|
||||||
|
settings: pond.GrainSettings,
|
||||||
|
otherTeam?: string
|
||||||
|
) => Promise<AxiosResponse<pond.AddGrainResponse>>;
|
||||||
|
getGrain: (key: string, otherTeam?: string) => Promise<AxiosResponse<pond.GetGrainResponse>>
|
||||||
|
listGrains: (
|
||||||
|
limit: number,
|
||||||
|
offset: number,
|
||||||
|
order?: "asc" | "desc",
|
||||||
|
orderBy?: string,
|
||||||
|
search?: string,
|
||||||
|
keys?: string[],
|
||||||
|
types?: string[],
|
||||||
|
otherTeam?: string,
|
||||||
|
) => Promise<AxiosResponse<pond.ListGrainsResponse>>;
|
||||||
|
updateGrain: (key: string, settings: pond.GrainSettings, otherTeam?: string) => Promise<AxiosResponse<pond.UpdateGrainResponse>>
|
||||||
|
removeGrain: (key: string, otherTeam?: string) => Promise<AxiosResponse<pond.RemoveGrainResponse>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GrainAPIcontext = createContext<IGrainInterface>({} as IGrainInterface);
|
||||||
|
|
||||||
|
interface Props {}
|
||||||
|
|
||||||
|
export default function GrainProvider(props: PropsWithChildren<Props>) {
|
||||||
|
const { children } = props;
|
||||||
|
const { get, del, post, put } = useHTTP();
|
||||||
|
const [{ as }] = useGlobalState();
|
||||||
|
|
||||||
|
//add
|
||||||
|
const addGrain = (settings: pond.GrainSettings, otherTeam?: string) => {
|
||||||
|
const view = otherTeam ? otherTeam : as
|
||||||
|
if (view) {
|
||||||
|
return post<pond.AddGrainResponse>(
|
||||||
|
pondURL("/grains?&as=" + view),
|
||||||
|
settings
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return post<pond.AddGrainResponse>(pondURL("/grains"), settings);
|
||||||
|
};
|
||||||
|
|
||||||
|
//get
|
||||||
|
const getGrain = (key: string, otherTeam?: string) => {
|
||||||
|
const view = otherTeam ? otherTeam : as
|
||||||
|
const url = "/grains/" + key + (view ? "?as=" + view : "")
|
||||||
|
return new Promise<AxiosResponse<pond.GetGrainResponse>>((resolve, reject)=>{
|
||||||
|
get<pond.GetGrainResponse>(pondURL(url))
|
||||||
|
.then(resp => {
|
||||||
|
if (resp.data.grain){
|
||||||
|
resp.data.grain = pond.GrainObject.fromObject(resp.data.grain)
|
||||||
|
}
|
||||||
|
resolve(resp)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//list
|
||||||
|
const listGrains = (
|
||||||
|
limit: number,
|
||||||
|
offset: number,
|
||||||
|
order?: "asc" | "desc",
|
||||||
|
orderBy?: string,
|
||||||
|
search?: string,
|
||||||
|
keys?: string[],
|
||||||
|
types?: string[],
|
||||||
|
otherTeam?: string
|
||||||
|
) => {
|
||||||
|
return new Promise<AxiosResponse<pond.ListGrainsResponse>>((resolve, reject)=>{
|
||||||
|
const view = otherTeam ? otherTeam : as
|
||||||
|
get<pond.ListGrainsResponse>(
|
||||||
|
pondURL(
|
||||||
|
"/grains?limit=" +
|
||||||
|
limit +
|
||||||
|
"&offset=" +
|
||||||
|
offset +
|
||||||
|
("&order=" + or(order, "asc")) +
|
||||||
|
("&by=" + or(orderBy, "key")) +
|
||||||
|
(search ? "&search=" + search : "") +
|
||||||
|
(keys ? "&keys=" + keys.join(",") : "") +
|
||||||
|
(types ? "&types=" + types.join(",") : "") +
|
||||||
|
(view ? "&as=" + view : "")
|
||||||
|
)
|
||||||
|
).then(resp => {
|
||||||
|
resp.data = pond.ListGrainsResponse.fromObject(resp.data);
|
||||||
|
return resolve(resp)
|
||||||
|
}).catch(err => {
|
||||||
|
return reject(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
//update
|
||||||
|
const updateGrain = (key: string, settings: pond.GrainSettings, otherTeam?: string) => {
|
||||||
|
const view = otherTeam ? otherTeam : as
|
||||||
|
if (view) {
|
||||||
|
return put<pond.UpdateGrainResponse>(
|
||||||
|
pondURL("/grains/"+ key + "?&as=" + view),
|
||||||
|
settings
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return put<pond.UpdateGrainResponse>(pondURL("/grains/" + key), settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
//remove
|
||||||
|
const removeGrain = (key: string, otherTeam?: string) => {
|
||||||
|
const view = otherTeam ? otherTeam : as
|
||||||
|
if (view) {
|
||||||
|
return del<pond.RemoveGrainResponse>(pondURL("/grains/" + key + "?as=" + view));
|
||||||
|
}
|
||||||
|
return del<pond.RemoveGrainResponse>(pondURL("/grains/" + key));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GrainAPIcontext.Provider
|
||||||
|
value={{
|
||||||
|
addGrain,
|
||||||
|
getGrain,
|
||||||
|
listGrains,
|
||||||
|
updateGrain,
|
||||||
|
removeGrain
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</GrainAPIcontext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGrainAPI = () => useContext(GrainAPIcontext);
|
||||||
|
|
@ -36,6 +36,7 @@ export interface IInteractionsAPIContext {
|
||||||
alerts: pond.AlertData[],
|
alerts: pond.AlertData[],
|
||||||
otherTeam?: string
|
otherTeam?: string
|
||||||
) => Promise<AxiosResponse<pond.SetAlertInteractionsResponse>>;
|
) => Promise<AxiosResponse<pond.SetAlertInteractionsResponse>>;
|
||||||
|
clearInteractions: (device: number, sources?: quack.ComponentID[], otherTeam?: string) => Promise<AxiosResponse<pond.ClearInteractionsResponse>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InteractionsAPIContext = createContext<IInteractionsAPIContext>(
|
export const InteractionsAPIContext = createContext<IInteractionsAPIContext>(
|
||||||
|
|
@ -285,6 +286,25 @@ export default function InteractionProvider(props: PropsWithChildren<Props>) {
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clearInteractions = (device: number, sources?: quack.ComponentID[], otherTeam?: string): Promise<AxiosResponse<pond.ClearInteractionsResponse>> => {
|
||||||
|
const view = otherTeam ? otherTeam : as
|
||||||
|
let sourceArray: string[] = []
|
||||||
|
if (sources){
|
||||||
|
sources.forEach(source => {
|
||||||
|
sourceArray.push(componentIDToString(source))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
let url = pondURL("/devices/"+ device + "/interactions/clear" + (view ? "?as=" + view : "") + (sources ? "&sources=" + sourceArray.toString() : ""))
|
||||||
|
return new Promise<AxiosResponse<pond.ClearInteractionsResponse>>((resolve, reject) => {
|
||||||
|
post<pond.ClearInteractionsResponse>(url).then(resp => {
|
||||||
|
resp.data = pond.ClearInteractionsResponse.fromObject(resp.data)
|
||||||
|
return resolve(resp)
|
||||||
|
}).catch(err => {
|
||||||
|
return reject(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InteractionsAPIContext.Provider
|
<InteractionsAPIContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
|
@ -296,7 +316,8 @@ export default function InteractionProvider(props: PropsWithChildren<Props>) {
|
||||||
updateInteractionPondSettings,
|
updateInteractionPondSettings,
|
||||||
removeInteraction,
|
removeInteraction,
|
||||||
listInteractionsByDevice,
|
listInteractionsByDevice,
|
||||||
listInteractionsByComponent
|
listInteractionsByComponent,
|
||||||
|
clearInteractions
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</InteractionsAPIContext.Provider>
|
</InteractionsAPIContext.Provider>
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ import JohnDeereProvider, { useJohnDeereProxyAPI } from "./johnDeereProxyAPI";
|
||||||
import LibraCartProvider, { useLibraCartProxyAPI } from "./libracartProxyAPI";
|
import LibraCartProvider, { useLibraCartProxyAPI } from "./libracartProxyAPI";
|
||||||
import CNHiProvider, { useCNHiProxyAPI } from "./cnhiProxyAPI";
|
import CNHiProvider, { useCNHiProxyAPI } from "./cnhiProxyAPI";
|
||||||
import DevicePresetProvider, { useDevicePresetAPI } from "./devicePresetAPI";
|
import DevicePresetProvider, { useDevicePresetAPI } from "./devicePresetAPI";
|
||||||
|
import GrainProvider, { useGrainAPI } from "./grainAPI";
|
||||||
// import NoteProvider from "providers/noteAPI";
|
// import NoteProvider from "providers/noteAPI";
|
||||||
|
|
||||||
export const pondURL = (partial: string, demo: boolean = false): string => {
|
export const pondURL = (partial: string, demo: boolean = false): string => {
|
||||||
|
|
@ -93,7 +94,9 @@ export default function PondProvider(props: PropsWithChildren<any>) {
|
||||||
<KeyManagerProvider>
|
<KeyManagerProvider>
|
||||||
<DevicePresetProvider>
|
<DevicePresetProvider>
|
||||||
<ImagekitProvider>
|
<ImagekitProvider>
|
||||||
{children}
|
<GrainProvider>
|
||||||
|
{children}
|
||||||
|
</GrainProvider>
|
||||||
</ImagekitProvider>
|
</ImagekitProvider>
|
||||||
</DevicePresetProvider>
|
</DevicePresetProvider>
|
||||||
</KeyManagerProvider>
|
</KeyManagerProvider>
|
||||||
|
|
@ -173,5 +176,6 @@ export {
|
||||||
useJohnDeereProxyAPI,
|
useJohnDeereProxyAPI,
|
||||||
useCNHiProxyAPI,
|
useCNHiProxyAPI,
|
||||||
useLibraCartProxyAPI,
|
useLibraCartProxyAPI,
|
||||||
useDevicePresetAPI
|
useDevicePresetAPI,
|
||||||
|
useGrainAPI
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import AeroGrowDarkLogo from "../assets/whitelabels/AeroGrow/darkLogo.png";
|
||||||
import AeroGrowLightLogo from "../assets/whitelabels/AeroGrow/lightLogo.png";
|
import AeroGrowLightLogo from "../assets/whitelabels/AeroGrow/lightLogo.png";
|
||||||
import MiVentLightLogo from "../assets/whitelabels/MiVent/lightLogo.png";
|
import MiVentLightLogo from "../assets/whitelabels/MiVent/lightLogo.png";
|
||||||
// import OmniAirLogo from "../assets/whitelabels/OmniAir/OmniAirLogo.png";
|
// import OmniAirLogo from "../assets/whitelabels/OmniAir/OmniAirLogo.png";
|
||||||
import MiPCALogo from "../assets/whitelabels/OmniAir/MiPCALogo.png";
|
import MiPCALogo from "../assets/whitelabels/MiPCA/MiPCALogo.png";
|
||||||
import StreamlineLogo from "../assets/whitelabels/Streamline/stream-logo.png"
|
import StreamlineLogo from "../assets/whitelabels/Streamline/stream-logo.png"
|
||||||
// import { green, yellow } from "@mui/material/colors";
|
// import { green, yellow } from "@mui/material/colors";
|
||||||
|
|
||||||
|
|
@ -284,6 +284,32 @@ export function IsOmniAir(): boolean {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MIPCA_WHITE_LABEL: WhiteLabel = {
|
||||||
|
name: "MiPCA",
|
||||||
|
primaryColour: "#004f9b",
|
||||||
|
secondaryColour: "yellow",
|
||||||
|
signatureColour: "#272727",
|
||||||
|
signatureAccentColour: "#fff",
|
||||||
|
//omni air and MiPCA are the same client ID in Auth0, it is to replace it, once omniair gets removed we can re-name this
|
||||||
|
auth0ClientId: import.meta.env.VITE_AUTH0_OMNIAIR_CLIENT_ID,
|
||||||
|
redirectOnLogout: true,
|
||||||
|
logoutRedirectTarget: "https://mionetech.com",
|
||||||
|
darkLogo: MiPCALogo,
|
||||||
|
lightLogo: MiPCALogo,
|
||||||
|
transparentLogoBG: true,
|
||||||
|
blacklist: ["cost"],
|
||||||
|
docs: "MiPCA",
|
||||||
|
protips: protips.concat([])
|
||||||
|
};
|
||||||
|
|
||||||
|
export function IsMiPCA(): boolean {
|
||||||
|
return (
|
||||||
|
getName() === "MiPCA" ||
|
||||||
|
window.location.origin.includes("staging") ||
|
||||||
|
window.location.origin.includes("localhost")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const whitelabels = new Map<string, WhiteLabel>([
|
const whitelabels = new Map<string, WhiteLabel>([
|
||||||
["streamline", STREAMLINE_WHITE_LABEL],
|
["streamline", STREAMLINE_WHITE_LABEL],
|
||||||
["adaptiveag", ADAPTIVE_AGRICULTURE_WHITE_LABEL],
|
["adaptiveag", ADAPTIVE_AGRICULTURE_WHITE_LABEL],
|
||||||
|
|
@ -296,7 +322,8 @@ const whitelabels = new Map<string, WhiteLabel>([
|
||||||
["10.0", ADAPTIVE_CONSTRUCTION_WHITE_LABEL],
|
["10.0", ADAPTIVE_CONSTRUCTION_WHITE_LABEL],
|
||||||
["mivent", MIVENT_WHITE_LABEL],
|
["mivent", MIVENT_WHITE_LABEL],
|
||||||
["adaptiveconstruction", ADAPTIVE_CONSTRUCTION_WHITE_LABEL],
|
["adaptiveconstruction", ADAPTIVE_CONSTRUCTION_WHITE_LABEL],
|
||||||
["omniair", OMNIAIR_WHITE_LABEL]
|
["omniair", OMNIAIR_WHITE_LABEL],
|
||||||
|
["mipca", MIPCA_WHITE_LABEL]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function getWhitelabel(): WhiteLabel {
|
export function getWhitelabel(): WhiteLabel {
|
||||||
|
|
@ -309,7 +336,7 @@ export function getWhitelabel(): WhiteLabel {
|
||||||
return BXT_WHITE_LABEL;
|
return BXT_WHITE_LABEL;
|
||||||
}
|
}
|
||||||
if (window.location.origin.includes("localhost")) {
|
if (window.location.origin.includes("localhost")) {
|
||||||
return STREAMLINE_WHITE_LABEL;
|
return MIPCA_WHITE_LABEL;
|
||||||
}
|
}
|
||||||
if (window.location.origin.includes("staging") || import.meta.env.VITE_LOCAL_STAGING=='true') {
|
if (window.location.origin.includes("staging") || import.meta.env.VITE_LOCAL_STAGING=='true') {
|
||||||
return STAGING_WHITELABEL;
|
return STAGING_WHITELABEL;
|
||||||
|
|
|
||||||