added the cable estimator and the pdf stuff that goes with it

This commit is contained in:
csawatzky 2025-04-09 14:49:04 -06:00
parent 9074120f74
commit e039a75bf8
26 changed files with 3157 additions and 24 deletions

486
package-lock.json generated
View file

@ -24,6 +24,7 @@
"@mui/material": "^6.1.6", "@mui/material": "^6.1.6",
"@mui/styles": "^6.1.6", "@mui/styles": "^6.1.6",
"@mui/x-date-pickers": "^7.26.0", "@mui/x-date-pickers": "^7.26.0",
"@react-pdf/renderer": "^4.3.0",
"@sentry/react": "^8.38.0", "@sentry/react": "^8.38.0",
"@turf/area": "^7.2.0", "@turf/area": "^7.2.0",
"@turf/turf": "^7.2.0", "@turf/turf": "^7.2.0",
@ -3216,6 +3217,180 @@
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
}, },
"node_modules/@react-pdf/fns": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@react-pdf/fns/-/fns-3.1.2.tgz",
"integrity": "sha512-qTKGUf0iAMGg2+OsUcp9ffKnKi41RukM/zYIWMDJ4hRVYSr89Q7e3wSDW/Koqx3ea3Uy/z3h2y3wPX6Bdfxk6g==",
"license": "MIT"
},
"node_modules/@react-pdf/font": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@react-pdf/font/-/font-4.0.2.tgz",
"integrity": "sha512-/dAWu7Y2RD1RxarDZ9SkYPHgBYOhmcDnet4W/qN/m8k+A2Hr3ja54GymSR7GGxWBtxjKtNauVKrTa9LS1n8WUw==",
"license": "MIT",
"dependencies": {
"@react-pdf/pdfkit": "^4.0.3",
"@react-pdf/types": "^2.9.0",
"fontkit": "^2.0.2",
"is-url": "^1.2.4"
}
},
"node_modules/@react-pdf/image": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@react-pdf/image/-/image-3.0.3.tgz",
"integrity": "sha512-lvP5ryzYM3wpbO9bvqLZYwEr5XBDX9jcaRICvtnoRqdJOo7PRrMnmB4MMScyb+Xw10mGeIubZAAomNAG5ONQZQ==",
"license": "MIT",
"dependencies": {
"@react-pdf/png-js": "^3.0.0",
"jay-peg": "^1.1.1"
}
},
"node_modules/@react-pdf/layout": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@react-pdf/layout/-/layout-4.4.0.tgz",
"integrity": "sha512-Aq+Cc6JYausWLoks2FvHe3PwK9cTuvksB2uJ0AnkKJEUtQbvCq8eCRb1bjbbwIji9OzFRTTzZij7LzkpKHjIeA==",
"license": "MIT",
"dependencies": {
"@react-pdf/fns": "3.1.2",
"@react-pdf/image": "^3.0.3",
"@react-pdf/primitives": "^4.1.1",
"@react-pdf/stylesheet": "^6.1.0",
"@react-pdf/textkit": "^6.0.0",
"@react-pdf/types": "^2.9.0",
"emoji-regex": "^10.3.0",
"queue": "^6.0.1",
"yoga-layout": "^3.2.1"
}
},
"node_modules/@react-pdf/pdfkit": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@react-pdf/pdfkit/-/pdfkit-4.0.3.tgz",
"integrity": "sha512-k+Lsuq8vTwWsCqTp+CCB4+2N+sOTFrzwGA7aw3H9ix/PDWR9QksbmNg0YkzGbLAPI6CeawmiLHcf4trZ5ecLPQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@react-pdf/png-js": "^3.0.0",
"browserify-zlib": "^0.2.0",
"crypto-js": "^4.2.0",
"fontkit": "^2.0.2",
"jay-peg": "^1.1.1",
"linebreak": "^1.1.0",
"vite-compatible-readable-stream": "^3.6.1"
}
},
"node_modules/@react-pdf/png-js": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@react-pdf/png-js/-/png-js-3.0.0.tgz",
"integrity": "sha512-eSJnEItZ37WPt6Qv5pncQDxLJRK15eaRwPT+gZoujP548CodenOVp49GST8XJvKMFt9YqIBzGBV/j9AgrOQzVA==",
"license": "MIT",
"dependencies": {
"browserify-zlib": "^0.2.0"
}
},
"node_modules/@react-pdf/primitives": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@react-pdf/primitives/-/primitives-4.1.1.tgz",
"integrity": "sha512-IuhxYls1luJb7NUWy6q5avb1XrNaVj9bTNI40U9qGRuS6n7Hje/8H8Qi99Z9UKFV74bBP3DOf3L1wV2qZVgVrQ==",
"license": "MIT"
},
"node_modules/@react-pdf/reconciler": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@react-pdf/reconciler/-/reconciler-1.1.4.tgz",
"integrity": "sha512-oTQDiR/t4Z/Guxac88IavpU2UgN7eR0RMI9DRKvKnvPz2DUasGjXfChAdMqDNmJJxxV26mMy9xQOUV2UU5/okg==",
"license": "MIT",
"dependencies": {
"object-assign": "^4.1.1",
"scheduler": "0.25.0-rc-603e6108-20241029"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@react-pdf/reconciler/node_modules/scheduler": {
"version": "0.25.0-rc-603e6108-20241029",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-rc-603e6108-20241029.tgz",
"integrity": "sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA==",
"license": "MIT"
},
"node_modules/@react-pdf/render": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@react-pdf/render/-/render-4.3.0.tgz",
"integrity": "sha512-MdWfWaqO6d7SZD75TZ2z5L35V+cHpyA43YNRlJNG0RJ7/MeVGDQv12y/BXOJgonZKkeEGdzM3EpAt9/g4E22WA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@react-pdf/fns": "3.1.2",
"@react-pdf/primitives": "^4.1.1",
"@react-pdf/textkit": "^6.0.0",
"@react-pdf/types": "^2.9.0",
"abs-svg-path": "^0.1.1",
"color-string": "^1.9.1",
"normalize-svg-path": "^1.1.0",
"parse-svg-path": "^0.1.2",
"svg-arc-to-cubic-bezier": "^3.2.0"
}
},
"node_modules/@react-pdf/renderer": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@react-pdf/renderer/-/renderer-4.3.0.tgz",
"integrity": "sha512-28gpA69fU9ZQrDzmd5xMJa1bDf8t0PT3ApUKBl2PUpoE/x4JlvCB5X66nMXrfFrgF2EZrA72zWQAkvbg7TE8zw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@react-pdf/fns": "3.1.2",
"@react-pdf/font": "^4.0.2",
"@react-pdf/layout": "^4.4.0",
"@react-pdf/pdfkit": "^4.0.3",
"@react-pdf/primitives": "^4.1.1",
"@react-pdf/reconciler": "^1.1.4",
"@react-pdf/render": "^4.3.0",
"@react-pdf/types": "^2.9.0",
"events": "^3.3.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"queue": "^6.0.1"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@react-pdf/stylesheet": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@react-pdf/stylesheet/-/stylesheet-6.1.0.tgz",
"integrity": "sha512-BGZ2sYNUp38VJUegjva/jsri3iiRGnVNjWI+G9dTwAvLNOmwFvSJzqaCsEnqQ/DW5mrTBk/577FhDY7pv6AidA==",
"license": "MIT",
"dependencies": {
"@react-pdf/fns": "3.1.2",
"@react-pdf/types": "^2.9.0",
"color-string": "^1.9.1",
"hsl-to-hex": "^1.0.0",
"media-engine": "^1.0.3",
"postcss-value-parser": "^4.1.0"
}
},
"node_modules/@react-pdf/textkit": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@react-pdf/textkit/-/textkit-6.0.0.tgz",
"integrity": "sha512-fDt19KWaJRK/n2AaFoVm31hgGmpygmTV7LsHGJNGZkgzXcFyLsx+XUl63DTDPH3iqxj3xUX128t104GtOz8tTw==",
"license": "MIT",
"dependencies": {
"@react-pdf/fns": "3.1.2",
"bidi-js": "^1.0.2",
"hyphen": "^1.6.4",
"unicode-properties": "^1.4.1"
}
},
"node_modules/@react-pdf/types": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/@react-pdf/types/-/types-2.9.0.tgz",
"integrity": "sha512-ckj80vZLlvl9oYrQ4tovEaqKWP3O06Eb1D48/jQWbdwz1Yh7Y9v1cEmwlP8ET+a1Whp8xfdM0xduMexkuPANCQ==",
"license": "MIT",
"dependencies": {
"@react-pdf/font": "^4.0.2",
"@react-pdf/primitives": "^4.1.1",
"@react-pdf/stylesheet": "^6.1.0"
}
},
"node_modules/@remix-run/router": { "node_modules/@remix-run/router": {
"version": "1.21.0", "version": "1.21.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.21.0.tgz", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.21.0.tgz",
@ -3735,6 +3910,15 @@
"sourcemap-codec": "^1.4.8" "sourcemap-codec": "^1.4.8"
} }
}, },
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@szmarczak/http-timer": { "node_modules/@szmarczak/http-timer": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
@ -6597,6 +6781,12 @@
"url": "https://opencollective.com/vitest" "url": "https://opencollective.com/vitest"
} }
}, },
"node_modules/abs-svg-path": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz",
"integrity": "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==",
"license": "MIT"
},
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.14.0", "version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
@ -6858,6 +7048,35 @@
"resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
"integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA=="
}, },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/bidi-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
"license": "MIT",
"dependencies": {
"require-from-string": "^2.0.2"
}
},
"node_modules/bignumber.js": { "node_modules/bignumber.js": {
"version": "9.1.2", "version": "9.1.2",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz",
@ -6889,6 +7108,24 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/brotli": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
"license": "MIT",
"dependencies": {
"base64-js": "^1.1.2"
}
},
"node_modules/browserify-zlib": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
"integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
"license": "MIT",
"dependencies": {
"pako": "~1.0.5"
}
},
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.24.4", "version": "4.24.4",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
@ -7153,6 +7390,15 @@
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
}, },
"node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
"license": "MIT",
"engines": {
"node": ">=0.8"
}
},
"node_modules/clone-response": { "node_modules/clone-response": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
@ -7190,9 +7436,18 @@
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"license": "MIT",
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/combined-stream": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -7302,6 +7557,12 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
},
"node_modules/crypto-random-string": { "node_modules/crypto-random-string": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
@ -7759,6 +8020,12 @@
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/dfa": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==",
"license": "MIT"
},
"node_modules/doc-path": { "node_modules/doc-path": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/doc-path/-/doc-path-4.1.1.tgz", "resolved": "https://registry.npmjs.org/doc-path/-/doc-path-4.1.1.tgz",
@ -7820,6 +8087,12 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/emoji-regex": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
"integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
"license": "MIT"
},
"node_modules/end-of-stream": { "node_modules/end-of-stream": {
"version": "1.4.4", "version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@ -8443,6 +8716,23 @@
} }
} }
}, },
"node_modules/fontkit": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
"integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
"license": "MIT",
"dependencies": {
"@swc/helpers": "^0.5.12",
"brotli": "^1.3.2",
"clone": "^2.1.2",
"dfa": "^1.2.0",
"fast-deep-equal": "^3.1.3",
"restructure": "^3.0.0",
"tiny-inflate": "^1.0.3",
"unicode-properties": "^1.4.0",
"unicode-trie": "^2.0.0"
}
},
"node_modules/for-each": { "node_modules/for-each": {
"version": "0.3.5", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@ -9012,6 +9302,21 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/hsl-to-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/hsl-to-hex/-/hsl-to-hex-1.0.0.tgz",
"integrity": "sha512-K6GVpucS5wFf44X0h2bLVRDsycgJmf9FF2elg+CrqD8GcFU8c6vYhgXn8NjUkFCwj+xDFb70qgLbTUm6sxwPmA==",
"license": "MIT",
"dependencies": {
"hsl-to-rgb-for-reals": "^1.1.0"
}
},
"node_modules/hsl-to-rgb-for-reals": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/hsl-to-rgb-for-reals/-/hsl-to-rgb-for-reals-1.1.1.tgz",
"integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==",
"license": "ISC"
},
"node_modules/http-cache-semantics": { "node_modules/http-cache-semantics": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
@ -9031,6 +9336,12 @@
"node": ">=10.19.0" "node": ">=10.19.0"
} }
}, },
"node_modules/hyphen": {
"version": "1.10.6",
"resolved": "https://registry.npmjs.org/hyphen/-/hyphen-1.10.6.tgz",
"integrity": "sha512-fXHXcGFTXOvZTSkPJuGOQf5Lv5T/R2itiiCVPg9LxAje5D00O0pP83yJShFq5V89Ly//Gt6acj7z8pbBr34stw==",
"license": "ISC"
},
"node_modules/hyphenate-style-name": { "node_modules/hyphenate-style-name": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz",
@ -9121,7 +9432,6 @@
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/internal-slot": { "node_modules/internal-slot": {
@ -9561,6 +9871,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-url": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
"license": "MIT"
},
"node_modules/is-weakmap": { "node_modules/is-weakmap": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
@ -9648,6 +9964,15 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/jay-peg": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/jay-peg/-/jay-peg-1.1.1.tgz",
"integrity": "sha512-D62KEuBxz/ip2gQKOEhk/mx14o7eiFRaU+VNNSP4MOiIkwb/D6B3G1Mfas7C/Fit8EsSV2/IWjZElx/Gs6A4ww==",
"license": "MIT",
"dependencies": {
"restructure": "^3.0.0"
}
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -9907,6 +10232,25 @@
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.15.tgz", "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.15.tgz",
"integrity": "sha512-M7+rtYi9l5RvMmHyjyoF3BHHUpXTYdJ0PezZGHNs0GyW1lO+K7jxlXpbdIb7a56h0nqLYdjIw+E+z0ciGaJP7g==" "integrity": "sha512-M7+rtYi9l5RvMmHyjyoF3BHHUpXTYdJ0PezZGHNs0GyW1lO+K7jxlXpbdIb7a56h0nqLYdjIw+E+z0ciGaJP7g=="
}, },
"node_modules/linebreak": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
"integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
"license": "MIT",
"dependencies": {
"base64-js": "0.0.8",
"unicode-trie": "^2.0.0"
}
},
"node_modules/linebreak/node_modules/base64-js": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
"integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/lines-and-columns": { "node_modules/lines-and-columns": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@ -10112,6 +10456,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/media-engine": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/media-engine/-/media-engine-1.0.3.tgz",
"integrity": "sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==",
"license": "MIT"
},
"node_modules/memoize-one": { "node_modules/memoize-one": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
@ -10416,6 +10766,15 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/normalize-svg-path": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz",
"integrity": "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==",
"license": "MIT",
"dependencies": {
"svg-arc-to-cubic-bezier": "^3.0.0"
}
},
"node_modules/normalize-url": { "node_modules/normalize-url": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
@ -10609,6 +10968,12 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/parent-module": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -10637,6 +11002,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/parse-svg-path": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz",
"integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==",
"license": "MIT"
},
"node_modules/path-exists": { "node_modules/path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -10805,6 +11176,12 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"license": "MIT"
},
"node_modules/potpack": { "node_modules/potpack": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz", "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz",
@ -10920,6 +11297,15 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/queue": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
"integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
"license": "MIT",
"dependencies": {
"inherits": "~2.0.3"
}
},
"node_modules/queue-microtask": { "node_modules/queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@ -11712,7 +12098,6 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@ -11769,6 +12154,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/restructure": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
"integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==",
"license": "MIT"
},
"node_modules/reusify": { "node_modules/reusify": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@ -11877,7 +12268,6 @@
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@ -12153,6 +12543,21 @@
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
"dev": true "dev": true
}, },
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"license": "MIT",
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/simple-swizzle/node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
"license": "MIT"
},
"node_modules/skmeans": { "node_modules/skmeans": {
"version": "0.9.7", "version": "0.9.7",
"resolved": "https://registry.npmjs.org/skmeans/-/skmeans-0.9.7.tgz", "resolved": "https://registry.npmjs.org/skmeans/-/skmeans-0.9.7.tgz",
@ -12345,6 +12750,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/string.prototype.matchall": { "node_modules/string.prototype.matchall": {
"version": "4.0.12", "version": "4.0.12",
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
@ -12535,6 +12949,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/svg-arc-to-cubic-bezier": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz",
"integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==",
"license": "ISC"
},
"node_modules/sweepline-intersections": { "node_modules/sweepline-intersections": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/sweepline-intersections/-/sweepline-intersections-1.5.0.tgz", "resolved": "https://registry.npmjs.org/sweepline-intersections/-/sweepline-intersections-1.5.0.tgz",
@ -12598,6 +13018,12 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true "dev": true
}, },
"node_modules/tiny-inflate": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
"license": "MIT"
},
"node_modules/tiny-invariant": { "node_modules/tiny-invariant": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@ -13024,6 +13450,16 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/unicode-properties": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
"integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.0",
"unicode-trie": "^2.0.0"
}
},
"node_modules/unicode-property-aliases-ecmascript": { "node_modules/unicode-property-aliases-ecmascript": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz",
@ -13034,6 +13470,22 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/unicode-trie": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
"license": "MIT",
"dependencies": {
"pako": "^0.2.5",
"tiny-inflate": "^1.0.0"
}
},
"node_modules/unicode-trie/node_modules/pako": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
"license": "MIT"
},
"node_modules/union-value": { "node_modules/union-value": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
@ -13131,6 +13583,12 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0" "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
} }
}, },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/validate-npm-package-license": { "node_modules/validate-npm-package-license": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
@ -13739,6 +14197,20 @@
} }
} }
}, },
"node_modules/vite-compatible-readable-stream": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/vite-compatible-readable-stream/-/vite-compatible-readable-stream-3.6.1.tgz",
"integrity": "sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/vite-node": { "node_modules/vite-node": {
"version": "3.0.7", "version": "3.0.7",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.7.tgz", "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.7.tgz",
@ -14478,6 +14950,12 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
},
"node_modules/yoga-layout": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz",
"integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==",
"license": "MIT"
} }
} }
} }

View file

@ -34,6 +34,7 @@
"@mui/material": "^6.1.6", "@mui/material": "^6.1.6",
"@mui/styles": "^6.1.6", "@mui/styles": "^6.1.6",
"@mui/x-date-pickers": "^7.26.0", "@mui/x-date-pickers": "^7.26.0",
"@react-pdf/renderer": "^4.3.0",
"@sentry/react": "^8.38.0", "@sentry/react": "^8.38.0",
"@turf/area": "^7.2.0", "@turf/area": "^7.2.0",
"@turf/turf": "^7.2.0", "@turf/turf": "^7.2.0",

View file

@ -0,0 +1,373 @@
import { colors } from "@mui/material";
import { jsonBin } from "common/DataImports/BinCables/BinCableImporter";
import { useEffect, useState } from "react";
import { distanceConversion } from "utils";
interface Props {
bin: jsonBin;
}
interface svgPoint {
x: number;
y: number;
}
export default function SideView(props: Props) {
const svgViewBoxSize = 250;
//percentage to of the svg to be the bin drawing from 0 to 1,
//NOTE: this is the percentage based on the longest side
//ie. if the bin is taller than it is wide the bin will be x percent of the height of the viewbox and the width will be scaled to the height and vice versa
const scaleMultiplier = 0.8;
const nodeSpacing = 4; //space in feet between the nodes
const dimensionFontSize = 7;
const gapSize = svgViewBoxSize * 0.05;
const { bin } = props;
const [scale, setScale] = useState(0); //the scale to multiply the bin dimension by in order to determine how long to draw the line
const [hopperHeight, setHopperHeight] = useState(0);
const [roofHeight, setRoofHeight] = useState(0);
useEffect(() => {
//get the peak height of the hopper
let h = (bin.Diameter / 2) * Math.tan(bin.HopperAngle * (Math.PI / 180));
setHopperHeight(h);
setRoofHeight(bin.Peak - bin.Sidewall);
let heightscale = svgViewBoxSize / (bin.Peak + h);
let widthscale = svgViewBoxSize / bin.Diameter;
//need to use the scale of the longer of the two dimensions
setScale((heightscale < widthscale ? heightscale : widthscale) * scaleMultiplier);
}, [bin, svgViewBoxSize]);
const binOutline = () => {
let startX = svgViewBoxSize / 2;
let startY = 0;
//let paddingVal = (padding * svgViewBoxSize)
return (
<path
strokeWidth={2}
stroke={colors.grey[400]}
fillOpacity={0}
d={
"M " +
startX +
" " +
startY + //starting point
" L " +
(startX + (bin.Diameter / 2) * scale) +
" " +
roofHeight * scale + //right side of the bin roof
" V " +
(roofHeight + bin.Sidewall) * scale + //right sidewall
(hopperHeight === 0
? " H " + (startX - (bin.Diameter / 2) * scale) //bin bottom if there is no hopper
: //else
" L " +
startX +
" " +
(roofHeight + hopperHeight + bin.Sidewall) * scale + //right side of the hopper
" L " +
(startX - (bin.Diameter / 2) * scale) +
" " +
(roofHeight + bin.Sidewall) * scale) + //left side of the hopper
" V " +
roofHeight * scale + //left sidewall
" Z " //left side of the bin roof (Z command draws the line from where you currently are back to the startign point)
}
/>
);
};
const cableLines = () => {
let cables: JSX.Element[] = [];
//let paddingVal = (padding * svgViewBoxSize)
// line for the roof at the top of the sidewall
// cables.push(
// <line
// key={"roof"}
// strokeWidth={2}
// stroke="yellow"
// x1={0}
// strokeDasharray={3}
// x2={svgViewBoxSize}
// y1={roofHeight*scale + paddingVal}
// y2={roofHeight*scale + paddingVal}
// />
// )
bin.CableSums.forEach(sum => {
let cableLength = 0;
bin.Cables.forEach(c => {
if (sum.Orbit === c.Orbit) {
//numNodes = c.Nodes
cableLength = c.Length;
}
});
let numNodes = Math.ceil(cableLength / nodeSpacing);
let xVal = svgViewBoxSize / 2 + sum.DistanceFromCenter * scale;
let cableStart = sum.DistanceFromCenter * Math.tan(bin.RoofAngle * (Math.PI / 180)) * scale;
//need to offset the padding to the top since we are now below the half mark as well as pad the bottom
let cableEnd = cableStart + cableLength * scale;
cables.push(
<line
key={"orbit" + sum.Orbit}
strokeWidth={2}
stroke="yellow"
x1={xVal}
y1={cableStart}
x2={xVal}
y2={cableEnd}
/>
);
let svgSpacing = (cableEnd - cableStart) / numNodes;
for (let i = 0; i < numNodes; i++) {
let nodeY = cableEnd - svgSpacing * i;
cables.push(
<circle key={"orbit" + sum.Orbit + "node" + i} cx={xVal} cy={nodeY} r={2} fill="blue" />
);
}
});
return cables;
};
const dimensionText = (key: string, x: number, y: number, text: string) => {
return (
<text
key={key}
style={{ fontSize: dimensionFontSize, fill: "white" }}
x={x}
y={y}
textAnchor="middle"
dominantBaseline={"middle"}>
{text}
</text>
);
};
const dimensionPath = (key: string, start: svgPoint, corner: svgPoint, end: svgPoint) => {
return (
//<line key={key} strokeWidth={1} stroke="white" x1={x1} x2={x2 ?? x1} y1={y1} y2={y2 ?? y1} />
<path
key={key}
strokeWidth={1}
stroke="white"
fillOpacity={0}
d={
"M " +
start.x +
" " +
start.y +
"L " +
corner.x +
" " +
corner.y +
"L " +
end.x +
" " +
end.y
}
/>
);
};
const bottomDiameter = () => {
let d: JSX.Element[] = [];
let x1 = svgViewBoxSize / 2 - (bin.Diameter / 2) * scale;
let x2 = svgViewBoxSize / 2 + (bin.Diameter / 2) * scale;
let y1 =
(roofHeight + bin.Sidewall + hopperHeight) * scale +
svgViewBoxSize * (1 - scaleMultiplier) * 0.1;
let y2 =
(roofHeight + bin.Sidewall + hopperHeight) * scale +
svgViewBoxSize * (1 - scaleMultiplier) * 0.25;
d.push(
dimensionText(
"bottomText",
svgViewBoxSize / 2,
y2,
distanceConversion(bin.Diameter).toFixed(1)
)
);
d.push(
dimensionPath(
"bottomDiameterLine1",
{ x: x1, y: y1 },
{ x: x1, y: y2 },
{ x: svgViewBoxSize / 2 - gapSize, y: y2 }
)
);
d.push(
dimensionPath(
"bottomDiameterLine2",
{ x: svgViewBoxSize / 2 + gapSize, y: y2 },
{ x: x2, y: y2 },
{ x: x2, y: y1 }
)
);
return d;
};
//height of the sidewall, on the left side - always used
const sidwallDimension = () => {
let d: JSX.Element[] = [];
let centerX = svgViewBoxSize / 2;
let x1 = centerX - (bin.Diameter / 2) * scale - svgViewBoxSize * (1 - scaleMultiplier) * 0.1;
let x2 = x1 - svgViewBoxSize * (1 - scaleMultiplier) * 0.15;
let midpoint = (roofHeight + bin.Sidewall / 2) * scale;
let y1 = roofHeight * scale;
let y2 = (roofHeight + bin.Sidewall) * scale;
d.push(
dimensionText("sidewallText", x2, midpoint, distanceConversion(bin.Sidewall).toFixed(1))
);
d.push(
dimensionPath(
"sidwallTopLine",
{ x: x1, y: y1 },
{ x: x2, y: y1 },
{ x: x2, y: midpoint - gapSize }
)
);
d.push(
dimensionPath(
"sidewallbottomLine",
{ x: x2, y: midpoint + gapSize },
{ x: x2, y: y2 },
{ x: x1, y: y2 }
)
);
return d;
};
//height of the bottom of the sidewall to the peak of the bin, on the left side - only used for hopper bins
const peakDimension = () => {
let d: JSX.Element[] = [];
let centerX = svgViewBoxSize / 2;
let xWall = centerX - (bin.Diameter / 2) * scale - svgViewBoxSize * (1 - scaleMultiplier) * 0.1;
let xPeak = svgViewBoxSize / 2 - svgViewBoxSize * (1 - scaleMultiplier) * 0.1;
let xText = xWall - svgViewBoxSize * (1 - scaleMultiplier) * 0.5;
let midpoint = ((roofHeight + bin.Sidewall) / 2) * scale;
let yTop = 0;
let yBottom = (roofHeight + bin.Sidewall) * scale;
d.push(
dimensionText(
"peakText",
xText,
midpoint,
distanceConversion(bin.Sidewall + roofHeight).toFixed(1)
)
);
d.push(
dimensionPath(
"peakTopLine",
{ x: xPeak, y: yTop },
{ x: xText, y: yTop },
{ x: xText, y: midpoint - gapSize }
)
);
d.push(
dimensionPath(
"peakbottomLine",
{ x: xText, y: midpoint + gapSize },
{ x: xText, y: yBottom },
{ x: xWall, y: yBottom }
)
);
return d;
};
//total height of the bin, on the right side - always used
const totalDimension = () => {
let d: JSX.Element[] = [];
let centerX = svgViewBoxSize / 2;
//let paddingVal = (padding* svgViewBoxSize)
let binRightX = centerX + (bin.Diameter / 2) * scale;
let xBinCenter = centerX + svgViewBoxSize * (1 - scaleMultiplier) * 0.1;
let xBinWall = binRightX + svgViewBoxSize * (1 - scaleMultiplier) * 0.1;
let x2 = binRightX + svgViewBoxSize * (1 - scaleMultiplier) * 0.2;
let midpoint = ((roofHeight + bin.Sidewall + hopperHeight) / 2) * scale;
let y1 = 0;
let y2 = (roofHeight + bin.Sidewall + hopperHeight) * scale;
d.push(
dimensionText(
"totalText",
x2,
midpoint,
distanceConversion(roofHeight + bin.Sidewall + hopperHeight).toFixed(1)
)
);
d.push(
dimensionPath(
"totalTopLine",
{ x: xBinCenter, y: y1 },
{ x: x2, y: y1 },
{ x: x2, y: midpoint - gapSize }
)
);
d.push(
dimensionPath(
"totalBottomLine",
{ x: x2, y: midpoint + gapSize },
{ x: x2, y: y2 },
{ x: hopperHeight > 0 ? xBinCenter : xBinWall, y: y2 }
)
);
return d;
};
const binDimensions = () => {
let dimensions: JSX.Element[] = [];
dimensions.push(...bottomDiameter());
// sidwall Height
dimensions.push(...sidwallDimension());
//total bin height
dimensions.push(...totalDimension());
if (hopperHeight > 0) {
//show the height of the peak + sidwall without the hopper
dimensions.push(...peakDimension());
}
return dimensions;
};
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox={
"0 " +
(svgViewBoxSize * (scaleMultiplier - 1)) / 2 +
" " +
svgViewBoxSize +
" " +
svgViewBoxSize
}
height="100%"
width="100%">
{cableLines()}
{binOutline()}
{binDimensions()}
</svg>
);
}

View file

@ -0,0 +1,73 @@
import { colors } from "@mui/material";
import { jsonBin } from "common/DataImports/BinCables/BinCableImporter";
interface Props {
bin: jsonBin;
}
export default function TopView(props: Props) {
const { bin } = props;
const totalRadius = 25;
const outerEdge = () => {
return (
<circle
cx="50%"
cy="50%"
r={totalRadius}
stroke={colors.grey[400]}
strokeWidth="1"
fillOpacity={0}
/>
);
};
const orbits = () => {
let o: JSX.Element[] = [];
bin.CableSums.forEach((sum, i) => {
//get the radius of the bin
let binRadius = bin.Diameter / 2;
//determine the percentage the orbit is from the center
let perc = sum.DistanceFromCenter / binRadius;
//use the percentage to find the radius for the svg
let svgRadius = perc * totalRadius;
o.push(
<circle
key={"orbit" + i}
cx="50%"
cy="50%"
r={svgRadius}
stroke={colors.grey[400]}
strokeWidth="1"
fillOpacity={0}
/>
);
//then determine the points the cables will be
let angleDist = 360 / sum.NumberOfCables;
let currentAngle = 90;
for (let k = 0; k < sum.NumberOfCables; k++) {
let x = svgRadius * Math.sin(currentAngle * (Math.PI / 180));
let y = svgRadius * Math.cos(currentAngle * (Math.PI / 180));
o.push(
<circle
key={"orbit" + i + "cable" + k}
r={1.5}
cx="50%"
cy="50%"
transform={"translate(" + x + " " + y + ")"}
fill={"blue"}
/>
);
currentAngle += angleDist;
}
});
return o;
};
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 55 55" height="100%" width="100%">
{outerEdge()}
{orbits()}
</svg>
);
}

View file

@ -0,0 +1,74 @@
import { Box, Grid2 as Grid, Typography } from "@mui/material";
import { jsonBin } from "common/DataImports/BinCables/BinCableImporter";
import { useMobile } from "hooks";
import { cloneDeep } from "lodash";
import SideView from "./binSVGs/sideView";
import TopView from "./binSVGs/topView";
import CableMounting from "./cableMounting";
import CableTable from "./cableTable";
interface Props {
bin: jsonBin;
updateBin: (newBin: jsonBin) => void;
}
export default function CableDisplay(props: Props) {
const { bin, updateBin } = props;
const isMobile = useMobile();
return (
<Box padding={2}>
<Typography style={{ fontSize: 20, fontWeight: 650 }}>
{bin.Manufacturer + " " + bin.Model}
</Typography>
<Grid container direction="row" alignContent="center" alignItems="center" spacing={3}>
<Grid size={isMobile ? 12 : 6}>
<Box width="100%" paddingLeft={isMobile ? 0 : "15%"} paddingRight={isMobile ? 0 : "15%"}>
<SideView bin={bin} />
</Box>
</Grid>
<Grid size={isMobile ? 12 : 6} container direction="column">
<Box width="100%">
<Grid
style={{ paddingLeft: isMobile ? 0 : "30%", paddingRight: isMobile ? 0 : "30%" }}>
<TopView bin={bin} />
</Grid>
<Grid>
<CableMounting bin={bin} />
</Grid>
</Box>
</Grid>
<Grid size={12}>
<Box width="100%">
<CableTable
bin={bin}
cablesChanged={newCables => {
//update the json bin with the newly edited cables
let newBin = cloneDeep(bin);
//if the number of cables has changed will need to adjust the cable sums accordingly
let newCounts: Map<number, number> = new Map();
//loop through the cables to find the numbers for each orbit
newCables.forEach(cable => {
let currentOrbit = newCounts.get(cable.Orbit);
if (currentOrbit) {
newCounts.set(cable.Orbit, (currentOrbit + cable.Count) as number);
} else {
newCounts.set(cable.Orbit, cable.Count);
}
});
//loop thorugh the sums setting the new counts for each orbit
newBin.CableSums.forEach(sum => {
let orbitCount = newCounts.get(sum.Orbit);
if (orbitCount && orbitCount !== sum.NumberOfCables) {
sum.NumberOfCables = orbitCount;
}
});
newBin.Cables = newCables;
updateBin(newBin);
}}
/>
</Box>
</Grid>
</Grid>
</Box>
);
}

View file

@ -0,0 +1,955 @@
import { useEffect, useState } from "react";
import {
Box,
Button,
Card,
FormControl,
FormControlLabel,
FormLabel,
Grid2 as Grid,
InputAdornment,
Radio,
RadioGroup,
Slider,
Switch,
TextField,
Typography
} from "@mui/material";
//import MaterialTable from "material-table";
//import { getTableIcons } from "common/ResponsiveTable";
import CableDisplay from "./cableDisplay";
import { distanceConversion, getDistanceUnit } from "utils";
import { pond } from "protobuf-ts/pond";
import CableQuote from "./cableQuote";
import { useMobile } from "hooks";
import { Cable, CableSum, jsonBin } from "common/DataImports/BinCables/BinCableImporter";
import BinSelector from "bin/BinSelector";
import { cloneDeep } from "lodash";
import {
ConeVolume,
CylinderVolume,
RadiansToDegrees,
TriangleOppositeLength
} from "common/TrigFunctions";
import ResponsiveTable, { Column } from "common/ResponsiveTable";
import { makeStyles } from "@mui/styles";
const useStyles = makeStyles(() => ({
sliderThumb: {
// height: 15,
// width: 15,
marginTop: 0,
marginLeft: 0,
//backgroundColor: "yellow"
},
// sliderValLabel: {
// left: "calc(-50%)",
// top: 22,
// "& *": {
// background: "transparent",
// color: "#fff"
// }
// }
}))
export default function CableEstimator() {
//the conversion constant to convert cubic feet to bushels
const conversionConstantFT = 0.7786;
const classes = useStyles();
const [binOptions, setBinOptions] = useState<jsonBin[]>([]);
const [displayedRows, setDisplayedRows] = useState<jsonBin[]>([]);
const [tablePage, setTablePage] = useState(0)
const [pageSize, setPageSize] = useState(10)
const [rowCount, setRowCount] = useState(0)
const [selectedBin, setSelectedBin] = useState<jsonBin | undefined>();
const [quoteOpen, setQuoteOpen] = useState(false);
const [useCustomBin, setUseCustomBin] = useState(false);
const [diameterFormEntry, setDiameterFormEntry] = useState(
getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "9.1" : "30"
);
//const [capacityFormEntry, setCapacityFormEntry] = useState("0");
const [ringsFormEntry, setRingsFormEntry] = useState("0");
const [ringHeightFormEntry, setRingHeightFormEntry] = useState("0");
const [ringHeightFt, setRingHeightFt] = useState(0);
const [sidewallFormEntry, setSidewallFormEntry] = useState(
getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "7" : "23"
);
const [peakFormEntry, setPeakFormEntry] = useState(
getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "9.1" : "30"
);
//const [eaveToPeakFormEntry, setEaveToPeakFormEntry] = useState("0");
const [roofAngleFormEntry, setRoofAngleFormEntry] = useState(
getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "7.6" : "25"
);
const [hopperAngleFormEntry, setHopperAngleFormEntry] = useState("0");
const nodeSpacing = 4;
const [customBin, setCustomBin] = useState<jsonBin>({
ID: 0,
Manufacturer: "",
Type: "Farm", //all bins are currently farm use
Bottom: "Flat Bottom",
Diameter: 30,
Name: "",
Model: "",
Capacity: 13942,
Rings: 0,
Sidewall: 23,
Peak: 30, //absolute total height from the ground to the peak of the cone
EaveToPeak: 0, //the length along the roof from the peak to the eave (not actually used anywhere at the moment)
RoofAngle: 25,
HopperAngle: 0,
CableSums: [],
Cables: []
});
const isMobile = useMobile();
const [tableSearch, setTableSearch] = useState("")
const [loading, setLoading] = useState(false)
const columns: Column<jsonBin>[] = [
{
title: "Manufacturer",
render: (row: jsonBin) => <Box padding={2}>{row.Manufacturer}</Box>
},
{
title: "Model",
render: (row: jsonBin) => <Box padding={2}>{row.Model}</Box>
},
{
title: "Capacity",
render: (row: jsonBin) => <Box padding={2}>{row.Capacity}</Box>
},
{
title: "Hopper Angle",
render: (row: jsonBin) => <Box padding={2}>{row.HopperAngle}</Box>
},
{
title: "Diameter " + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"),
render: (row: jsonBin) => <Box padding={2}>{distanceConversion(row.Diameter).toFixed(2)}</Box>
},
{
title: "Sidewall" + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"),
render: (row: jsonBin) => <Box padding={2}>{distanceConversion(row.Sidewall).toFixed(2)}</Box>
},
{
title: "Peak Height" + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"),
render: (row: jsonBin) => <Box padding={2}>{distanceConversion(row.Peak).toFixed(2)}</Box>
},
{
title: "Eave To Peak" + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"),
render: (row: jsonBin) => <Box padding={2}>{distanceConversion(row.EaveToPeak).toFixed(2)}</Box>
},
{
title: "Roof Angle",
render: (row: jsonBin) => <Box padding={2}>{row.RoofAngle}</Box>
},
{
title: "Rings",
render: (row: jsonBin) => <Box padding={2}>{row.Rings}</Box>
}
]
useEffect(()=>{
setLoading(true)
//create a clone to manipulate
let clone: jsonBin[] = cloneDeep(binOptions)
//having the search filter in here is quite inefficient as it will filter each time you change pages as well
//TODO: re-work the filter so it only filters when the search is changed
if(tableSearch !== ""){
//filter the data in the cloned list
clone = clone.filter(bin => {
if(tableSearch.toLowerCase() === bin.Manufacturer.toLowerCase()) return true
if(tableSearch.toLowerCase() === bin.Model.toLowerCase()) return true
if(tableSearch.toLowerCase() === bin.Capacity.toString().toLowerCase()) return true
if(tableSearch.toLowerCase() === bin.HopperAngle.toString().toLowerCase()) return true
if(tableSearch.toLowerCase() === bin.Diameter.toString().toLowerCase()) return true
if(tableSearch.toLowerCase() === bin.Sidewall.toString().toLowerCase()) return true
if(tableSearch.toLowerCase() === bin.Peak.toString().toLowerCase()) return true
if(tableSearch.toLowerCase() === bin.EaveToPeak.toString().toLowerCase()) return true
if(tableSearch.toLowerCase() === bin.RoofAngle.toString().toLowerCase()) return true
if(tableSearch.toLowerCase() === bin.Rings.toString().toLowerCase()) return true
return false
})
}
setRowCount(clone.length)
//split out the first x amount for the page
let rows = clone.splice(tablePage * pageSize, pageSize)
setDisplayedRows(rows)
setLoading(false)
},[binOptions, tablePage, pageSize, tableSearch])
const binOpTable = () => {
return (
<ResponsiveTable<jsonBin>
title={<Typography style={{ fontSize: 20, fontWeight: 650 }}>Bins</Typography>}
isLoading={loading}
setSearchText={(search) => {setTableSearch(search)}}
onRowClick={(bin) => {
setSelectedBin(bin);
}}
page={tablePage}
columns={columns}
pageSize={pageSize}
setPage={(page) => {setTablePage(page)}}
handleRowsPerPageChange={(e) => {setPageSize(e.target.value)}}
rows={displayedRows}
total={rowCount}
/>
);
};
const cableSummaries = () => {
//numbers in the summaries are in feet
let summaries: CableSum[] = [];
if (customBin.Diameter < 24) {
summaries.push({
Orbit: 0,
DistanceFromCenter: 2,
NumberOfCables: 1,
Bracket: 0
});
} else if (customBin.Diameter < 36) {
summaries.push({
Orbit: 1,
DistanceFromCenter: 7,
NumberOfCables: 3,
Bracket: 0
});
} else if (customBin.Diameter < 42) {
summaries.push({
Orbit: 0,
DistanceFromCenter: 2,
NumberOfCables: 1,
Bracket: 0
});
summaries.push({
Orbit: 1,
DistanceFromCenter: 9,
NumberOfCables: 3,
Bracket: 0
});
} else if (customBin.Diameter < 48) {
summaries.push({
Orbit: 0,
DistanceFromCenter: 2,
NumberOfCables: 1,
Bracket: 0
});
summaries.push({
Orbit: 1,
DistanceFromCenter: 14,
NumberOfCables: 4,
Bracket: 0
});
} else if (customBin.Diameter < 54) {
summaries.push({
Orbit: 0,
DistanceFromCenter: 2,
NumberOfCables: 1,
Bracket: 0
});
summaries.push({
Orbit: 1,
DistanceFromCenter: 16,
NumberOfCables: 5,
Bracket: 0
});
} else if (customBin.Diameter < 60) {
summaries.push({
Orbit: 0,
DistanceFromCenter: 2,
NumberOfCables: 1,
Bracket: 0
});
summaries.push({
Orbit: 1,
DistanceFromCenter: 18,
NumberOfCables: 6,
Bracket: 0
});
} else if (customBin.Diameter < 72) {
summaries.push({
Orbit: 0,
DistanceFromCenter: 2,
NumberOfCables: 1,
Bracket: 0
});
summaries.push({
Orbit: 1,
DistanceFromCenter: 20,
NumberOfCables: 7,
Bracket: 0
});
} else if (customBin.Diameter < 78) {
summaries.push({
Orbit: 1,
DistanceFromCenter: 10,
NumberOfCables: 4,
Bracket: 0
});
summaries.push({
Orbit: 2,
DistanceFromCenter: 27,
NumberOfCables: 9,
Bracket: 0
});
} else if (customBin.Diameter < 90) {
summaries.push({
Orbit: 1,
DistanceFromCenter: 10,
NumberOfCables: 4,
Bracket: 0
});
summaries.push({
Orbit: 2,
DistanceFromCenter: 29,
NumberOfCables: 10,
Bracket: 0
});
} else if (customBin.Diameter < 105) {
summaries.push({
Orbit: 0,
DistanceFromCenter: 2,
NumberOfCables: 1,
Bracket: 0
});
summaries.push({
Orbit: 1,
DistanceFromCenter: 18,
NumberOfCables: 6,
Bracket: 0
});
summaries.push({
Orbit: 2,
DistanceFromCenter: 36,
NumberOfCables: 12,
Bracket: 0
});
} else {
//diameter is 105 or larger
summaries.push({
Orbit: 1,
DistanceFromCenter: 9,
NumberOfCables: 3,
Bracket: 0
});
summaries.push({
Orbit: 2,
DistanceFromCenter: 26.5,
NumberOfCables: 9,
Bracket: 0
});
summaries.push({
Orbit: 3,
DistanceFromCenter: 44,
NumberOfCables: 14,
Bracket: 0
});
}
return summaries;
};
const buildCables = () => {
let bin = cloneDeep(customBin);
let binRadius = bin.Diameter / 2;
//use the diameter of the bin to build the cable summaries (orbit information)
let sums = cableSummaries();
//then use the orbits and the rest of the bins dimensions to build the cables
let hasCenter = false;
let roofRadians = bin.RoofAngle * (Math.PI / 180);
let hopperRadians = bin.HopperAngle * (Math.PI / 180);
let newCables: Cable[] = [];
sums.forEach((sum, i) => {
if (i === 0) {
hasCenter = true;
}
let hopperHeight = 0;
// calculate the hight of the cone section of the cable here
let topHeight = (binRadius - sum.DistanceFromCenter) * Math.tan(roofRadians);
//if the hopper angle is greater than 0 calculate the hopper height for the orbit
if (bin.HopperAngle > 0) {
hopperHeight = (binRadius - sum.DistanceFromCenter) * Math.tan(hopperRadians);
}
//get the maximum value for the top of the bin to the botton
let maxCablelength = Math.round((topHeight + bin.Sidewall + hopperHeight) * 10) / 10;
//shorten cable for the clearance from the bottom and round the cable down to the nearest even number
let lenX10 = (maxCablelength - 2) * 10;
let cableLen = (lenX10 - (lenX10 % 20)) / 10;
//set the bracket in the cableSum based on the length of the cable and whether the bin has a center cable
if (cableLen < 36) {
sum.Bracket = 3;
} else {
if (hasCenter) {
sum.Bracket = 2;
} else {
sum.Bracket = 1;
}
}
// divide the cable by the node spacing to get the number of nodes
let sensors = Math.ceil(cableLen / nodeSpacing);
//add in the cables for each orbit
//moisture cables
var moistureCables: Cable = {
Orbit: sum.Orbit,
Length: cableLen,
Nodes: sensors,
Type: 2,
Count: 1
};
newCables.push(moistureCables);
//temp Cables
if (sum.NumberOfCables > 1) {
let tempCables: Cable = {
Orbit: sum.Orbit,
Length: cableLen,
Nodes: sensors,
Type: 1,
Count: sum.NumberOfCables - 1
};
newCables.push(tempCables);
}
});
//add the summaries and cables to the bin
bin.CableSums = sums;
bin.Cables = newCables;
console.log(bin);
setCustomBin(bin);
};
const calcRoofAngle = (binRad: number, topConeHeight: number) => {
//calculate the angle using the the two given sides of a right triangle
let angle = RadiansToDegrees(Math.atan(topConeHeight / binRad)); //('arcTan(opposite / adjacent))' and convert to degrees
angle = Math.round(angle * 100) / 100; // round to 2 digits
return angle;
};
const calcConeHeight = (binRad: number, roofAngle: number) => {
return TriangleOppositeLength(binRad, roofAngle);
};
/**
* takes in the dimensions of the bin to calculate its bushel capacity
* @param diameter
* @param topConeHeight
* @param sidewallHeight
* @param hopperHeight
* @returns the bushel capacity of the bin with given dimensions
*/
const advancedGrainCalc = (bin: jsonBin) => {
let tcH = bin.Peak - bin.Sidewall;
let sH = bin.Sidewall;
let hH = TriangleOppositeLength(bin.Diameter / 2, bin.HopperAngle); //get the height of the hopper since it is not stored in the bin data
let d = bin.Diameter;
//calculate the volumes and convert them to bushels
let topConeBushels = ConeVolume(d / 2, tcH) * conversionConstantFT;
let sidewallBushels = CylinderVolume(d / 2, sH) * conversionConstantFT;
let hopperBushels = ConeVolume(d / 2, hH ?? 0) * conversionConstantFT;
return Math.round(topConeBushels + sidewallBushels + hopperBushels);
};
const customBinForm = () => {
return (
<Card style={{ padding: 20 }}>
<Grid container spacing={2}>
{/* first row */}
<Grid size={{xs:12, md:4}}>
<TextField
variant="outlined"
fullWidth
label="Bin Manufacturer"
value={customBin.Manufacturer}
onChange={e => {
let bin = cloneDeep(customBin);
bin.Manufacturer = e.target.value;
setCustomBin(bin);
}}
/>
</Grid>
<Grid size={{xs:12, md:4}}>
<TextField
variant="outlined"
fullWidth
label="Bin Name"
value={customBin.Name}
onChange={e => {
let bin = cloneDeep(customBin);
bin.Name = e.target.value;
setCustomBin(bin);
}}
/>
</Grid>
<Grid size={{xs:12, md:4}}>
<TextField
variant="outlined"
fullWidth
label="Bin Model"
value={customBin.Model}
onChange={e => {
let bin = cloneDeep(customBin);
bin.Model = e.target.value;
setCustomBin(bin);
}}
/>
</Grid>
{/* second row */}
<Grid size={{xs:12, md:4}}>
<TextField
type="number"
fullWidth
variant="outlined"
label="Diameter"
helperText={"The full diameter of the bin"}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "m" : "ft"}
</InputAdornment>
)
}}
value={diameterFormEntry}
error={isNaN(parseFloat(diameterFormEntry))}
onChange={e => {
setDiameterFormEntry(e.target.value);
let val = isNaN(parseFloat(e.target.value)) ? 0 : parseFloat(e.target.value);
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
val = val * 3.281;
}
//when the diameter is changed use the current peak height to adjust the roof angle
let bin = cloneDeep(customBin);
bin.Diameter = val;
let newAngle = calcRoofAngle(bin.Diameter / 2, bin.Peak - bin.Sidewall);
setRoofAngleFormEntry(newAngle.toString());
bin.RoofAngle = newAngle;
bin.Capacity = advancedGrainCalc(bin);
setCustomBin(bin);
}}
/>
</Grid>
<Grid size={{xs:12, md:6}}>
<TextField
type="number"
fullWidth
variant="outlined"
label="Peak Height"
InputProps={{
endAdornment: (
<InputAdornment position="end">
{getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "m" : "ft"}
</InputAdornment>
)
}}
helperText={
"total height from the bottom of the sidewall to the top of the roof, calculated from the diameter, roof angle, and sidewall height of the bin"
}
value={peakFormEntry}
error={isNaN(parseFloat(peakFormEntry))}
onChange={e => {
setPeakFormEntry(e.target.value);
let val = isNaN(parseFloat(e.target.value)) ? 0 : parseFloat(e.target.value);
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
val = val * 3.281;
}
//when the peak height is changed use the diameter to calculate the roof angle
let bin = cloneDeep(customBin);
bin.Peak = val;
let newAngle = calcRoofAngle(bin.Diameter / 2, bin.Peak - bin.Sidewall);
setRoofAngleFormEntry(newAngle.toString());
bin.RoofAngle = newAngle;
bin.Capacity = advancedGrainCalc(bin);
setCustomBin(bin);
}}
/>
</Grid>
{/* third row */}
<Grid size={{xs:12, md:4}}>
<TextField
type="number"
fullWidth
variant="outlined"
label="Rings"
helperText={"Number of rings along the sidewall"}
value={ringsFormEntry}
error={isNaN(parseFloat(ringsFormEntry))}
onChange={e => {
setRingsFormEntry(e.target.value);
let bin = cloneDeep(customBin);
let val = isNaN(parseFloat(e.target.value)) ? 0 : parseFloat(e.target.value);
bin.Rings = val;
//use the new number of rings and ring height to get the sidewall height
let sidewallFt = val * ringHeightFt;
bin.Sidewall = sidewallFt;
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
setSidewallFormEntry((sidewallFt / 3.281).toString());
} else {
setSidewallFormEntry(sidewallFt.toString());
}
//with the sidewall height changing the roof ange should also change
let newAngle = calcRoofAngle(bin.Diameter / 2, bin.Peak - bin.Sidewall);
setRoofAngleFormEntry(newAngle.toString());
bin.RoofAngle = newAngle;
bin.Capacity = advancedGrainCalc(bin);
setCustomBin(bin);
}}
/>
</Grid>
<Grid size={{xs:12, md:4}}>
<TextField
type="number"
fullWidth
variant="outlined"
label="Height of Ring"
helperText={"The Height of a single ring/band of the bin"}
InputProps={{
endAdornment: <InputAdornment position="end">{"in"}</InputAdornment>
}}
value={ringHeightFormEntry}
error={isNaN(parseFloat(ringHeightFormEntry))}
onChange={e => {
setRingHeightFormEntry(e.target.value);
let inVal = isNaN(parseFloat(e.target.value)) ? 0 : parseFloat(e.target.value);
// if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
// val = val * 3.281;
// }
let ringFt = inVal / 12;
setRingHeightFt(ringFt);
//based on this entry and the number of rings calculate the sidwall
let bin = cloneDeep(customBin);
let sidewallFt = bin.Rings * ringFt;
bin.Sidewall = sidewallFt;
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
setSidewallFormEntry((sidewallFt / 3.281).toString());
} else {
setSidewallFormEntry(sidewallFt.toString());
}
//with the sidewall height changing the roof angle should also change with it
let newAngle = calcRoofAngle(bin.Diameter / 2, bin.Peak - bin.Sidewall);
setRoofAngleFormEntry(newAngle.toString());
bin.RoofAngle = newAngle;
bin.Capacity = advancedGrainCalc(bin);
setCustomBin(bin);
}}
/>
</Grid>
<Grid size={{xs:12, md:4}}>
<TextField
type="number"
fullWidth
variant="outlined"
label="Bin Sidewall Height"
helperText={"Full height of all of the rings together"}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "m" : "ft"}
</InputAdornment>
)
}}
value={sidewallFormEntry}
error={isNaN(parseFloat(sidewallFormEntry))}
onChange={e => {
//need to handle conversions here
setSidewallFormEntry(e.target.value);
let val = isNaN(parseFloat(e.target.value)) ? 0 : parseFloat(e.target.value);
//if the value entered is meters convert it to feet for the bin
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
val = val * 3.281;
}
let bin = cloneDeep(customBin);
bin.Sidewall = val;
let newAngle = calcRoofAngle(bin.Diameter / 2, bin.Peak - bin.Sidewall);
setRoofAngleFormEntry(newAngle.toString());
bin.RoofAngle = newAngle;
bin.Capacity = advancedGrainCalc(bin);
setCustomBin(bin);
}}
/>
</Grid>
{/* fourth row */}
<Grid size={{xs:12, md:6}}>
<Box display="flex" justifyContent="center" alignItems="center">
<Slider
classes={{
thumb: classes.sliderThumb,
//valueLabel: classes.sliderValLabel
}}
valueLabelDisplay="off"
min={20}
max={40}
name="roofAngle"
aria-label="roofAngle"
step={0.1}
color="secondary"
//track={false}
value={customBin.RoofAngle}
onChange={(_, newValue) => {
let val = newValue as number;
setRoofAngleFormEntry(val.toString());
let bin = cloneDeep(customBin);
bin.RoofAngle = val;
//adjusting the roof angle should also calculate the new top cone and adjust the peak height of the bin
let coneHeight = calcConeHeight(bin.Diameter / 2, bin.RoofAngle);
bin.Peak = bin.Sidewall + coneHeight; //assign it as feet
let peakHeightDisplay = bin.Sidewall + coneHeight;
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
peakHeightDisplay = peakHeightDisplay / 3.281;
}
setPeakFormEntry(peakHeightDisplay.toFixed(1));
bin.Capacity = advancedGrainCalc(bin);
setCustomBin(bin);
}}
/>
<TextField
style={{ marginLeft: 10 }}
type="number"
variant="outlined"
label="Roof Angle"
helperText={"Calculated from diameter, sidewall and peak height. Usually 25-35"}
InputProps={{
endAdornment: <InputAdornment position="end">{"deg"}</InputAdornment>
}}
value={roofAngleFormEntry}
error={isNaN(parseFloat(roofAngleFormEntry))}
onChange={e => {
setRoofAngleFormEntry(e.target.value);
let bin = cloneDeep(customBin);
bin.RoofAngle = isNaN(parseFloat(e.target.value))
? 0
: parseFloat(e.target.value);
//adjusting the roof angle should also calculate the new top cone and adjust the peak height of the bin
let coneHeight = calcConeHeight(bin.Diameter / 2, bin.RoofAngle);
bin.Peak = bin.Sidewall + coneHeight;
let peakHeightDisplay = bin.Sidewall + coneHeight;
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
peakHeightDisplay = peakHeightDisplay / 3.281;
}
setPeakFormEntry(peakHeightDisplay.toFixed(1));
bin.Capacity = advancedGrainCalc(bin);
setCustomBin(bin);
}}
/>
</Box>
</Grid>
<Grid size={{xs:12, md:6}}>
<Box display="flex" justifyContent="center" alignItems="center">
<Slider
classes={{
thumb: classes.sliderThumb,
//valueLabel: classes.sliderValLabel,
}}
valueLabelDisplay="off"
min={20}
max={40}
name="hopperAngle"
aria-label="hopperAngle"
step={0.1}
color="secondary"
disabled={customBin.Bottom === "Flat Bottom"}
//track={false}
value={customBin.HopperAngle}
onChange={(_, newValue) => {
setHopperAngleFormEntry(newValue.toString());
let bin = cloneDeep(customBin);
bin.HopperAngle = newValue as number;
bin.Capacity = advancedGrainCalc(bin);
setCustomBin(bin);
}}
/>
<TextField
type="number"
style={{ marginLeft: 10 }}
fullWidth
variant="outlined"
disabled={customBin.Bottom === "Flat Bottom"}
label="Hopper Angle"
helperText={"Calculated from the hopper height and diameter"}
InputProps={{
endAdornment: <InputAdornment position="end">{"deg"}</InputAdornment>
}}
value={hopperAngleFormEntry}
error={isNaN(parseFloat(hopperAngleFormEntry))}
onChange={e => {
setHopperAngleFormEntry(e.target.value);
let bin = cloneDeep(customBin);
bin.HopperAngle = isNaN(parseFloat(e.target.value))
? 0
: parseFloat(e.target.value);
bin.Capacity = advancedGrainCalc(bin);
setCustomBin(bin);
}}
/>
</Box>
</Grid>
{/* bottom */}
<Grid>
<FormControl
component="fieldset"
variant="outlined">
<FormLabel component="legend">Shape</FormLabel>
<RadioGroup
aria-label="bin shape"
name="binShape"
value={customBin.Bottom}
onChange={(_, value) => {
let bin = cloneDeep(customBin);
bin.Bottom = value;
if (value === "Hopper Bottom") {
bin.HopperAngle = 25;
setHopperAngleFormEntry("25");
} else {
bin.HopperAngle = 0;
setHopperAngleFormEntry("0");
}
bin.Capacity = advancedGrainCalc(bin);
setCustomBin(bin);
}}>
<FormControlLabel
key={"flat"}
value={"Flat Bottom"}
control={<Radio />}
label={"Flat Bottom"}
/>
<FormControlLabel
key={"hopper"}
value={"Hopper Bottom"}
control={<Radio />}
label={"Hopper"}
/>
</RadioGroup>
</FormControl>
</Grid>
</Grid>
{/* not strictly necessary for quoting */}
{/* <TextField
type="number"
variant="outlined"
label="Bin Capacity"
value={capacityFormEntry}
error={isNaN(parseFloat(capacityFormEntry))}
onChange={e => {
setCapacityFormEntry(e.target.value);
let bin = cloneDeep(customBin);
bin.Capacity = isNaN(parseFloat(e.target.value)) ? 0 : parseFloat(e.target.value);
setCustomBin(bin);
}}
/> */}
{/* this dimension isn't actually used for anything */}
{/* <TextField
type="number"
variant="outlined"
label="Eave to Peak"
value={eaveToPeakFormEntry}
error={isNaN(parseFloat(eaveToPeakFormEntry))}
onChange={e => {
//need to handle conversions here
setEaveToPeakFormEntry(e.target.value);
let val = isNaN(parseFloat(e.target.value)) ? 0 : parseFloat(e.target.value);
//if the value entered is meters convert it to feet for the bin
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
val = val * 3.281;
}
let bin = cloneDeep(customBin);
bin.EaveToPeak = val;
setCustomBin(bin);
}}
/> */}
<Button variant="contained" color="primary" onClick={buildCables}>
Generate Cables
</Button>
<Typography style={{ fontWeight: 650 }}>
Estimated Capacity: {customBin.Capacity} bushels
</Typography>
</Card>
);
};
const binDisplay = () => {
let displayBin = selectedBin;
if (useCustomBin) {
displayBin = customBin;
}
if (displayBin) {
return (
<CableDisplay
bin={displayBin}
updateBin={newBin => {
setSelectedBin(newBin);
}}
/>
);
}
};
const quoteDisplay = () => {
let displayBin = selectedBin;
if (useCustomBin) {
displayBin = customBin;
}
if (displayBin) {
return (
<CableQuote
bin={displayBin}
open={quoteOpen}
close={() => {
setQuoteOpen(false);
}}
/>
);
}
};
return (
<Box padding={2}>
<FormControlLabel
label="Custom Bin"
control={
<Switch
color="default"
value={useCustomBin}
checked={useCustomBin}
onChange={(_, checked) => {
setUseCustomBin(checked);
}}
name="storage"
/>
}
/>
{/* grid of select boxes for pre-built options */}
<Grid container direction="row" spacing={2} alignContent="center" alignItems="center">
{!useCustomBin && (
<Grid size={isMobile ? 12 : 10}>
<BinSelector
optionsChanged={ops => {
setBinOptions(ops);
}}
/>
</Grid>
)}
<Grid size={isMobile ? 12 : 2}>
<Button
variant="contained"
color="primary"
onClick={() => {
setQuoteOpen(true);
}}>
Generate Quote
</Button>
</Grid>
</Grid>
{useCustomBin ? customBinForm() : binOpTable()}
{/* summary of cable data */}
{binDisplay()}
{quoteDisplay()}
</Box>
);
}

View file

@ -0,0 +1,71 @@
import { Box, Typography } from "@mui/material";
import { jsonBin, CableSum, getBracket } from "common/DataImports/BinCables/BinCableImporter";
import ResponsiveTable, { Column } from "common/ResponsiveTable";
//import { getTableIcons } from "common/ResponsiveTable";
//import MaterialTable from "material-table";
import { pond } from "protobuf-ts/pond";
import { distanceConversion, getDistanceUnit } from "utils";
interface Props {
bin: jsonBin;
}
export default function CableMounting(props: Props) {
const { bin } = props;
// const [tablePage, setTablePage] = useState(0)
// const [pageSize, setPageSize] = useState(10)
const roofDist = (distance: number) => {
return distance / Math.cos(bin.RoofAngle * (Math.PI / 180));
};
const columns: Column<CableSum>[] = [
{
title: "Orbit",
render: (row: CableSum) => <Box padding={2}>{row.Orbit === 0 ? "Center" : row.Orbit}</Box>
},
{
title: "From Center " + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"),
render: (row: CableSum) => (
<Box padding={2}>{distanceConversion(row.DistanceFromCenter).toFixed(2)}</Box>
)
},
{
title: "Along Roof" + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"),
render: (row: CableSum) => (
<Box padding={2}>{distanceConversion(roofDist(row.DistanceFromCenter)).toFixed(2)}</Box>
)
},
{
title: "Bracket",
render: (row: CableSum) => {
let bracketType = "None"
let bracket = getBracket(row.Bracket)
if(row.Orbit !== 0 && bracket !== undefined) {
bracketType = bracket.Name + " (" + row.NumberOfCables + ")"
}else {
bracketType = "Unknown"
}
return (
<Box padding={2}>
{bracketType}
</Box>
)
}
}
]
return (
<ResponsiveTable<CableSum>
title={<Typography style={{ fontSize: 20, fontWeight: 650 }}>Install Guide</Typography>}
rows={bin.CableSums}
columns={columns}
page={0}
pageSize={0}
handleRowsPerPageChange={()=>{}}
setPage={()=>{}}
total={0}
hidePagination
/>
)
}

View file

@ -0,0 +1,52 @@
import { Button, DialogActions, DialogContent } from "@mui/material";
import { PDFViewer, StyleSheet } from "@react-pdf/renderer";
import AgPDFTemplate from "common/pdfTemplates/Agriculture/agPDFTemplate";
import ResponsiveDialog from "common/ResponsiveDialog";
import { useMobile } from "hooks";
import { jsonBin } from "common/DataImports/BinCables/BinCableImporter";
import PdfContent from "./pdfComponents/pdfContent";
interface Props {
bin: jsonBin;
open: boolean;
close: () => void;
}
export default function CableQuote(props: Props) {
const { open, close, bin } = props;
const isMobil = useMobile();
const pdfStyle = StyleSheet.create({
viewerDesktop: {
height: 500 * 1.44,
width: 500
},
viewerMobile: {
height: "100%",
width: "100%"
}
});
const pdfDoc = () => {
return <AgPDFTemplate content={<PdfContent bin={bin} />} />;
};
return (
<ResponsiveDialog open={open} onClose={close}>
<DialogContent>
<PDFViewer style={isMobil ? pdfStyle.viewerMobile : pdfStyle.viewerDesktop}>
{pdfDoc()}
</PDFViewer>
</DialogContent>
<DialogActions>
<Button
variant="contained"
color="primary"
onClick={() => {
close();
}}>
Close
</Button>
</DialogActions>
</ResponsiveDialog>
);
}

View file

@ -0,0 +1,142 @@
import { Box, Typography } from "@mui/material";
//import { getTableIcons } from "common/ResponsiveTable";
//import MaterialTable from "material-table";
import { pond } from "protobuf-ts/pond";
import { useEffect, useState } from "react";
import { distanceConversion, getDistanceUnit } from "utils";
import { Cable, jsonBin } from "common/DataImports/BinCables/BinCableImporter";
import { cloneDeep } from "lodash";
import ResponsiveTable, { Column } from "common/ResponsiveTable";
interface Props {
bin: jsonBin;
cablesChanged: (newCables: Cable[]) => void;
}
export default function CableTable(props: Props) {
const { bin, /*cablesChanged*/ } = props;
//const nodeSpacing = 4; //space between the nodes in feet
const [cableData, setCableData] = useState(bin.Cables);
useEffect(() => {
let convertedCables: Cable[] = cloneDeep(bin.Cables);
convertedCables.forEach(cable => {
cable.Length = Math.round(distanceConversion(cable.Length) * 100) / 100;
});
setCableData(convertedCables);
}, [bin]);
// const table = () => {
// return (
// <MaterialTable
// data={cableData}
// title={
// <Typography style={{ fontSize: 20, fontWeight: 650 }}>Recommended Cables</Typography>
// }
// icons={getTableIcons()}
// editable={{
// onRowUpdate: (newCable, oldCable) =>
// new Promise((resolve, reject) => {
// const dataUpdate = [...cableData];
// if (oldCable) {
// //make sure the cables lengths are in feet
// if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
// newCable.Length = newCable.Length * 3.281;
// oldCable.Length = oldCable.Length * 3.281;
// }
// //if the length was eddited
// if (oldCable.Length !== newCable.Length) {
// //recalc the number of nodes
// newCable.Nodes = Math.ceil(newCable.Length / nodeSpacing);
// }
// const index = dataUpdate.indexOf(oldCable);
// //this line seems ridiculous, however the browser seems to see newCable.Count as a string if it was the one that was edited in the table
// newCable.Count = parseInt(newCable.Count.toString());
// dataUpdate[index] = newCable;
// }
// resolve(cablesChanged(dataUpdate));
// })
// }}
// columns={[
// {
// title: <Typography style={{ fontSize: 15, fontWeight: 650 }}>Orbit</Typography>,
// render: (row: Cable) => <div>{row.Orbit === 0 ? "Center" : row.Orbit.toString()}</div>
// },
// {
// title: <Typography style={{ fontSize: 15, fontWeight: 650 }}>Type</Typography>,
// render: (row: Cable) => <div>{row.Type === 2 ? "Moisture" : "Temperature"}</div>
// },
// {
// title: (
// <Typography style={{ fontSize: 15, fontWeight: 650 }}>Number of Cables</Typography>
// ),
// field: "Count",
// initialEditValue: 0,
// type: "numeric"
// },
// {
// title: (
// <Typography style={{ fontSize: 15, fontWeight: 650 }}>
// Length{getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"}
// </Typography>
// ),
// field: "Length",
// initialEditValue: 0,
// type: "numeric"
// },
// {
// title: (
// <Typography style={{ fontSize: 15, fontWeight: 650 }}>Nodes Per Cable</Typography>
// ),
// editable: "never",
// type: "numeric",
// field: "Nodes"
// }
// ]}
// options={{
// search: false,
// paging: false
// }}
// />
// );
// };
const columns: Column<Cable>[] = [
{
title: "Orbit",
render: (row: Cable) => <Box padding={2}>{row.Orbit === 0 ? "Center" : row.Orbit.toString()}</Box>
},
{
title: "Type",
render: (row: Cable) => <Box padding={2}>{row.Type === 2 ? "Moisture" : "Temperature"}</Box>
},
{
title: "Number of Cables",
render: (row) => <Box padding={2}>{row.Count}</Box>
},
{
title: "Length" + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"),
render: (row) => <Box padding={2}>{row.Length}</Box>
},
{
title: "Nodes Per Cable",
render: (row) => <Box padding={2}>{row.Nodes}</Box>
}
]
//TODO: in order to add back in the editable functionality will need to add that to the responsive table
return (
<ResponsiveTable
title={<Typography style={{ fontSize: 20, fontWeight: 650 }}>Recommended Cables</Typography>}
columns={columns}
rows={cableData}
page={0}
pageSize={0}
total={0}
setPage={()=>{}}
handleRowsPerPageChange={()=>{}}
hidePagination
/>
);
}

View file

@ -0,0 +1,43 @@
import { StyleSheet, Text, View } from "@react-pdf/renderer";
import { pond } from "protobuf-ts/pond";
import { getDistanceUnit } from "utils";
interface Props {
borderColour: string;
}
export default function CableTableHeader(props: Props) {
const borderColor = props.borderColour;
const styles = StyleSheet.create({
view: {
flexDirection: "row",
borderBottomColor: borderColor,
borderBottomWidth: 1,
alignItems: "center",
height: 24,
textAlign: "center",
fontFamily: "Helvetica-Bold",
flexGrow: 1
},
cell: {
width: "20%",
borderRightColor: borderColor,
borderRightWidth: 1
},
endCell: {
width: "20%"
}
});
return (
<View style={styles.view}>
<Text style={styles.cell}>Orbit</Text>
<Text style={styles.cell}>Type</Text>
<Text style={styles.cell}>Qty</Text>
<Text style={styles.cell}>
Length {getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"}
</Text>
<Text style={styles.endCell}>Nodes</Text>
</View>
);
}

View file

@ -0,0 +1,60 @@
import { StyleSheet, Text, View } from "@react-pdf/renderer";
import { jsonBin } from "common/DataImports/BinCables/BinCableImporter";
import React from "react";
import { distanceConversion } from "utils";
interface Props {
bin: jsonBin;
borderColour: string;
}
export default function CableTableRow(props: Props) {
const styles = StyleSheet.create({
view: {
flexDirection: "row",
borderBottomColor: props.borderColour,
borderBottomWidth: 1,
alignItems: "center",
height: 24,
fontStyle: "normal",
fontWeight: 650
},
cell: {
width: "20%",
textAlign: "left",
borderRightColor: props.borderColour,
borderRightWidth: 1,
paddingLeft: 8
},
endCell: {
width: "20%",
paddingLeft: 8
}
});
const rows = () => {
let r: JSX.Element[] = [];
props.bin.Cables.forEach((cable, i) => {
r.push(
<View
style={{
flexDirection: "row",
borderBottomColor: props.borderColour,
borderBottomWidth: i === props.bin.Cables.length - 1 ? 0 : 1,
alignItems: "center",
height: 24
}}
key={i}>
<Text style={styles.cell}>{cable.Orbit === 0 ? "Center" : cable.Orbit}</Text>
<Text style={styles.cell}>{cable.Type === 2 ? "Moisture" : "Temperature"}</Text>
<Text style={styles.cell}>{cable.Count}</Text>
<Text style={styles.cell}>{distanceConversion(cable.Length).toFixed(2)}</Text>
<Text style={styles.endCell}>{cable.Nodes}</Text>
</View>
);
});
return r;
};
return <React.Fragment>{rows()}</React.Fragment>;
}

View file

@ -0,0 +1,54 @@
import { StyleSheet, Text, View } from "@react-pdf/renderer";
import { pond } from "protobuf-ts/pond";
import { getDistanceUnit } from "utils";
interface Props {
borderColour: string;
}
export default function MountingTableHeader(props: Props) {
const borderColor = props.borderColour;
const styles = StyleSheet.create({
view: {
flexDirection: "row",
borderBottomColor: borderColor,
borderBottomWidth: 1,
alignItems: "center",
height: 24,
textAlign: "center",
fontFamily: "Helvetica-Bold",
flexGrow: 1
},
orbit: {
width: "14%",
borderRightColor: borderColor,
borderRightWidth: 1
},
centerDist: {
width: "25%",
borderRightColor: borderColor,
borderRightWidth: 1
},
roofDist: {
width: "33%",
borderRightColor: borderColor,
borderRightWidth: 1
},
bracket: {
width: "28%"
}
});
return (
<View style={styles.view}>
<Text style={styles.orbit}>Orbit</Text>
<Text style={styles.centerDist}>
Radius {getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"}
</Text>
<Text style={styles.roofDist}>
Along Roof {getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"}
</Text>
<Text style={styles.bracket}>Bracket</Text>
</View>
);
}

View file

@ -0,0 +1,83 @@
import { StyleSheet, Text, View } from "@react-pdf/renderer";
import { getBracket, jsonBin } from "common/DataImports/BinCables/BinCableImporter";
import React from "react";
import { distanceConversion } from "utils";
interface Props {
bin: jsonBin;
borderColour: string;
}
export default function MountingTableRow(props: Props) {
const styles = StyleSheet.create({
orbit: {
width: "14%",
textAlign: "left",
borderRightColor: props.borderColour,
borderRightWidth: 1,
paddingLeft: 8
},
centerDist: {
width: "25%",
textAlign: "left",
borderRightColor: props.borderColour,
borderRightWidth: 1,
paddingLeft: 8
},
roofDist: {
width: "33%",
textAlign: "left",
paddingLeft: 8,
borderRightColor: props.borderColour,
borderRightWidth: 1
},
bracket: {
width: "28%",
textAlign: "center",
fontSize: 8
}
});
const roofDist = (distance: number) => {
return distance / Math.cos(props.bin.RoofAngle * (Math.PI / 180));
};
const rows = () => {
let r: JSX.Element[] = [];
props.bin.CableSums.forEach((orbit, i) => {
let bracketType = "None"
let bracket = getBracket(orbit.Bracket)
if(orbit.Orbit !== 0 && bracket !== undefined) {
bracketType = bracket.Name + " (" + orbit.NumberOfCables + ")"
}else {
bracketType = "Unk"
}
r.push(
<View
style={{
flexDirection: "row",
borderBottomColor: props.borderColour,
borderBottomWidth: i === props.bin.CableSums.length - 1 ? 0 : 1,
alignItems: "center",
height: 24
}}
key={i}>
<Text style={styles.orbit}>{orbit.Orbit === 0 ? "C" : orbit.Orbit}</Text>
<Text style={styles.centerDist}>
{distanceConversion(orbit.DistanceFromCenter).toFixed(2)}
</Text>
<Text style={styles.roofDist}>
{distanceConversion(roofDist(orbit.DistanceFromCenter)).toFixed(2)}
</Text>
<Text style={styles.bracket}>
{bracketType}
</Text>
</View>
);
});
return r;
};
return <React.Fragment>{rows()}</React.Fragment>;
}

View file

@ -0,0 +1,81 @@
import { StyleSheet, Text, View } from "@react-pdf/renderer";
import { colors } from "@mui/material";
import { jsonBin } from "common/DataImports/BinCables/BinCableImporter";
import CableTableHeader from "./cableTable/cableTableHeader";
import CableTableRow from "./cableTable/cableTableRow";
import PDFSideView from "./pdfSVG/pdfSideView";
import PDFTopView from "./pdfSVG/pdfTopView";
import MountingTableHeader from "./mountingTable/mountingTableHeader";
import MountingTableRow from "./mountingTable/mountingTableRows";
interface Props {
bin: jsonBin;
}
export default function PdfContent(props: Props) {
const tableBorderColour = colors.grey[800];
const styles = StyleSheet.create({
general: {
marginTop: 2,
flexDirection: "row",
borderTop: "1px solid black",
borderBottom: "1px solid black",
paddingTop: 12,
paddingBottom: 12,
fontFamily: "Helvetica-Bold",
fontSize: 20,
textAlign: "center"
},
svgLabels: {
marginTop: 2,
flexDirection: "row",
borderTop: "1px solid black",
paddingTop: 12,
textAlign: "left",
fontFamily: "Helvetica-Bold"
},
cableTable: {
borderColor: tableBorderColour,
borderWidth: 1
}
});
return (
<View>
<View style={styles.general}>
<Text style={{ width: "34%" }}>{props.bin.Manufacturer}</Text>
<Text style={{ width: "33%" }}>{props.bin.Model}</Text>
<Text style={{ width: "33%" }}>{props.bin.Bottom}</Text>
</View>
<View style={styles.svgLabels}>
<Text style={{ width: "50%" }}>Side View</Text>
<Text style={{ width: "50%" }}>Top View</Text>
</View>
<View style={{ height: 280, flexDirection: "row" }}>
<View style={{ width: 260 }}>
<PDFSideView bin={props.bin} />
</View>
<View style={{ width: 260 }}>
<View style={{ height: 195 }}>
<PDFTopView bin={props.bin} />
</View>
<View style={{ minHeight: 65 }}>
<Text style={{ fontFamily: "Helvetica-Bold", marginBottom: 5 }}>Install Guide</Text>
<View
style={{ borderColor: tableBorderColour, borderWidth: 1, flexDirection: "column" }}>
<MountingTableHeader borderColour={tableBorderColour} />
<MountingTableRow borderColour={tableBorderColour} bin={props.bin} />
</View>
</View>
</View>
</View>
<Text style={{ fontFamily: "Helvetica-Bold", marginTop: 12, marginBottom: 5 }}>
Recommended Cables
</Text>
<View style={styles.cableTable}>
<CableTableHeader borderColour={tableBorderColour} />
<CableTableRow bin={props.bin} borderColour={tableBorderColour} />
</View>
</View>
);
}

View file

@ -0,0 +1,387 @@
import { colors } from "@mui/material";
import { Circle, Line, Path, Svg, Text } from "@react-pdf/renderer";
import { jsonBin } from "common/DataImports/BinCables/BinCableImporter";
import { useEffect, useState } from "react";
import { distanceConversion } from "utils";
interface Props {
bin: jsonBin;
}
interface svgPoint {
x: number;
y: number;
}
export default function PDFSideView(props: Props) {
const svgViewBoxSize = 500;
//percentage to of the svg to be the bin drawing from 0 to 1,
//NOTE: this is the percentage based on the longest side
//ie. if the bin is taller than it is wide the bin will be x percent of the height of the viewbox and the width will be scaled to the height and vice versa
const scaleMultiplier = 0.8;
const nodeSpacing = 4; //space in feet between the nodes
const dimensionFontSize = 18;
const gapSize = svgViewBoxSize * 0.05;
const { bin } = props;
const [scale, setScale] = useState(0); //the scale to multiply the bin dimension by in order to determine how long to draw the line
const [hopperHeight, setHopperHeight] = useState(0);
const [roofHeight, setRoofHeight] = useState(0);
useEffect(() => {
//get the peak height of the hopper
let h = (bin.Diameter / 2) * Math.tan(bin.HopperAngle * (Math.PI / 180));
setHopperHeight(h);
setRoofHeight(bin.Peak - bin.Sidewall);
let heightscale = svgViewBoxSize / (bin.Peak + h);
let widthscale = svgViewBoxSize / bin.Diameter;
//need to use the scale of the longer of the two dimensions
setScale((heightscale < widthscale ? heightscale : widthscale) * scaleMultiplier);
}, [bin, svgViewBoxSize]);
const binOutline = () => {
let startX = svgViewBoxSize / 2;
let startY = 0;
//let paddingVal = (padding * svgViewBoxSize)
return (
<Path
strokeWidth={4}
stroke={colors.grey[400]}
fillOpacity={0}
d={
"M " +
startX +
" " +
startY + //starting point
" L " +
(startX + (bin.Diameter / 2) * scale) +
" " +
roofHeight * scale + //right side of the bin roof
" V " +
(roofHeight + bin.Sidewall) * scale + //right sidewall
(hopperHeight === 0
? " H " + (startX - (bin.Diameter / 2) * scale) //bin bottom if there is no hopper
: //else
" L " +
startX +
" " +
(roofHeight + hopperHeight + bin.Sidewall) * scale + //right side of the hopper
" L " +
(startX - (bin.Diameter / 2) * scale) +
" " +
(roofHeight + bin.Sidewall) * scale) + //left side of the hopper
" V " +
roofHeight * scale + //left sidewall
" Z " //left side of the bin roof (Z command draws the line from where you currently are back to the startign point)
}
/>
);
};
const cableLines = () => {
let cables: JSX.Element[] = [];
//let paddingVal = (padding * svgViewBoxSize)
// line for the roof at the top of the sidewall
// cables.push(
// <line
// key={"roof"}
// strokeWidth={2}
// stroke="yellow"
// x1={0}
// strokeDasharray={3}
// x2={svgViewBoxSize}
// y1={roofHeight*scale + paddingVal}
// y2={roofHeight*scale + paddingVal}
// />
// )
bin.CableSums.forEach(sum => {
let cableLength = 0;
bin.Cables.forEach(c => {
if (sum.Orbit === c.Orbit) {
//numNodes = c.Nodes
cableLength = c.Length;
}
});
let numNodes = Math.ceil(cableLength / nodeSpacing);
let xVal = svgViewBoxSize / 2 + sum.DistanceFromCenter * scale;
let cableStart = sum.DistanceFromCenter * Math.tan(bin.RoofAngle * (Math.PI / 180)) * scale;
//need to offset the padding to the top since we are now below the half mark as well as pad the bottom
let cableEnd = cableStart + cableLength * scale;
cables.push(
<Line
key={"orbit" + sum.Orbit}
strokeWidth={2}
stroke={colors.grey[400]}
x1={xVal}
y1={cableStart}
x2={xVal}
y2={cableEnd}
/>
);
let svgSpacing = (cableEnd - cableStart) / numNodes;
for (let i = 0; i < numNodes; i++) {
let nodeY = cableEnd - svgSpacing * i;
cables.push(
<Circle key={"orbit" + sum.Orbit + "node" + i} cx={xVal} cy={nodeY} r={2} fill="blue" />
);
}
});
return cables;
};
const dimensionText = (key: string, x: number, y: number, text: string) => {
return (
<Text
key={key}
style={{ fontSize: dimensionFontSize, fill: "black" }}
x={x}
y={y}
textAnchor="middle"
dominantBaseline={"middle"}>
{text}
</Text>
);
};
const dimensionPath = (key: string, start: svgPoint, corner: svgPoint, end: svgPoint) => {
return (
<Path
key={key}
strokeWidth={1}
stroke="black"
fillOpacity={0}
d={
"M " +
start.x +
" " +
start.y +
"L " +
corner.x +
" " +
corner.y +
"L " +
end.x +
" " +
end.y
}
/>
);
};
const bottomDiameter = () => {
let d: JSX.Element[] = [];
let x1 = svgViewBoxSize / 2 - (bin.Diameter / 2) * scale;
let x2 = svgViewBoxSize / 2 + (bin.Diameter / 2) * scale;
let y1 =
(roofHeight + bin.Sidewall + hopperHeight) * scale +
svgViewBoxSize * (1 - scaleMultiplier) * 0.1;
let y2 =
(roofHeight + bin.Sidewall + hopperHeight) * scale +
svgViewBoxSize * (1 - scaleMultiplier) * 0.25;
d.push(
dimensionText(
"bottomText",
svgViewBoxSize / 2,
y2,
distanceConversion(bin.Diameter).toFixed(1)
)
);
d.push(
dimensionPath(
"bottomDiameterLine1",
{ x: x1, y: y1 },
{ x: x1, y: y2 },
{ x: svgViewBoxSize / 2 - gapSize, y: y2 }
)
);
d.push(
dimensionPath(
"bottomDiameterLine2",
{ x: svgViewBoxSize / 2 + gapSize, y: y2 },
{ x: x2, y: y2 },
{ x: x2, y: y1 }
)
);
return d;
};
//height of the sidewall, on the left side - always used
const sidwallDimension = () => {
let d: JSX.Element[] = [];
let centerX = svgViewBoxSize / 2;
let x1 = centerX - (bin.Diameter / 2) * scale - svgViewBoxSize * (1 - scaleMultiplier) * 0.1;
let x2 = x1 - svgViewBoxSize * (1 - scaleMultiplier) * 0.15;
let midpoint = (roofHeight + bin.Sidewall / 2) * scale;
let y1 = roofHeight * scale;
let y2 = (roofHeight + bin.Sidewall) * scale;
d.push(
dimensionText("sidewallText", x2, midpoint, distanceConversion(bin.Sidewall).toFixed(1))
);
d.push(
dimensionPath(
"sidwallTopLine",
{ x: x1, y: y1 },
{ x: x2, y: y1 },
{ x: x2, y: midpoint - gapSize }
)
);
d.push(
dimensionPath(
"sidewallbottomLine",
{ x: x2, y: midpoint + gapSize },
{ x: x2, y: y2 },
{ x: x1, y: y2 }
)
);
return d;
};
//height of the bottom of the sidewall to the peak of the bin, on the left side - only used for hopper bins
const peakDimension = () => {
let d: JSX.Element[] = [];
let centerX = svgViewBoxSize / 2;
let xWall = centerX - (bin.Diameter / 2) * scale - svgViewBoxSize * (1 - scaleMultiplier) * 0.1;
let xPeak = svgViewBoxSize / 2 - svgViewBoxSize * (1 - scaleMultiplier) * 0.1;
let xText = xWall - svgViewBoxSize * (1 - scaleMultiplier) * 0.5;
let midpoint = ((roofHeight + bin.Sidewall) / 2) * scale;
let yTop = 0;
let yBottom = (roofHeight + bin.Sidewall) * scale;
d.push(
dimensionText(
"peakText",
xText,
midpoint,
distanceConversion(bin.Sidewall + roofHeight).toFixed(1)
)
);
d.push(
dimensionPath(
"peakTopLine",
{ x: xPeak, y: yTop },
{ x: xText, y: yTop },
{ x: xText, y: midpoint - gapSize }
)
);
d.push(
dimensionPath(
"peakbottomLine",
{ x: xText, y: midpoint + gapSize },
{ x: xText, y: yBottom },
{ x: xWall, y: yBottom }
)
);
return d;
};
//total height of the bin, on the right side - always used
const totalDimension = () => {
let d: JSX.Element[] = [];
let centerX = svgViewBoxSize / 2;
//let paddingVal = (padding* svgViewBoxSize)
let binRightX = centerX + (bin.Diameter / 2) * scale;
let xBinCenter = centerX + svgViewBoxSize * (1 - scaleMultiplier) * 0.1;
let xBinWall = binRightX + svgViewBoxSize * (1 - scaleMultiplier) * 0.1;
let x2 = binRightX + svgViewBoxSize * (1 - scaleMultiplier) * 0.2;
let midpoint = ((roofHeight + bin.Sidewall + hopperHeight) / 2) * scale;
let y1 = 0;
let y2 = (roofHeight + bin.Sidewall + hopperHeight) * scale;
d.push(
dimensionText(
"totalText",
x2,
midpoint,
distanceConversion(roofHeight + bin.Sidewall + hopperHeight).toFixed(1)
)
);
d.push(
dimensionPath(
"peakTopLine",
{ x: xBinCenter, y: y1 },
{ x: x2, y: y1 },
{ x: x2, y: midpoint - gapSize }
)
);
d.push(
dimensionPath(
"peakbottomLine",
{ x: x2, y: midpoint + gapSize },
{ x: x2, y: y2 },
{ x: hopperHeight > 0 ? xBinCenter : xBinWall, y: y2 }
)
);
return d;
};
const binDimensions = () => {
let dimensions: JSX.Element[] = [];
dimensions.push(...bottomDiameter());
// sidwall Height
dimensions.push(...sidwallDimension());
//total bin height
dimensions.push(...totalDimension());
if (hopperHeight > 0) {
//show the height of the peak + sidwall without the hopper
dimensions.push(...peakDimension());
}
return dimensions;
};
const grainEstimate = () => {
let x = svgViewBoxSize / 2 - (bin.Diameter / 2) * scale;
let y =
(roofHeight + bin.Sidewall + hopperHeight) * scale +
svgViewBoxSize * (1 - scaleMultiplier) * 0.65;
return (
<Text style={{ fontSize: dimensionFontSize, fill: "black" }} x={x} y={y}>
{/* textAnchor="middle"
dominantBaseline={"middle"}> */}
{"Estimated Capacity: " + bin.Capacity + " Bushels"}
</Text>
);
};
return (
<Svg
viewBox={
"0 " +
(svgViewBoxSize * (scaleMultiplier - 1)) / 2 +
" " +
svgViewBoxSize +
" " +
svgViewBoxSize
}
height="100%"
width="100%">
{cableLines()}
{binOutline()}
{binDimensions()}
{grainEstimate()}
</Svg>
);
}

View file

@ -0,0 +1,74 @@
import { jsonBin } from "common/DataImports/BinCables/BinCableImporter";
import { Circle, Svg } from "@react-pdf/renderer";
import { colors } from "@mui/material";
interface Props {
bin: jsonBin;
}
export default function PDFTopView(props: Props) {
const { bin } = props;
const totalRadius = 240;
const outerEdge = () => {
return (
<Circle
cx="50%"
cy="50%"
r={totalRadius}
stroke={colors.grey[400]}
strokeWidth={4}
fillOpacity={0}
/>
);
};
const orbits = () => {
let o: JSX.Element[] = [];
bin.CableSums.forEach((sum, i) => {
//get the radius of the bin
let binRadius = bin.Diameter / 2;
//determine the percentage the orbit is from the center
let perc = sum.DistanceFromCenter / binRadius;
//use the percentage to find the radius for the svg
let svgRadius = perc * totalRadius;
o.push(
<Circle
key={"orbit" + i}
cx="50%"
cy="50%"
r={svgRadius}
stroke={colors.grey[400]}
strokeWidth={4}
fillOpacity={0}
/>
);
//then determine the points the cables will be
let angleDist = 360 / sum.NumberOfCables;
let currentAngle = 90;
for (let k = 0; k < sum.NumberOfCables; k++) {
let x = svgRadius * Math.sin(currentAngle * (Math.PI / 180));
let y = svgRadius * Math.cos(currentAngle * (Math.PI / 180));
o.push(
<Circle
key={"orbit" + i + "cable" + k}
r={12}
cx="50%"
cy="50%"
transform={"translate(" + x + " " + y + ")"}
fill={"blue"}
/>
);
currentAngle += angleDist;
}
});
return o;
};
return (
<Svg viewBox="0 0 500 500" height="100%" width="100%">
{outerEdge()}
{orbits()}
</Svg>
);
}

View file

@ -93,6 +93,7 @@ interface Props<T> {
selectedRows?: number[]; //which rows are selected will need to be controlled by the parent in order to keep track between page changes selectedRows?: number[]; //which rows are selected will need to be controlled by the parent in order to keep track between page changes
customElement?: JSX.Element //element that will be placed in the table head between the table actions and the column header rows customElement?: JSX.Element //element that will be placed in the table head between the table actions and the column header rows
mobileView?: boolean //can be used to force the table to use its mobile view for narrow containing elements ie, drawer mobileView?: boolean //can be used to force the table to use its mobile view for narrow containing elements ie, drawer
hidePagination?: boolean //when passed in will hide the pagination at the bottom of the table
} }
export default function ResponsiveTable<T extends Object>(props: Props<T>) { export default function ResponsiveTable<T extends Object>(props: Props<T>) {
@ -120,7 +121,8 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
rowSelect, rowSelect,
selectedRows, selectedRows,
customElement, customElement,
mobileView mobileView,
hidePagination
} = props } = props
const classes = useStyles(); const classes = useStyles();
const columns = typeof(props.columns) === "function" ? props.columns() : props.columns const columns = typeof(props.columns) === "function" ? props.columns() : props.columns
@ -463,7 +465,7 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
<Checkbox <Checkbox
checked={selectedRows ? selectedRows.includes(index) : false} checked={selectedRows ? selectedRows.includes(index) : false}
onClick={(event) => {event.stopPropagation()}} onClick={(event) => {event.stopPropagation()}}
onChange={(e, checked) => { onChange={(_, checked) => {
rowSelect(row, index, checked) rowSelect(row, index, checked)
}}/> }}/>
</TableCell> </TableCell>
@ -529,15 +531,17 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
} }
</TableBody> </TableBody>
</Table> </Table>
<TablePagination {!hidePagination &&
rowsPerPageOptions={rowsPerPageOptions ? rowsPerPageOptions : [5, 10, 20]} <TablePagination
component="div" rowsPerPageOptions={rowsPerPageOptions ? rowsPerPageOptions : [5, 10, 20]}
count={total ? total : 0} component="div"
rowsPerPage={pageSize} count={total ? total : 0}
page={page} rowsPerPage={pageSize}
onPageChange={(_event, page) => handlePageChange(page)} page={page}
onRowsPerPageChange={handleRowsPerPageChange} onPageChange={(_event, page) => handlePageChange(page)}
/> onRowsPerPageChange={handleRowsPerPageChange}
/>
}
<Menu <Menu
id="userMenu" id="userMenu"
anchorEl={columnAnchor} anchorEl={columnAnchor}

View file

@ -0,0 +1,41 @@
import { Document, Page, StyleSheet, View } from "@react-pdf/renderer";
import Header from "./components/header";
import Footer from "./components/footer";
interface Props {
content: JSX.Element;
}
export default function AgPDFTemplate(props: Props) {
const pdfStyle = StyleSheet.create({
page: {
fontSize: 11,
padding: "5%"
},
header: {
flexDirection: "row",
height: "8%"
},
content: {
flexDirection: "row",
height: "87%"
},
footer: {
flexDirection: "row",
height: "5%"
}
});
return (
<Document>
<Page size={"A4"} style={pdfStyle.page}>
<View style={pdfStyle.header}>
<Header />
</View>
<View style={pdfStyle.content}>{props.content}</View>
<View style={pdfStyle.footer}>
<Footer />
</View>
</Page>
</Document>
);
}

View file

@ -0,0 +1,31 @@
import { Image, StyleSheet, View } from "@react-pdf/renderer";
import agFooter from "common/pdfTemplates/Agriculture/components/footerImages/agFooter.png";
import React from "react";
export default function Footer() {
const styles = StyleSheet.create({
view: {
flexDirection: "row",
fontStyle: "normal",
fontWeight: 650,
borderTop: "1px solid black",
padding: 10
},
label: {
width: "20%"
},
email: {
width: "40%"
},
phone: {
width: "40%"
}
});
return (
<React.Fragment>
<View style={styles.view}>
<Image src={agFooter} />
</View>
</React.Fragment>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View file

@ -0,0 +1,32 @@
import { Image, StyleSheet, Text, View } from "@react-pdf/renderer";
import moment from "moment";
import React from "react";
import logo from "assets/whitelabels/AdaptiveAgriculture/AgLogoText.png";
export default function Header() {
const styles = StyleSheet.create({
view: {
flexDirection: "row",
fontStyle: "normal",
fontWeight: 650,
borderBottom: "1px solid black"
},
logo: {
width: "55%",
textAlign: "left",
paddingBottom: 15
},
estimate: {
width: "45%",
textAlign: "right"
}
});
return (
<React.Fragment>
<View style={styles.view}>
<Image style={styles.logo} src={logo} />
<Text style={styles.estimate}>{moment().format("YYYY-MM-DD")}</Text>
</View>
</React.Fragment>
);
}

View file

@ -23,7 +23,8 @@ const adminFeatures = [
"json", "json",
"tasks", "tasks",
"teams", "teams",
"developer" "developer",
"installer"
]; ];
export class User { export class User {

View file

@ -19,6 +19,8 @@ import Heaters from "pages/Heaters";
import Heater from "pages/Heater"; import Heater from "pages/Heater";
import Tasks from "pages/Tasks"; import Tasks from "pages/Tasks";
import Transactions from "pages/Transactions"; import Transactions from "pages/Transactions";
import BinCableEstimator from "pages/BinCableEstimator";
import { useGlobalState } from "providers";
//import Site from "pages/Site"; //import Site from "pages/Site";
const DeviceHistory = lazy(() => import("pages/DeviceHistory")); const DeviceHistory = lazy(() => import("pages/DeviceHistory"));
@ -48,6 +50,7 @@ export default function Router(props: Props) {
const { toggleTheme } = props; const { toggleTheme } = props;
const { isAuthenticated, loginWithRedirect, isLoading } = useAuth0(); const { isAuthenticated, loginWithRedirect, isLoading } = useAuth0();
const whiteLabel = getWhitelabel(); const whiteLabel = getWhitelabel();
const [{ user }] = useGlobalState();
const RelativeRoutes = () => { const RelativeRoutes = () => {
return ( return (
@ -303,6 +306,9 @@ export default function Router(props: Props) {
<Route path="users" element={<Users/>} /> <Route path="users" element={<Users/>} />
<Route path="tasks" element={<Tasks />} /> <Route path="tasks" element={<Tasks />} />
<Route path="transactions" element={<Transactions />} /> <Route path="transactions" element={<Transactions />} />
{user.hasFeature("installer") &&
<Route path="cableEstimate" element={<BinCableEstimator />} />
}
{/* Map pages */} {/* Map pages */}
<Route path="visualFarm" element={<FieldMap />} /> <Route path="visualFarm" element={<FieldMap />} />

View file

@ -40,6 +40,7 @@ import JobsiteIcon from "products/Construction/JobSiteIcon";
import { getThemeType } from "theme"; import { getThemeType } from "theme";
import ObjectHeaterIcon from "products/Construction/ObjectHeaterIcon"; import ObjectHeaterIcon from "products/Construction/ObjectHeaterIcon";
import TasksIcon from "products/Construction/TasksIcon"; import TasksIcon from "products/Construction/TasksIcon";
import CableIcon from "products/Bindapt/CableIcon";
const drawerWidth = 230; const drawerWidth = 230;
@ -321,6 +322,20 @@ export default function SideNavigator(props: Props) {
{open && <ListItemText primary="Transactions" />} {open && <ListItemText primary="Transactions" />}
</ListItemButton> </ListItemButton>
</Tooltip> </Tooltip>
{(user.hasFeature("installer") && isAg || user.hasFeature("admin")) &&
<Tooltip title="Cable Estimator" placement="right">
<ListItemButton
id="tour-cable-estimator"
onClick={() => goTo("/cableEstimate")}
classes={getClasses("/cableEstimate")}
>
<ListItemIcon>
<CableIcon />
</ListItemIcon>
{open && <ListItemText primary="Cable Estimator" />}
</ListItemButton>
</Tooltip>
}
</List> </List>
) )
} }

View file

@ -0,0 +1,10 @@
import CableEstimator from "cableEstimator/cableEstimator";
import PageContainer from "./PageContainer";
export default function BinCableEstimator() {
return (
<PageContainer>
<CableEstimator />
</PageContainer>
);
}

View file

@ -22,16 +22,8 @@ export default function TransactionViewer(props: Props) {
const [pageSize, setPageSize] = useState(10) const [pageSize, setPageSize] = useState(10)
useEffect(()=>{ useEffect(()=>{
// let rows: Transaction[] = []
// transactions.forEach((transaction, index) => {
// if (index >= tablePage * pageSize && index < (tablePage + 1) * pageSize){
// rows.push(transaction)
// }
// })
let clone: Transaction[] = cloneDeep(transactions) let clone: Transaction[] = cloneDeep(transactions)
let rows = clone.splice(tablePage * pageSize, pageSize) let rows = clone.splice(tablePage * pageSize, pageSize)
console.log(transactions)
console.log(rows)
setDisplayedRows(rows) setDisplayedRows(rows)
},[transactions, tablePage, pageSize]) },[transactions, tablePage, pageSize])