Merge branch 'master' into cable_expander

This commit is contained in:
csawatzky 2025-10-23 14:02:44 -06:00
commit da0ce8416b
47 changed files with 1824 additions and 464 deletions

254
package-lock.json generated
View file

@ -2208,16 +2208,20 @@
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
"integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
"integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"eslint-visitor-keys": "^3.3.0"
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
},
"peerDependencies": {
"eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
}
@ -2235,21 +2239,23 @@
}
},
"node_modules/@eslint-community/regexpp": {
"version": "4.11.1",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz",
"integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==",
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
"integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
}
},
"node_modules/@eslint/config-array": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz",
"integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==",
"version": "0.21.0",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
"integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/object-schema": "^2.1.4",
"@eslint/object-schema": "^2.1.6",
"debug": "^4.3.1",
"minimatch": "^3.1.2"
},
@ -2257,20 +2263,38 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/core": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz",
"integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==",
"node_modules/@eslint/config-helpers": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz",
"integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^0.16.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/core": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz",
"integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/eslintrc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
"integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==",
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
"integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
@ -2294,6 +2318,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@ -2310,6 +2335,7 @@
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
@ -2321,15 +2347,20 @@
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
"dev": true,
"license": "MIT"
},
"node_modules/@eslint/js": {
"version": "9.13.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.13.0.tgz",
"integrity": "sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==",
"version": "9.37.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz",
"integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://eslint.org/donate"
}
},
"node_modules/@eslint/object-schema": {
@ -2337,16 +2368,19 @@
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
"integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/plugin-kit": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz",
"integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==",
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz",
"integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^0.16.0",
"levn": "^0.4.1"
},
"engines": {
@ -2405,18 +2439,20 @@
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanfs/node": {
"version": "0.16.5",
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.5.tgz",
"integrity": "sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==",
"version": "0.16.7",
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
"integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@humanfs/core": "^0.19.0",
"@humanwhocodes/retry": "^0.3.0"
"@humanfs/core": "^0.19.1",
"@humanwhocodes/retry": "^0.4.0"
},
"engines": {
"node": ">=18.18.0"
@ -2436,10 +2472,11 @@
}
},
"node_modules/@humanwhocodes/retry": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
"integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
"integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18"
},
@ -2622,20 +2659,6 @@
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz",
"integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="
},
"node_modules/@mapbox/mapbox-sdk/node_modules/form-data": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.3.tgz",
"integrity": "sha512-q5YBMeWy6E2Un0nMGWMgI65MAKtaylxfNJGJxpGh45YDciZB4epbWpaAfImil6CPAPTYB4sh0URQNDRIZG5F2w==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"mime-types": "^2.1.35"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/@mapbox/parse-mapbox-token": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@mapbox/parse-mapbox-token/-/parse-mapbox-token-0.2.0.tgz",
@ -6614,10 +6637,11 @@
"integrity": "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA=="
},
"node_modules/acorn": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
@ -6630,6 +6654,7 @@
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
@ -6669,7 +6694,8 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
"dev": true,
"license": "Python-2.0"
},
"node_modules/arr-union": {
"version": "3.1.0",
@ -6786,14 +6812,32 @@
}
},
"node_modules/axios": {
"version": "1.8.2",
"integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axios/node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/babel-plugin-macros": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
@ -8029,30 +8073,33 @@
}
},
"node_modules/eslint": {
"version": "9.13.0",
"integrity": "sha512-EYZK6SX6zjFHST/HRytOdA/zE72Cq/bfw45LSyuwrdvcclb/gqV8RRQxywOBEWO2+WDpva6UZa4CcDeJKzUCFA==",
"version": "9.37.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz",
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.11.0",
"@eslint/config-array": "^0.18.0",
"@eslint/core": "^0.7.0",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "9.13.0",
"@eslint/plugin-kit": "^0.2.0",
"@humanfs/node": "^0.16.5",
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.21.0",
"@eslint/config-helpers": "^0.4.0",
"@eslint/core": "^0.16.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.37.0",
"@eslint/plugin-kit": "^0.4.0",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.3.1",
"@humanwhocodes/retry": "^0.4.2",
"@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15",
"ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
"cross-spawn": "^7.0.6",
"debug": "^4.3.2",
"escape-string-regexp": "^4.0.0",
"eslint-scope": "^8.1.0",
"eslint-visitor-keys": "^4.1.0",
"espree": "^10.2.0",
"eslint-scope": "^8.4.0",
"eslint-visitor-keys": "^4.2.1",
"espree": "^10.4.0",
"esquery": "^1.5.0",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
@ -8066,8 +8113,7 @@
"lodash.merge": "^4.6.2",
"minimatch": "^3.1.2",
"natural-compare": "^1.4.0",
"optionator": "^0.9.3",
"text-table": "^0.2.0"
"optionator": "^0.9.3"
},
"bin": {
"eslint": "bin/eslint.js"
@ -8107,10 +8153,11 @@
}
},
"node_modules/eslint-scope": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz",
"integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==",
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
"integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
@ -8123,10 +8170,11 @@
}
},
"node_modules/eslint-visitor-keys": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
@ -8150,18 +8198,6 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/eslint/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@ -8169,14 +8205,15 @@
"dev": true
},
"node_modules/espree": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
"integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"acorn": "^8.14.0",
"acorn": "^8.15.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^4.2.0"
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -8202,6 +8239,7 @@
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"estraverse": "^5.2.0"
},
@ -8519,13 +8557,16 @@
}
},
"node_modules/form-data": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz",
"integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.35"
},
"engines": {
"node": ">= 6"
@ -9690,6 +9731,7 @@
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
@ -10911,7 +10953,7 @@
},
"node_modules/protobuf-ts": {
"version": "1.0.0",
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#ca5549f5023972ad0184658e4a427b6f9c39cce8",
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#f7e123b74e1bfc6a4cd6f75b0f22b2c953b4eaf6",
"dependencies": {
"protobufjs": "^6.8.8"
}
@ -12586,6 +12628,7 @@
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
},
@ -12701,12 +12744,6 @@
"node": ">=10"
}
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
"node_modules/tiny-inflate": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
@ -13720,10 +13757,11 @@
}
},
"node_modules/vite": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"version": "6.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -249,9 +249,9 @@ export default function BinCard(props: Props) {
noWrap
style={{ fontSize: "0.85rem", fontWeight: 650, color: tempColour }}>
{display +
(user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS
? C"
: F")}
(user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT
? F"
: C")}
</Typography>
);
};

View file

@ -22,6 +22,7 @@ interface Props {
loadMore?: () => void;
//startingTranslate: number;
valDisplay?: "high" | "low" | "average";
insert?: boolean
}
const useStyles = makeStyles((_theme) => {
@ -39,7 +40,7 @@ const useStyles = makeStyles((_theme) => {
});
export default function BinsList(props: Props) {
const { bins, duplicateBin, title, gridView, loadMore, valDisplay } = props;
const { bins, duplicateBin, title, gridView, loadMore, valDisplay, insert } = props;
const classes = useStyles();
// const history = useHistory();
const navigate = useNavigate()
@ -114,11 +115,16 @@ export default function BinsList(props: Props) {
{bins.map((b, i) =>
(
<Grid
size={{
size={insert ? {
xs: 6,
sm: 6,
md: 4,
lg: 3,
} : {
xs: 6,
sm: 4,
md: 3,
lg: 2,
lg: 1.5,
}}
key={i}
className={classes.gridListTile}

View file

@ -316,6 +316,7 @@ export default function BinyardDisplay(props: Props) {
<Grid size={12}>
<BinsList
gridView
insert
valDisplay="average"
bins={grainBins}
duplicateBin={duplicateBin}
@ -332,6 +333,7 @@ export default function BinyardDisplay(props: Props) {
<Grid size={12}>
<BinsList
gridView
insert
valDisplay="average"
bins={fertBins}
duplicateBin={duplicateBin}
@ -348,6 +350,7 @@ export default function BinyardDisplay(props: Props) {
<Grid size={12}>
<BinsList
gridView
insert
valDisplay="average"
bins={emptyBins}
duplicateBin={duplicateBin}

View file

@ -92,7 +92,7 @@ export default function DisplayDrawer(props: Props) {
classes={{ paper: classes.drawerPaper }}>
<Box
className={isMobile ? classes.drawerMobile : classes.drawerDesktop}
style={{ width: isMobile ? "100%" : width }}>
style={{ width: isMobile ? "100%" : width, height: isMobile ? drawerHeight + "vh" : 0}}>
<Box className={classes.header} style={{ width: isMobile ? "100%" : width }}>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Box>

View file

@ -114,6 +114,29 @@ export default function StatusDust(props: Props) {
)
}
function compareTimestamps(timestamp1: string, timestamp2: string): number {
const date1 = new Date(timestamp1);
const date2 = new Date(timestamp2);
// Check if dates are valid
if (isNaN(date1.getTime()) || isNaN(date2.getTime())) {
throw new Error("Invalid RFC3339 timestamp format");
}
// Compare using getTime() for millisecond precision
return date1.getTime() - date2.getTime();
}
if (device.status.sen5x?.timestamp) {
const now = new Date();
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
if (compareTimestamps(device.status.sen5x?.timestamp, oneDayAgo.toISOString()) < 0) {
return null
}
} else {
return null
}
return (
<Tooltip title={tooltip()}>
<PulseBox color={color} className={classes.box} >

View file

@ -152,9 +152,31 @@ export default function StatusGas(props: Props) {
)
}
function compareTimestamps(timestamp1: string, timestamp2: string): number {
const date1 = new Date(timestamp1);
const date2 = new Date(timestamp2);
// Check if dates are valid
if (isNaN(date1.getTime()) || isNaN(date2.getTime())) {
throw new Error("Invalid RFC3339 timestamp format");
}
// Compare using getTime() for millisecond precision
return date1.getTime() - date2.getTime();
}
const gasDisplay = () => {
const gas = gasType === "all" ? gasTypes[currentIndex] : gasType
// if (gasType !== "all") {
if (device.status[gas]?.timestamp) {
const now = new Date();
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
if (compareTimestamps(device.status[gas]?.timestamp, oneDayAgo.toISOString()) < 0) {
return null
}
} else {
return null
}
return (
<Tooltip title={tooltip()}>
<PulseBox color={color} className={gasType === "all" ? classes.boxTall : classes.box}>

View file

@ -80,6 +80,29 @@ export default function StatusPlenum(props: Props) {
)
}
function compareTimestamps(timestamp1: string, timestamp2: string): number {
const date1 = new Date(timestamp1);
const date2 = new Date(timestamp2);
// Check if dates are valid
if (isNaN(date1.getTime()) || isNaN(date2.getTime())) {
throw new Error("Invalid RFC3339 timestamp format");
}
// Compare using getTime() for millisecond precision
return date1.getTime() - date2.getTime();
}
if (device.status.plenum?.timestamp) {
const now = new Date();
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
if (compareTimestamps(device.status.plenum?.timestamp, oneDayAgo.toISOString()) < 0) {
return null
}
} else {
return null
}
return (
<Tooltip title={tooltip()}>
<PulseBox color={color} className={classes.box} >

View file

@ -120,6 +120,29 @@ export default function StatusSen5x(props: Props) {
)
}
function compareTimestamps(timestamp1: string, timestamp2: string): number {
const date1 = new Date(timestamp1);
const date2 = new Date(timestamp2);
// Check if dates are valid
if (isNaN(date1.getTime()) || isNaN(date2.getTime())) {
throw new Error("Invalid RFC3339 timestamp format");
}
// Compare using getTime() for millisecond precision
return date1.getTime() - date2.getTime();
}
if (device.status.sen5x?.timestamp) {
const now = new Date();
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
if (compareTimestamps(device.status.sen5x?.timestamp, oneDayAgo.toISOString()) < 0) {
return null
}
} else {
return null
}
return (
<Tooltip title={tooltip()}>
<PulseBox color={color} className={classes.box} >

View file

@ -8,6 +8,7 @@ import {
MenuItem,
OutlinedInput,
Select,
TextField,
Theme
} from "@mui/material";
import moment, { Moment } from "moment";
@ -16,6 +17,7 @@ import ReactDOM from "react-dom";
import { DateRangePreset, SetDefaultPreset } from "./DateRange";
import ResponsiveDialog from "common/ResponsiveDialog";
import { withStyles, WithStyles } from "@mui/styles";
import { cloneDeep } from "lodash";
const styles = (_theme: Theme) => createStyles({});
@ -249,18 +251,30 @@ class DateSelect extends React.Component<Props, State> {
onClose={this.closeDateRangeDialog}
aria-labelledby="date-range-dialog">
<DialogContent>
{/* <StaticDateRangePicker
disableFuture
value={dateRange}
onChange={date => this.updateDateRange(date)}
renderInput={(startProps, endProps) => (
<React.Fragment>
<TextField {...startProps} />
<DateRangeDelimiter> to </DateRangeDelimiter>
<TextField {...endProps} />
</React.Fragment>
)}
/> */}
<TextField
fullWidth
margin="normal"
type="date"
label="Start Time"
value={dateRange[0]?.format("YYYY-MM-DD")}
onChange={e => {
let range = cloneDeep(dateRange)
range[0] = moment(e.target.value)
this.updateDateRange(range)
}}
/>
<TextField
fullWidth
margin="normal"
type="date"
label="End Time"
value={dateRange[1]?.format("YYYY-MM-DD")}
onChange={e => {
let range = cloneDeep(dateRange)
range[1] = moment(e.target.value)
this.updateDateRange(range)
}}
/>
</DialogContent>
<DialogActions>
<Button onClick={this.closeDateRangeDialog} color="primary">

View file

@ -285,7 +285,6 @@ export default function ComponentCard(props: Props) {
// ) : (
<UnitMeasurementSummary
component={component}
excludedNodes={component.settings.excludedNodes}
reading={UnitMeasurement.convertLastMeasurement(measurements)}
//dense
/>

View file

@ -449,7 +449,7 @@ export default function ComponentForm(props: Props) {
let minimum = min ?? 0;
let maximum = max ?? 100;
if (compMode && form.component.settings.calibrate) {
return Number(form.offset) <= maximum && Number(form.offset) > minimum;
return Number(form.offset) <= maximum && Number(form.offset) >= minimum;
}
return !isNaN(Number(form.offset));
};

View file

@ -270,7 +270,7 @@ export default function UpgradeDevice(props: Props) {
<Divider />
<DialogContent className={classes.dialogContent}>
<DialogContentText id="alert-dialog-slide-description">
Upgrading from {currentFirmware()} to {latestFirmware()}
Upgrading {device.platformName()} from {currentFirmware()} to {latestFirmware()}
<br />
<br />
It is recommended that the device be plugged in during the upgrade as it will take

View file

@ -1,7 +1,13 @@
import {
Accordion,
AccordionDetails,
AccordionSummary,
Box,
Button,
Grid2,
IconButton,
Paper,
Stack,
Tab,
Table,
TableBody,
@ -9,16 +15,26 @@ import {
TableContainer,
TableHead,
TableRow,
Tabs
Tabs,
Typography
} from "@mui/material";
import React, { useCallback, useEffect, useState } from "react";
import { useMobile, useSnackbar } from "hooks";
import { useGlobalState, useFieldAPI } from "providers";
import { useMobile, useSnackbar, useUserAPI } from "hooks";
import { useGlobalState, useFieldAPI, useJohnDeereProxyAPI, useCNHiProxyAPI } from "providers";
//import HarvestTable from "harvestPlan/HarvestTable";
import { Field } from "models";
import { Field, fieldScope, teamScope } from "models";
import GrainDescriber from "grain/GrainDescriber";
import { pond } from "protobuf-ts/pond";
import HarvestSettings from "harvestPlan/HarvestSettings";
import ResponsiveTable, { Column } from "common/ResponsiveTable";
import FieldMinimap from "./Fieldminimap";
import FieldActions from "./FieldActions";
import FieldSettings from "./FieldSettings";
import { ArrowForward, ExpandMore, Settings } from "@mui/icons-material";
import { cloneDeep } from "lodash";
import ShareAllFields from "./ShareAllFields";
import EventBlocker from "common/EventBlocker";
import { useNavigate } from "react-router-dom";
interface TabPanelProps {
children?: React.ReactNode;
@ -26,128 +42,244 @@ interface TabPanelProps {
value: any;
}
function TabPanelMine(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
aria-labelledby={`simple-tab-${index}`}
style={{ height: "94%" }}
{...other}>
{value === index && <React.Fragment>{children}</React.Fragment>}
</div>
);
}
export default function FieldList() {
const [{ as }] = useGlobalState();
const isMobile = useMobile();
const fieldAPI = useFieldAPI();
const jdAPI = useJohnDeereProxyAPI();
const cnhAPI = useCNHiProxyAPI();
const { openSnack } = useSnackbar();
const [fields, setFields] = useState<Field[]>([]);
const [value, setValue] = React.useState(0);
const [tabValue, setTabValue] = React.useState(0);
const [openHarvestSettings, setOpenHarvestSettings] = useState(false);
const [fieldForPlan, setFieldForPlan] = useState(Field.create());
// const [fieldForPlan, setFieldForPlan] = useState(Field.create());
const [fields, setFields] = useState<Field[]>([]);
const [total, setTotal] = useState(0)
const [rowsPerPage, setRowsPerPage] = useState(5)
const [page, setPage] = useState(0)
const [selectedField, setSelectedField] = useState<Field>()
const [openFieldSettings, setOpenFieldSettings] = useState(false)
const [shareOpen, setShareOpen] = useState(false)
const navigate = useNavigate();
const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
setValue(newValue);
setTabValue(newValue);
setPage(0)
};
const loadFields = useCallback(() => {
const loadAdaptiveFields = useCallback(() => {
fieldAPI
.listFields(500, 0, "asc", "fieldName", undefined, as)
.listFields(rowsPerPage, page*rowsPerPage, "asc", "fieldName", undefined, as)
.then(resp => {
setFields(resp.data.fields.map(f => Field.any(f)));
// resp.data.fields.forEach(f => {
// if (f.settings) {
// fieldAPI.updateField(f.settings.key, f.settings);
// }
// });
setFields(resp.data.fields.map(f => Field.create(f)));
setTotal(resp.data.total)
})
.catch(err => {
openSnack("Failed to load Field Mapping");
openSnack("Failed to load Fields");
});
}, [fieldAPI, as, openSnack]);
}, [fieldAPI, as, openSnack, page, rowsPerPage]);
const loadJDFields = useCallback(() => {
jdAPI.listFields(rowsPerPage, page*rowsPerPage, as)
.then(resp => {
resp.data.fields ? setFields(resp.data.fields.map(f => Field.create(f))) : setFields([])
}).catch(err => {
openSnack("Failed to load John Deere Fields");
})
},[jdAPI, as, openSnack, page, rowsPerPage])
const loadCNHFields = useCallback(() => {
cnhAPI.listFields(rowsPerPage, page*rowsPerPage, as)
.then(resp => {
resp.data.fields ? setFields(resp.data.fields.map(f => Field.create(f))) : setFields([])
}).catch(err => {
openSnack("Failed to load John Deere Fields");
})
},[cnhAPI, as, openSnack, page, rowsPerPage])
useEffect(() => {
loadFields();
}, [loadFields]);
//based on the current tab load the correct fields
switch(tabValue){
case 0:
loadAdaptiveFields();
break;
case 1:
loadJDFields();
break;
case 2:
loadCNHFields();
break;
}
}, [loadAdaptiveFields, loadJDFields, loadCNHFields, tabValue]);
const calcTotalAcres = () => {
let totalAcres = 0;
fields.forEach(field => {
totalAcres += field.acres();
});
totalAcres = Math.round(totalAcres);
return totalAcres;
};
const goTo = (field: Field) => {
let path = field.key();
navigate(path, { state: {field: field, permissions: field.permissions} });
}
const listFieldsInfo = fields.map((field, index) => (
<TableRow key={index}>
<TableCell>{field.fieldName()}</TableCell>
<TableCell>{field.landLoc()}</TableCell>
<TableCell>
{field.crop() === pond.Grain.GRAIN_CUSTOM
? field.customType()
: GrainDescriber(field.crop()).name}
</TableCell>
<TableCell>{field.calculateAcres()}</TableCell>
<TableCell>
{field.permissions.includes(pond.Permission.PERMISSION_WRITE) && (
<Button
variant="contained"
color="primary"
onClick={() => {
setFieldForPlan(field);
setOpenHarvestSettings(true);
}}>
New Crop Plan
</Button>
)}
</TableCell>
</TableRow>
));
const fieldActions = (field: Field) => {
return (
<EventBlocker>
<IconButton
onClick={() => {
setSelectedField(field)
setOpenFieldSettings(true)
}}><Settings /></IconButton>
<FieldActions field={field} permissions={field.permissions} refreshCallback={()=>{}}/>
{isMobile && <IconButton onClick={() => {goTo(field)}}><ArrowForward /></IconButton>}
</EventBlocker>
)
}
const columns: Column<Field>[] = [
{
title: "View",
render: row => {
return (
<Box margin={1} height={200} width={200}>
<FieldMinimap field={row} squared borderSpacing={0.001}/>
</Box>
)
}
},
{
title: "Field Name",
render: row => {
return <Typography paddingLeft={2}>{row.name()}</Typography>
}
},
{
title: "Current Crop",
render: row => {
return <Typography paddingLeft={2}>{GrainDescriber(row.crop()).name}</Typography>
}
},
{
title: "Acres",
render: row => {
return <Typography paddingLeft={2}>{row.acres()}</Typography>
}
},
{
title: "Actions",
hidden: tabValue > 0,
render: row => fieldActions(row)
}
]
const fieldTable = (fields: Field[]) => {
return (
<ResponsiveTable<Field>
columns={columns}
rows={fields}
onRowClick={(row) => {
!isMobile && goTo(row)
}}
setPage={(newPage)=>{
setPage(newPage)
}}
handleRowsPerPageChange={(e)=>{setRowsPerPage(e.target.value)}}
page={page}
pageSize={rowsPerPage}
total={total}
resizeable
loadMore={() => {
let currentFields = cloneDeep(fields)
switch(tabValue){
case 0:
fieldAPI.listFields(rowsPerPage, currentFields.length, "asc", "fieldName", undefined, as).then(resp => {
setFields(currentFields.concat(resp.data.fields.map(f => Field.create(f))))
}).catch(err => {
openSnack("Failed to load more fields")
})
break;
case 1:
jdAPI.listFields(rowsPerPage, currentFields.length, as).then(resp => {
resp.data.fields ? setFields(currentFields.concat(resp.data.fields.map(f => Field.create(f)))) : setFields([])
}).catch(err => {
openSnack("Failed to load more fields")
})
break;
case 2:
cnhAPI.listFields(rowsPerPage, currentFields.length, as).then(resp => {
resp.data.fields ? setFields(currentFields.concat(resp.data.fields.map(f => Field.create(f)))) : setFields([])
}).catch(err => {
openSnack("Failed to load more fields")
})
break;
}
}}
renderMobile={(row, index) => {
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMore />}>
<Grid2 alignItems="center" width="100%" container direction={"row"} justifyContent="space-between" wrap="nowrap">
<Grid2>
<Typography textAlign="center">
{row.name()}
</Typography>
</Grid2>
<Grid2>
{fieldActions(row)}
</Grid2>
</Grid2>
</AccordionSummary>
<AccordionDetails>
<Grid2 alignItems="center" width="100%" container direction={"row"} justifyContent="space-between" wrap="nowrap">
<Grid2 size={6}>
<Box height={150} width="100%">
<FieldMinimap field={row} borderSpacing={0.001} squared />
</Box>
</Grid2>
<Grid2 size={6}>
<Stack spacing={2}>
<Typography textAlign="center">{row.grainName()}</Typography>
<Typography textAlign="center">{row.acres()} Acres</Typography>
</Stack>
<Button variant="contained" color="primary">Go</Button>
</Grid2>
</Grid2>
</AccordionDetails>
</Accordion>
)
}}
/>
)
}
return (
<Box>
<Button
onClick={()=>{setShareOpen(true)}}
variant="contained"
color="primary">
Share All
</Button>
<Tabs
value={value}
value={tabValue}
onChange={handleChange}
TabIndicatorProps={{ style: { background: "rgba(255,255,0,255)" } }}>
<Tab label="Field Overview" />
{/* {!isMobile && <Tab label="Current Harvest Plans" />}
{!isMobile && <Tab label="Plan History" />} */}
<Tab label="Adaptive" />
<Tab label="John Deere" />
<Tab label="Case New Holland" />
</Tabs>
<TabPanelMine index={0} value={value}>
<TableContainer component={Paper}>
<Table style={{ minWidth: 1000 }}>
<TableHead>
<TableRow>
<TableCell>Field Name</TableCell>
<TableCell>Land Location</TableCell>
<TableCell>Main Crop Type</TableCell>
<TableCell>Acres ({calcTotalAcres()} Total)</TableCell>
<TableCell>Create New Plan</TableCell>
</TableRow>
</TableHead>
<TableBody>{listFieldsInfo}</TableBody>
</Table>
</TableContainer>
</TabPanelMine>
{/* need to re-factor the harvest table file before restoring these functions */}
{/* <TabPanelMine index={1} value={value}>
<HarvestTable fields={fields} display="current" />
</TabPanelMine>
<TabPanelMine index={2} value={value}>
<HarvestTable fields={fields} display="history" />
</TabPanelMine> */}
<HarvestSettings
{fieldTable(fields)}
{/* <HarvestSettings
open={openHarvestSettings}
close={() => setOpenHarvestSettings(false)}
field={fieldForPlan}
/> */}
<FieldSettings
selectedField={selectedField}
removeField={() => {
//will need to re-load the fields after one is deleted
loadAdaptiveFields()
}}
open={openFieldSettings}
onClose={() => {
setOpenFieldSettings(false);
}}
/>
<ShareAllFields open={shareOpen} close={() => {setShareOpen(false)}}/>
</Box>
);
}

View file

@ -152,9 +152,7 @@ export default function FieldSettings(props: Props) {
const fieldInformation = () => {
return (
<Box>
<Box>
<DialogTitle>Field Information</DialogTitle>
<React.Fragment>
<TextField
value={fieldName}
margin="normal"
@ -258,7 +256,14 @@ export default function FieldSettings(props: Props) {
Field Colour
<ColourPicker onChange={color => setFeatColor(color)} />
</Box>
</Box>
</React.Fragment>
);
};
return (
<ResponsiveDialog fullWidth open={props.open} onClose={onClose}>
<DialogTitle>Field Information</DialogTitle>
<DialogContent>{fieldInformation()}</DialogContent>
<DialogActions>
<Grid container direction="row">
<Grid>
@ -282,13 +287,6 @@ export default function FieldSettings(props: Props) {
</Grid>
</Grid>
</DialogActions>
</Box>
);
};
return (
<ResponsiveDialog fullWidth fullScreen={false} open={props.open} onClose={onClose}>
<DialogContent>{fieldInformation()}</DialogContent>
</ResponsiveDialog>
);
}

View file

@ -0,0 +1,61 @@
import { CircularProgress } from "@mui/material";
import { Field } from "models";
import { useEffect, useState } from "react";
import Map from "react-map-gl/mapbox";
import 'mapbox-gl/dist/mapbox-gl.css';
import GeoMapLayer from "maps/mapLayers/geoMapLayer";
import { Feature, FeatureCollection } from "geojson";
import { GeometryMapping } from "models/GeometryMapping";
interface Props {
field: Field
squared?: boolean
borderSpacing?: number
}
export default function FieldMinimap(props: Props) {
const { field, squared, borderSpacing } = props
const MAPBOX_TOKEN = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;
const [geoCollection, setGeoCollection] = useState<FeatureCollection>();
useEffect(()=>{
let fieldData = field.settings.fieldGeoData
if(fieldData){
let fieldFeature = GeometryMapping.geoJSON(fieldData.geoShape, fieldData.shapes, fieldData.holes) as Feature;
fieldFeature.id = fieldData.objectKey;
fieldFeature.properties = {
title: fieldData.title,
objectKey: fieldData.objectKey,
fill: fieldData.colour,
lineWidth: fieldData.geoShape === "LineString" ? 5 : 2,
origin: fieldData.origin
};
let collection: FeatureCollection = {
type: "FeatureCollection",
features: [fieldFeature]
};
setGeoCollection(collection);
}
},[field])
return geoCollection ?
(
<Map
zoom={1}
maxBounds={
field.fieldBounds(borderSpacing, squared)
}
reuseMaps
style={{width: "100%", height: "100%"}}
mapboxAccessToken={MAPBOX_TOKEN}
mapStyle="mapbox://styles/mapbox/satellite-streets-v11"
>
<GeoMapLayer
objectCollection={geoCollection}
/>
</Map>
)
:
(<CircularProgress />)
}

View file

@ -0,0 +1,201 @@
import { Box, Button, Checkbox, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, FormGroup, FormLabel, Grid2, Stack, Switch, TextField } from "@mui/material";
import ResponsiveDialog from "common/ResponsiveDialog";
import { cloneDeep } from "lodash";
import { pond } from "protobuf-ts/pond";
import { useFieldAPI, useSnackbar } from "providers";
import React, { useEffect, useState } from "react";
import TeamSearch from "teams/TeamSearch";
interface Props {
open: boolean
close: () => void
}
export default function ShareAllFields(props: Props) {
const {open, close} = props
const [teamShare, setTeamShare] = useState(false)
const fieldAPI = useFieldAPI();
const { openSnack } = useSnackbar()
const [team, setTeam] = useState<string>("")
const [user, setUser] = useState<string>("")
const [sharedPermissions, setSharedPermissions] = useState<pond.Permission[]>([
pond.Permission.PERMISSION_READ
])
const share = () => {
if(teamShare){
//if sharing to team use shareAllByKey
fieldAPI.shareAllByKey(team, sharedPermissions)
.then(resp => {
openSnack("Shared all fields to team")
}).catch(err => {
openSnack("There was a problem sharing the fields")
})
}else{
//if sharing to user use shareAll
fieldAPI.shareAll(user, sharedPermissions)
.then(resp => {
openSnack("Shared all fields to user")
}).catch(err => {
openSnack("There was a problem sharing the fields")
})
}
}
const target = () => {
// toggle for whether sharing to user or team
return (
<React.Fragment>
<Grid2 container alignItems="center">
<Grid2>User</Grid2>
<Grid2>
<Switch
color="default"
value={teamShare}
checked={teamShare}
onChange={(_, checked) => {
setTeamShare(checked)
}}
name="storage"
/>
</Grid2>
<Grid2>Team</Grid2>
</Grid2>
{teamShare ?
<Box>
<TeamSearch label="Team" setTeamCallback={(teamKey) => {setTeam(teamKey)}}/>
</Box> :
<Box>
<TextField
label="User Email"
fullWidth
value={user}
onChange={(e) => {
setUser(e.target.value)
}}
/>
</Box>}
</React.Fragment>
)
// if sharing to user have a text field
}
const changePermissions = (checked: boolean, permission: pond.Permission) => {
let currentPerms = cloneDeep(sharedPermissions)
if(checked){
//if the permissions does not include the permission add it
if(!sharedPermissions.includes(permission)) currentPerms.push(permission)
}else{
//if the permissions includes the permission remove it
if(sharedPermissions.includes(permission)) currentPerms.splice(currentPerms.indexOf(permission), 1)
}
setSharedPermissions(currentPerms)
}
const permissions = () => {
// the checkboxes of permissions
return (
<React.Fragment>
<FormControl sx={{marginTop: 2}} component="fieldset" fullWidth variant="outlined">
<FormLabel component="legend">Permissions</FormLabel>
<FormGroup>
<FormControlLabel
control={
<Checkbox
disabled={sharedPermissions.includes(pond.Permission.PERMISSION_READ)}
checked={sharedPermissions.includes(pond.Permission.PERMISSION_READ)}
onChange={(_, checked) => {changePermissions(checked, pond.Permission.PERMISSION_READ)}}
value={pond.Permission.PERMISSION_READ as pond.Permission}
/>
}
label="View"
labelPlacement="end"
/>
<FormControlLabel
control={
<Checkbox
checked={sharedPermissions.includes(pond.Permission.PERMISSION_WRITE)}
onChange={(_, checked) => {changePermissions(checked, pond.Permission.PERMISSION_WRITE)}}
value={pond.Permission.PERMISSION_WRITE as pond.Permission}
/>
}
label="Edit"
labelPlacement="end"
/>
<FormControlLabel
control={
<Checkbox
checked={sharedPermissions.includes(pond.Permission.PERMISSION_SHARE)}
onChange={(_, checked) => {changePermissions(checked, pond.Permission.PERMISSION_SHARE)}}
value={pond.Permission.PERMISSION_SHARE as pond.Permission}
/>
}
label="Share"
labelPlacement="end"
/>
<FormControlLabel
control={
<Checkbox
checked={sharedPermissions.includes(pond.Permission.PERMISSION_USERS)}
onChange={(_, checked) => {changePermissions(checked, pond.Permission.PERMISSION_USERS)}}
value={pond.Permission.PERMISSION_USERS as pond.Permission}
/>
}
label="Users"
labelPlacement="end"
/>
<FormControlLabel
control={
<Checkbox
checked={sharedPermissions.includes(pond.Permission.PERMISSION_FILE_MANAGEMENT)}
onChange={(_, checked) => {changePermissions(checked, pond.Permission.PERMISSION_FILE_MANAGEMENT)}}
value={pond.Permission.PERMISSION_FILE_MANAGEMENT as pond.Permission}
/>
}
label="Files"
labelPlacement="end"
/>
{/* billing */}
<FormControlLabel
control={
<Checkbox
checked={sharedPermissions.includes(pond.Permission.PERMISSION_BILLING)}
onChange={(_, checked) => {changePermissions(checked, pond.Permission.PERMISSION_BILLING)}}
value={pond.Permission.PERMISSION_BILLING as pond.Permission}
/>
}
label="Billing"
labelPlacement="end"
/>
</FormGroup>
</FormControl>
</React.Fragment>
)
}
const actions = () => {
//the buttons to shaare or cancel
return (
<React.Fragment>
<Button onClick={close}>Close</Button>
<Button onClick={share} variant="contained" color="primary">Share</Button>
</React.Fragment>
)
}
return (
<ResponsiveDialog open={open} onClose={close}>
<DialogTitle>Share All Fields</DialogTitle>
<DialogContent>
<Box minWidth={300}>
{target()}
{permissions()}
</Box>
</DialogContent>
<DialogActions>
{actions()}
</DialogActions>
</ResponsiveDialog>
)
}

View file

@ -249,9 +249,9 @@ export default function GateSVG(props: Props) {
<g key={"finalTemp"} id={"finalTemp"} data-name={"finalTemp"}>
<text fontSize={7} className={classes.fontBase} x={100} y={85}>
{convertFinalTemp(finalTemp).toFixed(2)}°
{user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS
? "C"
: "F"}
{user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT
? "°F"
: "°C"}
</text>
</g>
);
@ -260,9 +260,9 @@ export default function GateSVG(props: Props) {
<g key={"ambientTemp"} id={"ambientTemp"} data-name={"ambientTemp"}>
<text fontSize={7} className={classes.fontBase} x={30} y={114}>
Ambient: {ambientTemp}°
{user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS
? "C"
: "F"}
{user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT
? "°F"
: "°C"}
</text>
</g>
);
@ -271,13 +271,13 @@ export default function GateSVG(props: Props) {
<g key={"tempChain"} id={"tempChain"} data-name={"tempChain"}>
<text fontSize={7} className={classes.fontBase} x={30} y={149}>
T1: {innerTemps.t1}°
{user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS
? "C"
: "F"}
{user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT
? "°F"
: "°C"}
, T2: {innerTemps.t2}°
{user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS
? "C"
: "F"}
{user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT
? "°F"
: "°C"}
</text>
</g>
);

View file

@ -7,16 +7,17 @@ import {
TextField
} from "@mui/material";
import ResponsiveDialog from "common/ResponsiveDialog";
import { Option } from "common/SearchSelect";
import { Field, HarvestPlan, Task } from "models";
import moment from "moment";
import { pond } from "protobuf-ts/pond";
import { useGlobalState, useHarvestPlanAPI, useTaskAPI } from "providers";
import { useFieldAPI, useGlobalState, useHarvestPlanAPI, useTaskAPI } from "providers";
import { useCallback, useEffect, useState } from "react";
interface Props {
open: boolean;
close: () => void;
fields: Field[];
fields?: Field[];
plan: HarvestPlan;
}
@ -29,10 +30,14 @@ export default function DuplicateHarvestPlan(props: Props) {
const harvestPlanAPI = useHarvestPlanAPI();
const taskAPI = useTaskAPI();
const [ready, setReady] = useState(false);
const [fieldOptions, setFieldOptions] = useState<Field[]>([])
const fieldAPI = useFieldAPI();
const loadTasks = useCallback(() => {
taskAPI.listTasks(50, 0, "asc", "start", plan.key(), undefined).then(resp => {
setPlanTasks(resp.data.tasks.map(t => Task.any(t)));
if(resp.data.tasks){
setPlanTasks(resp.data.tasks.map(t => Task.any(t)));
}
setReady(true);
});
}, [taskAPI, plan]);
@ -41,9 +46,25 @@ export default function DuplicateHarvestPlan(props: Props) {
loadTasks();
}, [loadTasks]);
useEffect(()=>{
if(fields){
//if fields were passed in use those
setFieldOptions(fields)
}else{
//otherwise load them
fieldAPI.listFields(50, 0, "asc", "fieldName").then(resp => {
setFieldOptions(resp.data.fields.map(f => Field.create(f)))
}).catch(err => {
})
}
},[fields])
//this should load the fields here
const duplicate = () => {
let planSettings = HarvestPlan.clone(plan).settings;
planSettings.field = fields[newFieldIndex].key();
planSettings.field = fieldOptions[newFieldIndex].key();
planSettings.createDate = moment.now();
if (title !== "") {
planSettings.title = title;
@ -69,7 +90,7 @@ export default function DuplicateHarvestPlan(props: Props) {
value={newFieldIndex}
onChange={e => setNewFieldIndex(+e.target.value)}
color="primary">
{fields.map((option, i) => (
{fieldOptions.map((option, i) => (
<MenuItem key={option.key()} value={i}>
{option.fieldName()}
</MenuItem>
@ -98,8 +119,8 @@ export default function DuplicateHarvestPlan(props: Props) {
color="primary"
disabled={
!ready ||
(fields[newFieldIndex] &&
!fields[newFieldIndex].permissions.includes(pond.Permission.PERMISSION_WRITE))
(fieldOptions[newFieldIndex] &&
!fieldOptions[newFieldIndex].permissions.includes(pond.Permission.PERMISSION_WRITE))
}>
Duplicate
</Button>

View file

@ -13,7 +13,7 @@ import ShareObjectIcon from "@mui/icons-material/Share";
import ObjectUsersIcon from "@mui/icons-material/AccountCircle";
import ObjectTeamsIcon from "@mui/icons-material/SupervisedUserCircle";
import { pond } from "protobuf-ts/pond";
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import ObjectUsers from "user/ObjectUsers";
import ShareObject from "user/ShareObject";
import { isOffline } from "utils/environment";
@ -43,7 +43,7 @@ const useStyles = makeStyles((theme: Theme) => ({
interface Props {
plan: HarvestPlan;
planField: Field;
fieldList: Field[];
fieldList?: Field[];
permissions: pond.Permission[];
refreshCallback: (updatedPlan?: HarvestPlan) => void;
hideNew?: boolean;
@ -215,7 +215,7 @@ export default function HarvestPlanActions(props: Props) {
setUpdatePlan(true);
setOpenState({ ...openState, settings: true });
}}>
<Edit /> Edit or Add Activity
<Edit /> Activities
</Button>
)}
<IconButton

View file

@ -10,7 +10,7 @@ import {
import { Notes } from "@mui/icons-material";
import GrainDescriber from "grain/GrainDescriber";
import { Field, HarvestPlan } from "models";
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { getThemeType } from "theme";
import Chat from "chat/Chat";
import { pond } from "protobuf-ts/pond";
@ -23,7 +23,7 @@ interface Props {
permissions: pond.Permission[];
planField: Field;
loading: boolean;
fieldList: Field[];
fieldList?: Field[];
changePlan: (updatedPlan?: HarvestPlan) => void;
}
@ -175,7 +175,7 @@ export default function HarvestPlanDisplay(props: Props) {
<Typography
variant="h5"
style={{ fontWeight: 700, marginTop: "auto", marginBottom: "auto" }}>
Crop Plan{plan.key() !== "" ? " - " + plan.settings.title : " - None Found"}
{plan.key() !== "" ? plan.settings.title : "None Found"}
</Typography>
{!isMobile && permissions.includes(pond.Permission.PERMISSION_WRITE) && (
<HarvestPlanActions

View file

@ -0,0 +1,202 @@
import { ExpandMore } from "@mui/icons-material"
import { Accordion, AccordionDetails, AccordionSummary, Box, Theme, Typography } from "@mui/material"
import { makeStyles } from "@mui/styles"
import ResponsiveTable, { Column } from "common/ResponsiveTable"
import GrainDescriber from "grain/GrainDescriber"
import { cloneDeep } from "lodash"
import { Field, HarvestPlan } from "models"
import { useHarvestPlanAPI } from "providers"
import { useCallback, useEffect, useState } from "react"
import { getThemeType } from "theme"
interface Props {
field: Field
}
const useStyles = makeStyles((theme: Theme) => ({
dark: {
backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(50, 50, 50)",
padding: 5
},
light: {
backgroundColor: getThemeType() === "light" ? "rgb(235, 235, 235)" : "rgb(60, 60, 60)",
padding: 5
}
}));
export default function HarvestPlanTable(props: Props){
const {field} = props
const harvestAPI = useHarvestPlanAPI()
const [page, setPage] = useState(0)
const [pageSize, setPageSize] = useState(10)
const [loadedPlans, setLoadedPlans] = useState<HarvestPlan[]>([])
const [total, setTotal] = useState(0)
const classes = useStyles()
useEffect(()=>{
harvestAPI.listHarvestPlans(pageSize, page*pageSize, "desc", "harvestYear", field.key()).then(resp => {
if(resp.data.harvestPlan){
setLoadedPlans(resp.data.harvestPlan.map(hp => HarvestPlan.create(hp)))
}
setTotal(resp.data.total)
})
},[page, pageSize, field])
const columns: Column<HarvestPlan>[] = [
//these columns would be the ones on by default
//field - hideable
{
title: "Field",
hidden: field !== undefined,
render: (row) => <Typography align="center">{row.field()}</Typography>
},
//title
{
title: "Plan",
render: (row) => <Typography align="center">{row.settings.title}</Typography>
},
//year
{
title: "Year",
render: (row) => <Typography align="center">{row.harvestYear()}</Typography>
},
//grain - crop_type
{
title: "Grain",
render: (row) => <Typography align="center">{GrainDescriber(row.cropType()).name}</Typography>
},
//variant - grain_type
{
title: "Variant",
render: (row) => <Typography align="center">{row.grainType()}</Typography>
},
//yield target
{
title: "Yield Target (bu)",
render: (row) => <Typography align="right">{row.yieldTarget()}</Typography>
},
//actual yield
{
title: "Actual Yield (bu)",
render: (row) => <Typography align="right">{row.actualYield()}</Typography>
},
//bushel price
{
title: "Bushel Price",
render: (row) => <Typography align="right">{row.bushelPrice()}</Typography>
},
//material cost
{
title: "Material Cost",
render: (row) => <Typography align="right">{row.totalMaterialCost()}</Typography>
},
//equipment cost
{
title: "Equipment Cost",
render: (row) => <Typography align="right">{row.totalEquipmentCost()}</Typography>
}
//these columns could be optional (disabled by default but turned on later?)
//pre-seed materials
//pre-seed equpment
//seed materials
//seed equpment
//post-seed materials
//post-seed equpment
//harvest materials
//harvest equpment
//fall materials
//fall equpment
]
return (
<ResponsiveTable<HarvestPlan>
columns={columns}
title={"Harvest Plans"}
resizeable
page={page}
pageSize={pageSize}
rows={loadedPlans}
total={total}
handleRowsPerPageChange={(e)=>{
setPageSize(e.target.value)
}}
setPage={(page)=>{
setPage(page)
}}
loadMore={() => {
harvestAPI.listHarvestPlans(pageSize, loadedPlans.length, "desc", "harvestYear", field.key()).then(resp => {
let currentPlans = cloneDeep(loadedPlans)
let newPlans: HarvestPlan[] = []
if(resp.data.harvestPlan){
newPlans = resp.data.harvestPlan.map(hp => HarvestPlan.create(hp))
}
setLoadedPlans(currentPlans.concat(newPlans))
})
}}
renderMobile={(row, index) => {
// since these are more about previous harvest plans i am only showing the final tallys rather than the breakdowns
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography sx={{fontWeight: 650}}>
{row.settings.title} - {row.harvestYear()}
</Typography>
</AccordionSummary>
<AccordionDetails>
{/* grain */}
<Box className={classes.light} style={{ display: "flex", justifyContent: "space-between" }}>
<Typography variant="subtitle1">Grain:</Typography>
<Typography variant="subtitle1">{GrainDescriber(row.cropType()).name}</Typography>
</Box>
{/* variant */}
<Box className={classes.dark} style={{ display: "flex", justifyContent: "space-between" }}>
<Typography variant="subtitle1">Variant:</Typography>
<Typography variant="subtitle1">{row.grainType()}</Typography>
</Box>
{/* yield target */}
{/* <Box className={classes.dark} style={{ display: "flex", justifyContent: "space-between" }}>
<Typography variant="subtitle1">Yield Target:</Typography>
<Typography variant="subtitle1">{row.yieldTarget()}bu</Typography>
</Box> */}
{/* actual yield */}
<Box className={classes.light} style={{ display: "flex", justifyContent: "space-between" }}>
<Typography variant="subtitle1">Actual Yield:</Typography>
<Typography variant="subtitle1">{row.actualYield()}bu</Typography>
</Box>
{/* bushel price */}
<Box className={classes.dark} style={{ display: "flex", justifyContent: "space-between" }}>
<Typography variant="subtitle1">Bushel Price:</Typography>
<Typography variant="subtitle1">${row.bushelPrice()}</Typography>
</Box>
{/* material cost */}
<Box className={classes.light} style={{ display: "flex", justifyContent: "space-between" }}>
<Typography variant="subtitle1">Material Cost:</Typography>
<Typography variant="subtitle1">${row.totalMaterialCost() * field.acres()}</Typography>
</Box>
{/* equipment cost */}
<Box className={classes.dark} style={{ display: "flex", justifyContent: "space-between" }}>
<Typography variant="subtitle1">Equipment Cost:</Typography>
<Typography variant="subtitle1">${row.totalEquipmentCost() * field.acres()}</Typography>
</Box>
{/* pre-seed materials */}
{/* pre-seed equpment */}
{/* seed materials */}
{/* seed equpment */}
{/* post-seed materials */}
{/* post-seed equpment */}
{/* harvest materials */}
{/* harvest equpment */}
{/* fall materials */}
{/* fall equpment */}
{/* total cost */}
<Box className={classes.light} style={{ display: "flex", justifyContent: "space-between" }}>
<Typography variant="subtitle1">Total Cost:</Typography>
<Typography variant="subtitle1">${(row.totalEquipmentCost() + row.totalMaterialCost()) * field.acres()}</Typography>
</Box>
</AccordionDetails>
</Accordion>
)
}}
/>
)
}

View file

@ -210,7 +210,7 @@ export default function LibraCartAccess() {
</Grid>
<Box paddingTop={2}>
<Typography>
Libra Cart data will sync every 6 hours, if the data is needed immediately you can select the Sync button. It may take up to 15 minutes for the data to appear in our platform.
Libra Cart data will sync every 6 hours, if the data is needed immediately you can select the Sync button.
</Typography>
</Box>

View file

@ -241,7 +241,7 @@ export default function MapBase(props: Props) {
homeMarkerAPI
.listHomeMarkers(1, 0, undefined, undefined, undefined, undefined, as)
.then(resp => {
if (resp.data.homeMarker.length < 1) {
if (!resp.data.homeMarker || resp.data.homeMarker.length < 1) {
setHomePin(undefined);
setHaveHome(false);
setStartingView(props.currentView);
@ -271,6 +271,7 @@ export default function MapBase(props: Props) {
}
})
.catch(err => {
console.log(err)
openSnack("Could not load Home Pin");
});
}, [as, homeMarkerAPI, openSnack, props.ignoreHomeLoad, user]); // eslint-disable-line react-hooks/exhaustive-deps

View file

@ -273,7 +273,7 @@ export default function FieldDrawer(props: Props) {
<Weather longitude={field.center().longitude} latitude={field.center().latitude} />
</TabPanelMine>
<TabPanelMine value={value} index={2}>
<TaskViewer drawerView objectKey={field.key()} loadKeys={taskLoadKeys} />
<TaskViewer drawerView objectKey={field.key()} loadKeys={taskLoadKeys} overlayButton />
</TabPanelMine>
</React.Fragment>
);

View file

@ -6,7 +6,7 @@ interface Props {
objectCollection: FeatureCollection;
measurementCollection?: FeatureCollection;
showTitle?: boolean;
setInteractiveLayers: (layers: string[]) => void;
setInteractiveLayers?: (layers: string[]) => void;
}
export default function GeoMapLayer(props: Props) {
@ -18,7 +18,9 @@ export default function GeoMapLayer(props: Props) {
// }, [measurementCollection, objectCollection]);
useEffect(() => {
setInteractiveLayers(["shapefill", "shapeborder", "measurementBorder"]);
if(setInteractiveLayers){
setInteractiveLayers(["shapefill", "shapeborder", "measurementBorder"]);
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
return (
@ -60,6 +62,7 @@ export default function GeoMapLayer(props: Props) {
/>
)}
</Source>
{measurementCollection &&
<Source type="geojson" data={measurementCollection} id={"measurements"}>
<Layer
id="measurementBorder"
@ -90,6 +93,7 @@ export default function GeoMapLayer(props: Props) {
}}
/>
</Source>
}
</React.Fragment>
);
}

View file

@ -221,24 +221,26 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
.then(resp => {
let fields: Map<string, Field> = new Map();
let fieldEntries: Result[] = [];
resp.data.fields.forEach(f => {
let field = Field.any(f);
fields.set(field.key(), field);
fieldEntries.push({
id: field.key(),
place_name: field.name(),
place_type: ["field"],
center: [field.center().longitude, field.center().latitude],
address: "",
type: "Feature",
text: field.name(),
bbox: [0,0,0,0],
geometry: {type: "Point", coordinates: [field.center().longitude, field.center().latitude]},
context: [],
properties: {},
relevance: 0
if(resp.data.fields){
resp.data.fields.forEach(f => {
let field = Field.any(f);
fields.set(field.key(), field);
fieldEntries.push({
id: field.key(),
place_name: field.name(),
place_type: ["field"],
center: [field.center().longitude, field.center().latitude],
address: "",
type: "Feature",
text: field.name(),
bbox: [0,0,0,0],
geometry: {type: "Point", coordinates: [field.center().longitude, field.center().latitude]},
context: [],
properties: {},
relevance: 0
});
});
});
}
setFields(fields);
setFieldSearchEntries(fieldEntries);
})
@ -314,53 +316,56 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
let bagOp: BagModel[] = [];
let bagMarkerData: Map<string, MarkerData> = new Map();
let bagEntries: Result[] = [];
resp.data.grainBags.forEach(bag => {
let b = BagModel.create(bag);
bags.set(b.key(), b);
if (bag.settings) {
if (!b.settings.startLocation?.longitude && !b.settings.endLocation?.longitude) {
bagOp.push(b);
} else {
//build the data for the marker
//TODO-CS: think about making the creation of marker data a function in the object model
let mData: MarkerData = {
title: b.name(),
longitude: b.centerLocation().longitude,
latitude: b.centerLocation().latitude,
visibleLevels: { max: 13 },
colour: b.settings.theme?.color ?? "white",
clickFunc: (e, i, isMobile) => {
clickDelay();
closeDrawers();
setBagDrawer(true);
setIgnoreFeatures(true);
setObjectKey(b.key());
moveMap(
b.centerLocation().latitude,
b.centerLocation().longitude,
zoomLevels.far,
isMobile
);
}
};
bagMarkerData.set(b.key(), mData);
bagEntries.push({
center: [b.centerLocation().longitude, b.centerLocation().latitude],
id: b.key(),
place_name: b.name(),
place_type: ["grainBag"],
address: "",
bbox: [0,0,0,0],
context: [],
geometry: {type: "Point", coordinates: [b.centerLocation().longitude, b.centerLocation().latitude]},
properties: {},
relevance: 0,
text: "",
type: "Feature"
});
if(resp.data.grainBags){
resp.data.grainBags.forEach(bag => {
let b = BagModel.create(bag);
bags.set(b.key(), b);
if (bag.settings) {
if (!b.settings.startLocation?.longitude && !b.settings.endLocation?.longitude) {
bagOp.push(b);
} else {
//build the data for the marker
//TODO-CS: think about making the creation of marker data a function in the object model
let mData: MarkerData = {
title: b.name(),
longitude: b.centerLocation().longitude,
latitude: b.centerLocation().latitude,
visibleLevels: { max: 13 },
colour: b.settings.theme?.color ?? "white",
clickFunc: (e, i, isMobile) => {
clickDelay();
closeDrawers();
setBagDrawer(true);
setIgnoreFeatures(true);
setObjectKey(b.key());
moveMap(
b.centerLocation().latitude,
b.centerLocation().longitude,
zoomLevels.far,
isMobile
);
}
};
bagMarkerData.set(b.key(), mData);
bagEntries.push({
center: [b.centerLocation().longitude, b.centerLocation().latitude],
id: b.key(),
place_name: b.name(),
place_type: ["grainBag"],
address: "",
bbox: [0,0,0,0],
context: [],
geometry: {type: "Point", coordinates: [b.centerLocation().longitude, b.centerLocation().latitude]},
properties: {},
relevance: 0,
text: "",
type: "Feature"
});
}
}
}
});
});
}
setBagOptions(bagOp);
setGrainBags(bags);
grainBagsRef.current = bags;
@ -382,19 +387,21 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
binYardAPI
.listBinYards(400, 0, "asc", undefined, undefined, undefined, as)
.then(resp => {
resp.data.yard.forEach(yard => {
if (yard.settings) {
yardMap.set(yard.settings.key, yard.settings);
if (
(!yard.settings.latitude && !yard.settings.longitude) ||
(yard.settings.latitude === 0 && yard.settings.longitude === 0)
) {
yardOps.push(yard.settings);
} else {
let mData: MarkerData = {
title: yard.settings.name,
longitude: yard.settings.longitude,
latitude: yard.settings.latitude,
if(resp.data.yard){
resp.data.yard.forEach(yard => {
if (yard.settings) {
yardMap.set(yard.settings.key, yard.settings);
if (
(!yard.settings.latitude && !yard.settings.longitude) ||
(yard.settings.latitude === 0 && yard.settings.longitude === 0)
) {
yardOps.push(yard.settings);
} else {
let mData: MarkerData = {
title: yard.settings.name,
longitude: yard.settings.longitude,
latitude: yard.settings.latitude,
colour: yard.settings.theme?.color ?? "green",
markerIcon: <BinsIcon width={50 * 0.6} height={50 * 0.6} type="light" />,
visibleLevels: { max: 17 },
@ -429,6 +436,7 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
}
}
});
}
setYardMarkerData(newYardMarkers);
setYardSearchEntries(yardEntries);
setBinYards(yardMap);
@ -447,6 +455,9 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
let markerData: Map<string, MarkerData> = new Map();
let searchEntries: Result[] = [];
let binOp: IBin[] = [];
if(resp.data.bins){
resp.data.bins.forEach(bin => {
let b = IBin.create(bin);
binMap.set(b.key(), b);
@ -495,6 +506,7 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
}
}
});
}
setBinMarkerData(markerData);
setBinSearchEntries(searchEntries);
setBins(binMap);
@ -514,6 +526,9 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
let searchEntries: Result[] = [];
let options: DeviceModel[] = [];
if(resp.data.devices){
resp.data.devices.forEach((device, i) => {
//add device to the main map
let d = DeviceModel.any(device);
@ -553,6 +568,7 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
}
}
});
}
setDevices(map);
setDeviceOptions(options);
setDeviceMarkerData(newDevMarkers);
@ -725,6 +741,9 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
let fmMap: Map<string, FieldMarker> = new Map();
let markerData: Map<string, MarkerData> = new Map();
let searchEntries: Result[] = [];
if(resp.data.fieldMarker){
resp.data.fieldMarker.forEach(fieldMarker => {
let fm = FieldMarker.create(fieldMarker);
fmMap.set(fm.key(), fm);
@ -760,6 +779,7 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
place_type: ["fieldMarker"]
} as Result);
});
}
setFieldMarkers(fmMap);
setFieldMarkerData(markerData);
setFieldMarkerSearchEntries(searchEntries);

View file

@ -175,6 +175,7 @@ export default function AviationMapController(props: Props) {
let terminalMarkers: Map<string, MarkerData> = new Map();
let terminalOps: Terminal[] = [];
let searchEntries: Result[] = [];
if(resp.data.terminals){
resp.data.terminals.forEach(term => {
let terminal = Terminal.create(term);
terminalMap.set(terminal.key, terminal);
@ -207,6 +208,7 @@ export default function AviationMapController(props: Props) {
terminalOps.push(terminal);
}
});
}
setTerminals(terminalMap);
setTerminalMarkers(terminalMarkers);
setTerminalSearchEntries(searchEntries);

View file

@ -142,6 +142,7 @@ export default function ConstructionMapController(props: Props) {
siteAPI
.listSitesPageData(50, 0, "asc", "key", undefined, as)
.then(resp => {
if(resp.data.sites){
resp.data.sites.forEach(site => {
if (!site) return;
let newSite: Site = Site.any(site.site);
@ -179,6 +180,7 @@ export default function ConstructionMapController(props: Props) {
place_type: ["site"]
} as Result);
});
}
setSiteMarkerData(sm);
setSites(s);
setLoadingSites(false);

View file

@ -42,11 +42,11 @@ const featureVersions: Map<string, FeatureVersionByPlatform> = new Map([
v2Cell: "N/A",
v2WifiS3: "N/A",
v2CellS3: "N/A",
v2CellBlack: "2.1.6",
v2CellBlack: "2.1.7",
v2CellGreen: "N/A",
v2WifiBlue: "2.1.6",
v2CellBlue: "2.1.6",
v2EthBlue: "2.1.6"
v2WifiBlue: "2.1.7",
v2CellBlue: "2.1.7",
v2EthBlue: "2.1.7"
}
]
]);
@ -109,6 +109,35 @@ export class Device {
return m;
}
public platformName(): string {
switch (this.settings.platform) {
case pond.DevicePlatform.DEVICE_PLATFORM_PHOTON:
return "Photon"
case pond.DevicePlatform.DEVICE_PLATFORM_ELECTRON:
return "Electron"
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR:
return "V2 Cellular"
case pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI:
return "V2 Wifi"
case pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_S3:
return "V2 Wifi S3"
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_S3:
return "V2 Cellular S3"
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLACK:
return "V2 Cellular Black"
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_GREEN:
return "V2 Callular Green"
case pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_BLUE:
return "V2 Wifi Blue"
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLUE:
return "V2 Cellular Blue"
case pond.DevicePlatform.DEVICE_PLATFORM_V2_ETHERNET_BLUE:
return "V2 Ethernet Blue"
default:
return "";
}
}
public featureSupported(feature: string): boolean {
let versions = featureVersions.get(feature);
if (!versions) {

View file

@ -5,6 +5,7 @@ import area from "@turf/area";
import { Feature } from "geojson";
import { GeometryMapping } from "./GeometryMapping";
import GrainDescriber from "grain/GrainDescriber";
import { LngLat, LngLatBounds } from "mapbox-gl";
export class Field {
public settings: pond.FieldSettings = pond.FieldSettings.create();
@ -14,9 +15,11 @@ export class Field {
public static create(pf?: pond.Field): Field {
let my = new Field();
if (pf) {
my.settings = pond.FieldSettings.fromObject(cloneDeep(or(pf.settings, {})));
my.status = pond.FieldStatus.fromObject(cloneDeep(or(pf.status, {})));
my.permissions = pf.fieldPermissions;
//the from object is required here to deal with a protobuf quirk where enums would be seen as strings at runtime, but the code sees them as the actual value
let field = pond.Field.fromObject(cloneDeep(or(pf, pond.Field.create())))
my.settings = field.settings ? field.settings : pond.FieldSettings.create()
my.status = field.status ? field.status : pond.FieldStatus.create()
my.permissions = field.fieldPermissions
}
return my;
}
@ -152,6 +155,60 @@ export class Field {
return coords;
}
/**
* Returns a LngLatBounds object containing the southwest and northeast corners of a box to contain the field, spacing can also be provided
* to pad the sides of the bounding area
* @param spacing number(optional) - an optional paramater that will expand the bounding area by the given long lat amount
* @param squared boolean(optional) - when true will adjust the bounds by extending the narrower edge to match the wider edge
* @returns LngLatBounds - an object containing the southwest and northeast coordinates of a bounding box
*/
public fieldBounds(spacing?: number, squared?: boolean): LngLatBounds {
let minLong = 0;
let minLat = 0;
let maxLong = 0;
let maxLat = 0;
if (this.settings.fieldGeoData) {
this.settings.fieldGeoData.shapes.forEach(shape => {
shape.points.forEach(pair => {
if (pair.longitude < minLong || minLong === 0) minLong = pair.longitude;
if (pair.longitude > maxLong || maxLong === 0) maxLong = pair.longitude;
if (pair.latitude < minLat || minLat === 0) minLat = pair.latitude;
if (pair.latitude > maxLat || maxLat === 0) maxLat = pair.latitude;
});
});
}
// let southWest = [minLong,minLat]
// let northEast = [maxLong,maxLat]
if (squared){
//make sure the bounds are squared so as to fit the entire field in the map
//get the diff between the longs and the lats
let longDiff = maxLong - minLong
let latDiff = maxLat - minLat
//find which is narrower
if(longDiff > latDiff){
//if the longitude is the narrow bound apply half of the diff between the diffs to the min and max lats
let halfDiff = (longDiff - latDiff)/2
minLat = minLat - halfDiff
maxLat = maxLat + halfDiff
} else {
//if the longitude is not the narrow bound apply half of the diff between the diffs to the min and max longs
let halfDiff = (latDiff - longDiff)/2
minLong = minLong - halfDiff
maxLong = maxLong + halfDiff
}
}
if(spacing){
minLong = minLong - spacing
minLat = minLat - spacing
maxLong = maxLong + spacing
maxLat = maxLat + spacing
}
return new LngLatBounds(
new LngLat(minLong, minLat),
new LngLat(maxLong, maxLat)
);
}
public grainName(): string {
if (this.grain() !== pond.Grain.GRAIN_INVALID) {
if (this.grain() === pond.Grain.GRAIN_CUSTOM) {

View file

@ -47,6 +47,7 @@ const Contract = lazy(() => import("pages/Contract"));
const JohnDeere = lazy(() => import("pages/JohnDeere"));
const CNHi = lazy(() => import("pages/CNHi"));
const LibraCart = lazy(() => import("pages/LibraCart"));
const FieldPage = lazy(()=>import("pages/Field"));
export const appendToUrl = (appendage: number | string) => {
const basePath = location.pathname.replace(/\/$/, "");
@ -70,6 +71,7 @@ export default function Router() {
<Route path="terminals/*" element={<TerminalGatesRoute />} />
<Route path="jobsites/*" element={<JobsitesRoute />} />
<Route path="heaters/*" element={<ObjectHeatersRoute />} />
<Route path="fields/*" element={<FieldsRoute />} />
</Routes>
)
}
@ -284,6 +286,21 @@ export default function Router() {
);
};
const FieldsRoute = () => {
return (
<Routes>
<Route
path=""
element={<Fields />}
/>
<Route
path="/:fieldID"
element={<FieldPage />}
/>
</Routes>
);
}
if (isLoading) return null;
// if (!isAuthenticated) {
// loginWithRedirect()
@ -325,7 +342,6 @@ export default function Router() {
{user.hasFeature("admin") &&
<Route path="logs" element={<Logs />} />
}
<Route path="fields" element={<Fields />} />
<Route path="marketplace" element={<Marketplace />} />
{user.hasFeature("admin") && (
<Route path="firmware" element={<Firmware />} />

View file

@ -208,6 +208,118 @@ export default function Devices() {
})
}, [tab])
function compareTimestamps(timestamp1: string, timestamp2: string): number {
const date1 = new Date(timestamp1);
const date2 = new Date(timestamp2);
// Check if dates are valid
if (isNaN(date1.getTime()) || isNaN(date2.getTime())) {
throw new Error("Invalid RFC3339 timestamp format");
}
// Compare using getTime() for millisecond precision
return date1.getTime() - date2.getTime();
}
const doesDeviceHavePlenum = (device: pond.Device) => {
if (device.status?.plenum?.temperature && device.status?.plenum?.timestamp) {
if (device.status.plenum?.timestamp) {
const now = new Date();
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
if (compareTimestamps(device.status.plenum?.timestamp, oneDayAgo.toISOString()) < 0) {
return true
}
} else {
return false
}
}
return false
}
const doesDeviceHaveSen5x = (device: pond.Device) => {
if (device.status?.sen5x?.temperature && device.status?.sen5x?.timestamp) {
if (device.status.sen5x?.timestamp) {
const now = new Date();
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
if (compareTimestamps(device.status.sen5x?.timestamp, oneDayAgo.toISOString()) < 0) {
return true
}
} else {
return false
}
}
return false
}
// if (device.status?.o2) setHasO2(true)
// if (device.status?.co) setHasCo(true)
// if (device.status?.co2) setHasCo2(true)
// if (device.status?.no2) setHasNo2(true)
const doesDeviceHaveO2 = (device: pond.Device) => {
if (device.status?.o2?.ppm && device.status?.o2?.timestamp) {
if (device.status.sen5x?.timestamp) {
const now = new Date();
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
if (compareTimestamps(device.status.o2?.timestamp, oneDayAgo.toISOString()) < 0) {
return true
}
} else {
return false
}
}
return false
}
const doesDeviceHaveCo = (device: pond.Device) => {
if (device.status?.co?.ppm && device.status?.co?.timestamp) {
if (device.status.co?.timestamp) {
const now = new Date();
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
if (compareTimestamps(device.status.co?.timestamp, oneDayAgo.toISOString()) < 0) {
return true
}
} else {
return false
}
}
return false
}
const doesDeviceHaveCo2 = (device: pond.Device) => {
if (device.status?.co2?.ppm && device.status?.co2?.timestamp) {
if (device.status.co2?.timestamp) {
const now = new Date();
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
if (compareTimestamps(device.status.co2?.timestamp, oneDayAgo.toISOString()) < 0) {
return true
}
} else {
return false
}
}
return false
}
const doesDeviceHaveNo2 = (device: pond.Device) => {
if (device.status?.no2?.ppm && device.status?.no2?.timestamp) {
if (device.status.no2?.timestamp) {
const now = new Date();
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
if (compareTimestamps(device.status.no2?.timestamp, oneDayAgo.toISOString()) < 0) {
return true
}
} else {
return false
}
}
return false
}
useEffect(() => {
console.log(hasPlenums)
}, [hasPlenums])
const openProvisionDialog = () => {
setIsProvisionDialogOpen(true);
};
@ -319,12 +431,13 @@ export default function Devices() {
).then(resp => {
let newDevices: Device[] = []
resp.data.devices.forEach(device => {
if (device.status?.plenum?.temperature) setHasPlenums(true)
if (device.status?.sen5x?.temperature) setHasSen5x(true)
if (device.status?.o2) setHasO2(true)
if (device.status?.co) setHasCo(true)
if (device.status?.co2) setHasCo2(true)
if (device.status?.no2) setHasNo2(true)
setHasPlenums(doesDeviceHavePlenum(device))
// setHasPlenums(true)
setHasSen5x(doesDeviceHaveSen5x(device))
setHasO2(doesDeviceHaveO2(device))
setHasCo(doesDeviceHaveCo(device))
setHasCo2(doesDeviceHaveCo2(device))
setHasNo2(doesDeviceHaveNo2(device))
newDevices.push(Device.create(device))
})
setDevices(newDevices)
@ -809,12 +922,12 @@ export default function Devices() {
).then(resp => {
let newDevices: Device[] = []
resp.data.devices.forEach(device => {
if (device.status?.plenum?.temperature) setHasPlenums(true)
if (device.status?.sen5x?.temperature) setHasSen5x(true)
if (device.status?.o2) setHasO2(true)
if (device.status?.co) setHasCo(true)
if (device.status?.co2) setHasCo2(true)
if (device.status?.no2) setHasNo2(true)
setHasPlenums(doesDeviceHavePlenum(device))
setHasSen5x(doesDeviceHaveSen5x(device))
setHasO2(doesDeviceHaveO2(device))
setHasCo(doesDeviceHaveCo(device))
setHasCo2(doesDeviceHaveCo2(device))
setHasNo2(doesDeviceHaveNo2(device))
newDevices.push(Device.create(device))
})
setDevices(currentRows.concat(newDevices))

314
src/pages/Field.tsx Normal file
View file

@ -0,0 +1,314 @@
import { Field, fieldScope, HarvestPlan, teamScope } from "models";
import { useFieldAPI, useGlobalState, useHarvestPlanAPI } from "providers";
import React, { useCallback, useEffect, useState } from "react";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import PageContainer from "./PageContainer";
import { useMobile, useSnackbar, useUserAPI } from "hooks";
import { Box, Card, Grid2, IconButton, Tab, Tabs, Theme, Typography, darken } from "@mui/material";
import FieldActions from "field/FieldActions";
import { Settings } from "@mui/icons-material";
import { pond } from "protobuf-ts/pond";
import FieldMinimap from "field/Fieldminimap";
import Weather from "weather/weather";
import TaskViewer from "tasks/TaskViewer";
import HarvestPlanDisplay from "harvestPlan/HarvestPlanDisplay";
import { cloneDeep } from "lodash";
import HarvestPlanTable from "harvestPlan/HarvestPlanTable";
import { makeStyles } from "@mui/styles";
import FieldSettings from "field/FieldSettings";
const useStyles = makeStyles((theme: Theme) => {
return ({
card: {
height: "100%",
bgcolor: darken(theme.palette.background.paper, 0.05),
},
sectionHeader: {
fontSize: 35,
fontWeight: 650
}
})
})
interface TabPanelProps {
children?: React.ReactNode;
index: any;
value: any;
}
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
aria-labelledby={`simple-tab-${index}`}
style={{ height: "94%", paddingTop: "15px" }}
{...other}>
{value === index && <React.Fragment>{children}</React.Fragment>}
</div>
);
}
export default function FieldPage() {
const { state } = useLocation();
//const [{ as }] = useGlobalState()
const classes = useStyles();
const fieldAPI = useFieldAPI();
const userAPI = useUserAPI();
const [{as, user}] = useGlobalState();
const {openSnack} = useSnackbar();
const fieldID = useParams<{ fieldID: string }>()?.fieldID ?? "";
const [field, setField] = useState<Field>(state?.field ? Field.create(state.field) : Field.create())
const [permissions, setPermissions] = useState<pond.Permission[]>(state?.permissions ? state.permissions : [])
const isMobile = useMobile()
const [openFieldSettings, setOpenFieldSettings] = useState(false)
const [planLoading, setPlanLoading] = useState(false)
const [hPlan, setHPlan] = useState<HarvestPlan>(HarvestPlan.create());
const [loading, setLoading] = useState(false)
const hPlanAPI = useHarvestPlanAPI();
const [tabVal, setTabVal] = useState(0)
const navigate = useNavigate()
const loadField = useCallback(() => {
if(state.field){
fieldAPI.getField(state.field).then(resp => {
setField(Field.create(resp.data))
}).catch(err => {
})
}
},[as])
const goTo = () => {
navigate("/fields");
}
useEffect(()=>{
// if already loading do nothing
if(loading) return
//if the field is passed through in the state just use it
if(state?.field){
let field = Field.create(state.field)
field.permissions = state.permissions
setField(field)
setPermissions(state.permissions)
return
}
//otherwise load the field
setLoading(true)
fieldAPI.getField(fieldID, as).then(resp => {
setField(Field.create(resp.data))
}).catch(err => {
openSnack("Failed to load Field")
}).finally(() => {
setLoading(false)
})
//also get the permissions
let scope = fieldScope(fieldID);
if (as) {
scope = teamScope(as)
}
userAPI.getUser(user.id(), scope).then(resp => {
let f = cloneDeep(field)
f.permissions = resp.permissions
setField(f)
setPermissions(resp.permissions);
});
},[loadField, fieldID, as])
useEffect(() => {
if (field.key() !== "") {
hPlanAPI
.listHarvestPlans(1, 0, "desc", "createDate", field.key(), as)
.then(resp => {
if (resp.data.harvestPlan.length > 0) {
let plan = resp.data.harvestPlan[0];
setHPlan(HarvestPlan.any(plan));
} else {
setHPlan(HarvestPlan.create());
}
setPlanLoading(false);
})
.catch(err => {
//openSnack("Failed to load plan");
});
}
}, [field, as, hPlanAPI]);
const fieldActions = () => {
return (
<Grid2 container direction={"row"} justifyContent="space-between" alignContent="center" alignItems="center">
<Grid2>
<Typography className={classes.sectionHeader}>
{field.name()}
</Typography>
</Grid2>
<Grid2>
<IconButton
onClick={() => {
setOpenFieldSettings(true)
}}><Settings /></IconButton>
<FieldActions field={field} permissions={permissions} refreshCallback={()=>{}}/>
</Grid2>
</Grid2>
)
}
const map = () => {
return (
<Box minHeight={550} height="100%">
<FieldMinimap field={field} borderSpacing={0.001} squared/>
</Box>
)
}
const weather = () => {
return (<Weather longitude={field.center().longitude} latitude={field.center().latitude} />)
}
const harvestPlans = () => {
return (
<HarvestPlanDisplay
plan={hPlan}
permissions={permissions}
planField={field}
loading={planLoading}
changePlan={updatedPlan => {
if (updatedPlan) {
setHPlan(updatedPlan);
}
}}
/>)
}
const tasks = () => {
let taskLoadKeys: string[] = [];
if (!planLoading) {
field.key() !== "" && taskLoadKeys.push(field.key());
//hPlan.key() !== "" && taskLoadKeys.push(hPlan.key());
}
return (<TaskViewer drawerView objectKey={field.key()} loadKeys={taskLoadKeys} overlayButton={isMobile}/>)
}
const desktopView = () => {
return (
<React.Fragment>
<Box>
<Grid2 container spacing={1}>
<Grid2 size={4}>
<Card raised className={classes.card}>
{map()}
</Card>
</Grid2>
<Grid2 size={3}>
<Card raised className={classes.card}>
{weather()}
</Card>
</Grid2>
<Grid2 size={5}>
<Card raised className={classes.card}>
{tasks()}
</Card>
</Grid2>
</Grid2>
</Box>
<Box>
<Typography className={classes.sectionHeader}>
Harvest Overview
</Typography>
<Grid2 container spacing={1}>
<Grid2 size={4}>
<Card raised className={classes.card}>
{harvestPlans()}
</Card>
</Grid2>
<Grid2 size={8}>
<Card raised>
<HarvestPlanTable field={field}/>
</Card>
</Grid2>
</Grid2>
</Box>
</React.Fragment>
)
}
const mobileView = () => {
return (
<React.Fragment>
<Tabs
value={tabVal}
onChange={(_, val) => {
setTabVal(val)
}}
TabIndicatorProps={{ style: { background: "rgba(255,255,0,255)" } }}>
<Tab label="Overview" />
<Tab label="Activities" />
<Tab label="Harvest" />
</Tabs>
<TabPanel value={tabVal} index={0}>
<Grid2 container spacing={2} direction="column">
<Grid2 size={12}>
<Card raised sx={{height: "50vh"}}>
{map()}
</Card>
</Grid2>
<Grid2 size={12}>
<Card raised>
{weather()}
</Card>
</Grid2>
</Grid2>
</TabPanel>
<TabPanel value={tabVal} index={1}>
<Card raised>
{tasks()}
</Card>
</TabPanel>
<TabPanel value={tabVal} index={2}>
<Grid2>
<Grid2 size={12}>
<Card raised className={classes.card}>
{harvestPlans()}
</Card>
</Grid2>
<Grid2>
<HarvestPlanTable field={field}/>
</Grid2>
</Grid2>
</TabPanel>
</React.Fragment>
)
}
const fieldSettings = () => {
return (
<FieldSettings
open={openFieldSettings}
onClose={() => {
setOpenFieldSettings(false)
}}
selectedField={field}
updateFields={() => {
loadField()
}}
removeField={()=>{
goTo()
}}
/>
)
}
return (
<PageContainer>
<Box padding={2}>
{fieldActions()}
{isMobile ? mobileView() : desktopView()}
</Box>
{fieldSettings()}
</PageContainer>
)
}

View file

@ -39,8 +39,9 @@ export default function Heaters() {
objectHeaterAPI
.listObjectHeatersPageData(pageSize, tablePage * pageSize, undefined, undefined, searchText, as)
.then(resp => {
// setHeaters(resp.data.heaterData ?? []);
setTotal(resp.data.total)
setHeaters(resp.data.heaterData);
setHeaters(resp.data.heaterData ?? []);
});
}, [objectHeaterAPI, as, pageSize, tablePage, searchText]);

View file

@ -6,7 +6,7 @@ export default function Tasks() {
return (
<PageContainer>
<Box padding={2}>
<TaskViewer />
<TaskViewer overlayButton/>
</Box>
</PageContainer>
);

View file

@ -182,7 +182,9 @@ export default function Terminals(props: Props) {
if(loading) return
setLoading(true)
terminalAPI.listTerminals(200, 0, "asc", "name", as).then(resp => {
setTerminals(resp.data.terminals.map(a => Terminal.any(a)));
if(resp.data.terminals){
setTerminals(resp.data.terminals.map(a => Terminal.any(a)));
}
}).catch(err => {
console.log("There was a problem loading Terminals")
}).finally(() => {

View file

@ -108,7 +108,8 @@ export default function Users() {
"remove-devices",
"copy-token",
"recluse",
"pause-data"
"pause-data",
"access-object"
].sort();
const features = [
"admin",

View file

@ -5,10 +5,11 @@ import { useGlobalState } from "providers";
import React, { createContext, PropsWithChildren, useContext } from "react";
import { or } from "utils";
import { pondURL } from "./pond";
import { permissionToString } from "pbHelpers/Permission";
export interface IFieldAPIContext {
addField: (field: pond.FieldSettings, otherTeam?: string) => Promise<any>;
getField: (fieldId: string, otherTeam?: string) => Promise<any>;
getField: (fieldId: string, otherTeam?: string) => Promise<AxiosResponse<pond.Field>>;
listFields: (
limit: number,
offset: number,
@ -24,7 +25,15 @@ export interface IFieldAPIContext {
field: pond.FieldSettings,
asRoot?: true,
otherTeam?: string
) => Promise<AxiosResponse<pond.UpdateSiteResponse>>;
) => Promise<AxiosResponse<pond.UpdateFieldResponse>>;
shareAll: (
email: string,
permissions: pond.Permission[],
) => Promise<AxiosResponse<any>>
shareAllByKey: (
key: string,
permissions: pond.Permission[]
) => Promise<AxiosResponse<any>>
}
export const FieldAPIContext = createContext<IFieldAPIContext>({} as IFieldAPIContext);
@ -44,8 +53,8 @@ export default function FieldProvider(props: PropsWithChildren<Props>) {
const getField = (fieldId: string, otherTeam?: string) => {
const view = otherTeam ? otherTeam : as
if (view) return get(pondURL("/field/" + fieldId + "?as=" + view));
return get(pondURL("/field/" + fieldId));
if (view) return get<pond.Field>(pondURL("/field/" + fieldId + "?as=" + view));
return get<pond.Field>(pondURL("/field/" + fieldId));
};
const removeField = (key: string, otherTeam?: string) => {
@ -93,6 +102,36 @@ export default function FieldProvider(props: PropsWithChildren<Props>) {
);
};
const shareAll = (email: string, permissions: pond.Permission[]) => {
let url = pondURL("/shareAllFields")
if (as) url = url + "?as=" + as
return new Promise<AxiosResponse>((resolve, reject) => {
post(url, {
email: email,
permissions: permissions.map(permission => permissionToString(permission))
}).then(resp => {
return resolve(resp)
}).catch(err => {
return reject(err)
})
})
}
const shareAllByKey = (key: string, permissions: pond.Permission[]) => {
let url = pondURL("/shareAllFieldsByKey")
if (as) url = url + "?as=" + as
return new Promise<AxiosResponse>((resolve, reject) => {
post(url, {
key: key,
permissions: permissions.map(permission => permissionToString(permission))
}).then(resp => {
return resolve(resp)
}).catch(err => {
return reject(err)
})
})
}
return (
<FieldAPIContext.Provider
value={{
@ -100,7 +139,9 @@ export default function FieldProvider(props: PropsWithChildren<Props>) {
getField,
listFields,
removeField,
updateField
updateField,
shareAll,
shareAllByKey
}}>
{children}
</FieldAPIContext.Provider>

View file

@ -90,8 +90,7 @@ export default function InteractionProvider(props: PropsWithChildren<Props>) {
) => {
const view = otherTeam ? otherTeam : as
let url = pondURL(
"/interactions/forComponents/" +
"?componentIds=" +
"/interactions/forComponents?componentIds=" +
fullComponentLocations.toString() +
(view ? "&as=" + view : "")
)

View file

@ -135,7 +135,6 @@ export default function PermissionProvider(props: PropsWithChildren<Props>) {
let url = pondURL("/" + scope.kind + "s/" + scope.key + "/shareByKey")
if (as) url = pondURL(`/${scope.kind}s/${scope.key}/shareByKey?as=${as}`)
return new Promise<AxiosResponse>((resolve, reject) => {
post(url, {
key: key,
permissions: permissions.map(permission => permissionToString(permission))

View file

@ -119,7 +119,7 @@ export default function TeamProvider(props: PropsWithChildren<Props>) {
prefixSearch?: string,
) => {
//let asText = team ? "&as=" + team.key() : "";
const view = otherTeam ? otherTeam : as
const view = otherTeam ? otherTeam : (as.length > 0 ? as : undefined)
let url = pondURL(
"/teams" +
"?limit=" +

View file

@ -2,7 +2,7 @@ import React, { useState } from "react";
import { EventClickArg } from "@fullcalendar/core";
import { DateClickArg } from "@fullcalendar/interaction";
import { useGlobalState } from "providers";
import { useMobile, useSnackbar, useThemeType } from "hooks";
import { useMobile, useSnackbar } from "hooks";
import { useTaskAPI } from "providers";
import { useEffect, useCallback } from "react";
import TaskCalendar from "./TaskCalendar";
@ -12,7 +12,6 @@ import {
Button,
Fab,
Grid2 as Grid,
IconButton,
LinearProgress,
Theme,
Typography
@ -21,8 +20,6 @@ import { Task } from "models";
import TaskSettings from "./TaskSettings";
import { DayPicker } from "react-day-picker";
import "react-day-picker/dist/style.css";
import { CalendarToday } from "@mui/icons-material";
import NotesIcon from "@mui/icons-material/Notes";
import AddIcon from "@mui/icons-material/Add";
import TaskDrawer from "./TaskDrawer";
import moment from "moment";
@ -33,6 +30,7 @@ interface ViewProps {
label?: string;
drawerView?: boolean;
loadKeys?: string[]; //if you want to load tasks for a specific object(s)
overlayButton?: boolean;
}
interface TabPanelProps {
@ -42,17 +40,7 @@ interface TabPanelProps {
}
const useStyles = makeStyles((theme: Theme) => {
const themeType = useThemeType();
return ({
avatar: {
color: themeType === "light" ? theme.palette.common.black : theme.palette.common.white,
backgroundColor: "transparent",
width: theme.spacing(5),
height: theme.spacing(5),
border: "1px solid",
borderColor: theme.palette.divider
},
active: {
color: theme.palette.getContrastText(theme.palette.secondary.main),
backgroundColor: theme.palette.secondary.main,
@ -64,6 +52,22 @@ const useStyles = makeStyles((theme: Theme) => {
}
},
fab: {
zIndex: 20,
background: theme.palette.primary.main,
"&:hover": {
background: theme.palette.primary.main
},
"&:focus": {
background: theme.palette.primary.main
},
position: "absolute",
bottom: theme.spacing(8), //for mobile navigator
right: theme.spacing(2),
[theme.breakpoints.up("sm")]: {
bottom: theme.spacing(2)
}
},
overlayFab: {
zIndex: 20,
background: theme.palette.primary.main,
"&:hover": {
@ -83,19 +87,19 @@ const useStyles = makeStyles((theme: Theme) => {
});
export default function TaskViewer(props: ViewProps) {
const { objectKey, label, drawerView, loadKeys } = props;
const [{ user }] = useGlobalState();
const [{ as }] = useGlobalState();
const { objectKey, label, drawerView, loadKeys, overlayButton } = props;
const [{ user, as }] = useGlobalState();
//const [{ as }] = useGlobalState();
const taskAPI = useTaskAPI();
const { openSnack } = useSnackbar();
const [tasks, setTasks] = useState<Map<string, Task>>(new Map<string, Task>());
const [viewTask, setViewTask] = useState<Task>(Task.create());
const [tasks, setTasks] = useState<Map<string, Task>>(new Map<string, Task>([]));
const [viewTask, setViewTask] = useState<Task>();
const [newTaskDialog, setNewTaskDialog] = useState(false);
const [editTask, setEditTask] = useState<Task>();
const [loaded, setLoaded] = useState(false);
const [currentDate, setCurrentDate] = useState(new Date());
const isMobile = useMobile();
const [value, setValue] = React.useState(0);
const [value, setValue] = useState(0);
const classes = useStyles();
const [expand, setExpand] = useState(false);
const [hideCal, setHideCal] = useState(false);
@ -138,6 +142,7 @@ export default function TaskViewer(props: ViewProps) {
openDialog();
};
const loadMultitask = useCallback(() => {
if (!loadKeys) return;
if (loadKeys.length > 0) {
@ -146,11 +151,13 @@ export default function TaskViewer(props: ViewProps) {
taskAPI
.getMultiTasks(loadKeys, as)
.then(resp => {
resp.data.tasks.forEach(task => {
if (task.settings) {
temp.set(task.key, Task.any(task));
}
});
if(resp.data.tasks){
resp.data.tasks.forEach(task => {
if (task.settings) {
temp.set(task.key, Task.any(task));
}
});
}
setTasks(temp);
setLoaded(true);
})
@ -167,15 +174,18 @@ export default function TaskViewer(props: ViewProps) {
taskAPI
.listTasks(50, 0, "asc", "start", undefined, undefined, as, prevMonth, nextMonth)
.then(resp => {
resp.data.tasks.forEach(task => {
if (task.settings) {
temp.set(task.key, Task.any(task));
}
});
if(resp.data.tasks){
resp.data.tasks.forEach(task => {
if (task.settings) {
temp.set(task.key, Task.any(task));
}
});
}
setTasks(temp);
setLoaded(true);
})
.catch(err => {
console.log(err)
openSnack("Failed to load tasks");
});
}, [taskAPI, as, user, openSnack, nextMonth, prevMonth]);
@ -216,33 +226,6 @@ export default function TaskViewer(props: ViewProps) {
});
};
const tabIcons = () => {
return (
<Box display="flex" justifyContent="left" alignItems="center" zIndex={5}>
<IconButton
className={value === 0 ? classes.active : classes.avatar}
component="span"
style={{
marginBottom: "0.75rem",
marginRight: "0.5rem"
}}
onClick={() => setValue(0)}>
<CalendarToday />
</IconButton>
<IconButton
className={value === 1 ? classes.active : classes.avatar}
component="span"
style={{
marginBottom: "0.75rem",
marginRight: "0.5rem"
}}
onClick={() => setValue(1)}>
<NotesIcon />
</IconButton>
</Box>
);
};
function TabPanelMine(props: TabPanelProps) {
const { children, value, index, ...other } = props;
@ -266,15 +249,7 @@ export default function TaskViewer(props: ViewProps) {
return (
<React.Fragment>
{isMobile || drawerView ? (
<Box>
<Fab
onClick={openDialog}
aria-label="Create Task"
className={classes.fab}
size={isMobile ? "medium" : "large"}>
<AddIcon />
</Fab>
{!drawerView && tabIcons()}
<Box height={"100%"} position={"relative"}>
<TabPanelMine value={value} index={0}>
<Box paddingBottom={2}>
<TaskCalendar
@ -347,6 +322,13 @@ export default function TaskViewer(props: ViewProps) {
openTask={(taskId: string) => openSelectedTask(taskId)}
/>
</TabPanelMine>
<Fab
onClick={openDialog}
aria-label="Create Task"
className={overlayButton ? classes.overlayFab : classes.fab}
size={isMobile ? "medium" : "large"}>
<AddIcon />
</Fab>
</Box>
) : (
<Grid container direction="row" alignContent="center" alignItems="center">
@ -436,7 +418,7 @@ export default function TaskViewer(props: ViewProps) {
};
return (
<Box>
<Box height={"100%"}>
{!loaded ? <LinearProgress style={{ marginTop: 20 }} /> : viewer()}
<TaskSettings
open={newTaskDialog}
@ -455,15 +437,15 @@ export default function TaskViewer(props: ViewProps) {
setNewTaskDialog(false);
}}
/>
{
<TaskDrawer
task={viewTask}
open={openDrawer}
closeCallback={() => setOpenDrawer(false)}
completeTask={markComplete}
deleteTask={deleteTask}
editTask={setTaskToEdit}
/>
{viewTask &&
<TaskDrawer
task={viewTask}
open={openDrawer}
closeCallback={() => setOpenDrawer(false)}
completeTask={markComplete}
deleteTask={deleteTask}
editTask={setTaskToEdit}
/>
}
</Box>
);

View file

@ -266,7 +266,7 @@ export default function UserMenu() {
</MenuItem>
)}
<Divider />
{hasAdmin && (
{(hasAdmin || user.allowedTo("access-object")) && (
<Tooltip title="Access Object">
<MenuItem onClick={() => openAccessObject()} aria-label="Access Object" dense>
<ListItemIcon>
@ -276,7 +276,7 @@ export default function UserMenu() {
</MenuItem>
</Tooltip>
)}
<Divider />
{(hasAdmin || user.allowedTo("access-object")) && <Divider /> }
<MenuItem onClick={handleLogout} aria-label="Sign Out" dense>
<ListItemIcon>
<ExitToApp className={classes.red} />

View file

@ -17,6 +17,7 @@ import FeatureCard from "./FeatureCard";
//import { ImgIcon } from "common/ImgIcon";
//images to import for the image in the card for the feature make sure to have a 2:1 aspect ratio to keep the cards symmetrical
import FeatureImageTest from "assets/marketplaceImages/VisualFarmMPImage.png";
import LibraCartImage from "assets/marketplaceImages/Libra_Cart_temp.jpg";
// export interface FAQ {
// question: string;
@ -62,14 +63,14 @@ const agFeatureList: ProductDetails[] = [
// bulletPoints: ["bullet one", "bullet two"],
// questions: [],
},
// {
// featureName: "libra-cart",
// featureLogo: <LibraCartIcon />,
// featureCost: 0,
// cardImage: FeatureImageTest,
// featureTitle: "Libra Cart",
// longDescription: "Integrate with Libra Cart to bring your data into our platform"
// }
{
featureName: "libra-cart",
featureLogo: <LibraCartIcon />,
featureCost: 0,
cardImage: LibraCartImage,
featureTitle: "Libra Cart",
longDescription: "Integrate with Libra Cart to bring your data into our platform"
}
];
// const constructionFeatureList: ProductDetails[] = []

View file

@ -12,6 +12,8 @@ export default defineConfig({
VitePWA({
registerType: 'autoUpdate', // Automatically updates the service worker when a new version is available
workbox: {
skipWaiting: true, //makes the new worker call skip waiting
clientsClaim: true, //makes it claim control of clients as soon as it activates
globPatterns: ['**/*.{js,css,html,png,jpg,svg}'], // Cache all common asset types
maximumFileSizeToCacheInBytes: 20 * 1024 * 1024, // 20 MiB
cleanupOutdatedCaches: true,
@ -32,6 +34,14 @@ export default defineConfig({
cacheName: 'assets-cache',
},
},
{
//this forces all api calls to go through the network
urlPattern: ({ url }) => url.pathname.startsWith('/v1/'),
handler: 'NetworkOnly',
options: {
cacheName: 'api-cache',
},
},
]
},
}),