diff --git a/package-lock.json b/package-lock.json index dda1c90..afd8bad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,7 @@ "mui-tel-input": "^7.0.0", "notistack": "^3.0.1", "openweathermap-ts": "^1.2.10", - "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#master", + "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", @@ -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#d0ac088df3822c10c0497f49173d1679b4e107b0", + "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#c65c13a8eb98331c6648e401219debf8c9adc657", "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", diff --git a/package.json b/package.json index ee0e9b7..fc7a1ac 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "mui-tel-input": "^7.0.0", "notistack": "^3.0.1", "openweathermap-ts": "^1.2.10", - "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#master", + "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", diff --git a/src/bin/BinCardV2.tsx b/src/bin/BinCardV2.tsx index 4bfcb4f..a4afc33 100644 --- a/src/bin/BinCardV2.tsx +++ b/src/bin/BinCardV2.tsx @@ -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")} ); }; diff --git a/src/bin/BinConditioningCard.tsx b/src/bin/BinConditioningCard.tsx index eeceb37..cf3b3aa 100644 --- a/src/bin/BinConditioningCard.tsx +++ b/src/bin/BinConditioningCard.tsx @@ -33,7 +33,7 @@ const useStyles = makeStyles((theme: Theme) => { interface Props { interactions: Interaction[]; interactionDevices: Map; - mode: pond.BinMode.BIN_MODE_DRYING | pond.BinMode.BIN_MODE_HYDRATING; + mode: pond.BinMode; plenums?: Plenum[]; ambients?: Ambient[]; heaters?: Controller[]; @@ -94,10 +94,21 @@ export default function BinConditioningCard(props: Props) { return conditioningInteractions; }; + const modeDisplay = () => { + switch(mode){ + case pond.BinMode.BIN_MODE_DRYING: + return "Drying" + case pond.BinMode.BIN_MODE_HYDRATING: + return "Hydrating" + case pond.BinMode.BIN_MODE_COOLDOWN: + return "Cooldown" + } + } + return ( - {mode === pond.BinMode.BIN_MODE_DRYING ? "Drying" : "Hydrating"} Conditions + {modeDisplay()} Conditions {conditionsDisplay()} diff --git a/src/bin/BinConditioningInteraction.tsx b/src/bin/BinConditioningInteraction.tsx index 7f5ec13..7fe8bea 100644 --- a/src/bin/BinConditioningInteraction.tsx +++ b/src/bin/BinConditioningInteraction.tsx @@ -47,8 +47,6 @@ const useStyles = makeStyles((theme: Theme) => { sliderThumb: { height: 15, width: 15, - marginTop: -6, - marginLeft: -7.5, backgroundColor: "yellow" }, sliderTrack: { @@ -60,10 +58,10 @@ const useStyles = makeStyles((theme: Theme) => { backgroundColor: "white" }, sliderValLabel: { - left: "calc(-50%)", - top: 22, + left: -20, + top: 40, + background: "transparent", "& *": { - background: "transparent", color: "#fff" } }, diff --git a/src/bin/BinSVGV2.tsx b/src/bin/BinSVGV2.tsx index c3c1a40..f3c3454 100644 --- a/src/bin/BinSVGV2.tsx +++ b/src/bin/BinSVGV2.tsx @@ -507,9 +507,9 @@ export default function BinSVGV2(props: Props) { //if the cables have the same number of nodes if (b.temperatures.length === a.temperatures.length) { //sort by temp only last and any type that has humidity first - if ((avg(a.humidities) === 0 || a.humidities.length === 0) && (avg(b.humidities) !== 0 || b.humidities.length > 0)) { + if ((avg(a.humidities) === 0) && (avg(b.humidities) !== 0)) { return 1; - } else if ((avg(b.humidities) === 0 || b.humidities.length === 0) && (avg(a.humidities) !== 0 || a.humidities.length > 0)) { + } else if ((avg(b.humidities) === 0) && (avg(a.humidities) !== 0)) { return -1; } else { diff --git a/src/bin/BinStorageConditions.tsx b/src/bin/BinStorageConditions.tsx index 0f4daac..fa0e957 100644 --- a/src/bin/BinStorageConditions.tsx +++ b/src/bin/BinStorageConditions.tsx @@ -21,7 +21,7 @@ import { Bin, Component } from "models"; import { GrainCable } from "models/GrainCable"; import { UnitMeasurement } from "models/UnitMeasurement"; import Co2Icon from "products/CommonIcons/co2Icon"; -import { pond } from "protobuf-ts/pond"; +import { pond, quack } from "protobuf-ts/pond"; import { useBinAPI, useGlobalState, useSnackbar } from "providers"; import React, { useEffect, useState } from "react"; import { avg, getTemperatureUnit } from "utils"; @@ -200,33 +200,32 @@ export default function BinStorageConditions(props: Props) { // } }); - let nodeEMCs: number[] = []; - - filteredNodes.moistures.forEach((emc, i) => { + if(cable.subType() !== quack.GrainCableSubtype.GRAIN_CABLE_SUBTYPE_OPI_TEMP){ + let nodeEMCs: number[] = []; + filteredNodes.moistures.forEach((emc, i) => { if (bin.settings.inventory?.targetMoisture) { - // if (i < cable.topNode) { - nodeEMCs.push(emc); + nodeEMCs.push(emc); emcCounts.total++; if ( emc > bin.settings.inventory.targetMoisture + - bin.settings.inventory.moistureTargetDeviation + bin.settings.inventory.moistureTargetDeviation ) { emcCounts.above++; } else if ( emc < bin.settings.inventory.targetMoisture - - bin.settings.inventory.moistureTargetDeviation + bin.settings.inventory.moistureTargetDeviation ) { emcCounts.below++; } else { emcCounts.onTarget++; } - // } - } - }); + } + }); + cableEMCAvgs.push(avg(nodeEMCs)); + } cableTempAvgs.push(avg(nodeTemps)); - cableEMCAvgs.push(avg(nodeEMCs)); } }); if (cableTempAvgs.length > 0) { diff --git a/src/bin/BinsList.tsx b/src/bin/BinsList.tsx index 71a9240..483df50 100644 --- a/src/bin/BinsList.tsx +++ b/src/bin/BinsList.tsx @@ -124,7 +124,7 @@ export default function BinsList(props: Props) { xs: 6, sm: 4, md: 3, - lg: 2, + lg: 1.5, }} key={i} className={classes.gridListTile} diff --git a/src/common/DisplayDrawer.tsx b/src/common/DisplayDrawer.tsx index 8e31818..8497d0b 100644 --- a/src/common/DisplayDrawer.tsx +++ b/src/common/DisplayDrawer.tsx @@ -92,7 +92,7 @@ export default function DisplayDrawer(props: Props) { classes={{ paper: classes.drawerPaper }}> + style={{ width: isMobile ? "100%" : width, height: isMobile ? drawerHeight + "vh" : 0}}> diff --git a/src/common/StatusDust.tsx b/src/common/StatusDust.tsx index 2333318..a7e8e3a 100644 --- a/src/common/StatusDust.tsx +++ b/src/common/StatusDust.tsx @@ -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 ( diff --git a/src/common/StatusGas.tsx b/src/common/StatusGas.tsx index 7b40e60..dcf7ed3 100644 --- a/src/common/StatusGas.tsx +++ b/src/common/StatusGas.tsx @@ -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 ( diff --git a/src/common/StatusPlenum.tsx b/src/common/StatusPlenum.tsx index 9cb7489..fe2b00d 100644 --- a/src/common/StatusPlenum.tsx +++ b/src/common/StatusPlenum.tsx @@ -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 ( diff --git a/src/common/StatusSen5x.tsx b/src/common/StatusSen5x.tsx index 288eb10..fad0ecb 100644 --- a/src/common/StatusSen5x.tsx +++ b/src/common/StatusSen5x.tsx @@ -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 ( diff --git a/src/component/ComponentCard.tsx b/src/component/ComponentCard.tsx index bf83696..6d430d5 100644 --- a/src/component/ComponentCard.tsx +++ b/src/component/ComponentCard.tsx @@ -285,7 +285,6 @@ export default function ComponentCard(props: Props) { // ) : ( diff --git a/src/device/UpgradeDevice.tsx b/src/device/UpgradeDevice.tsx index 858f0a2..6b8c7d2 100644 --- a/src/device/UpgradeDevice.tsx +++ b/src/device/UpgradeDevice.tsx @@ -270,7 +270,7 @@ export default function UpgradeDevice(props: Props) { - Upgrading from {currentFirmware()} to {latestFirmware()} + Upgrading {device.platformName()} from {currentFirmware()} to {latestFirmware()}

It is recommended that the device be plugged in during the upgrade as it will take diff --git a/src/field/FieldList.tsx b/src/field/FieldList.tsx index 6243ce3..b22e23b 100644 --- a/src/field/FieldList.tsx +++ b/src/field/FieldList.tsx @@ -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 ( - - ); -} - 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([]); - 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([]); + const [total, setTotal] = useState(0) + const [rowsPerPage, setRowsPerPage] = useState(5) + const [page, setPage] = useState(0) + const [selectedField, setSelectedField] = useState() + 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) => ( - - {field.fieldName()} - {field.landLoc()} - - {field.crop() === pond.Grain.GRAIN_CUSTOM - ? field.customType() - : GrainDescriber(field.crop()).name} - - {field.calculateAcres()} - - {field.permissions.includes(pond.Permission.PERMISSION_WRITE) && ( - - )} - - - )); + const fieldActions = (field: Field) => { + return ( + + { + setSelectedField(field) + setOpenFieldSettings(true) + }}> + {}}/> + {isMobile && {goTo(field)}}>} + + ) + } + +const columns: Column[] = [ + { + title: "View", + render: row => { + return ( + + + + ) + } + }, + { + title: "Field Name", + render: row => { + return {row.name()} + } + }, + { + title: "Current Crop", + render: row => { + return {GrainDescriber(row.crop()).name} + } + }, + { + title: "Acres", + render: row => { + return {row.acres()} + } + }, + { + title: "Actions", + hidden: tabValue > 0, + render: row => fieldActions(row) + } +] + + const fieldTable = (fields: Field[]) => { + return ( + + 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 ( + + }> + + + + {row.name()} + + + + {fieldActions(row)} + + + + + + + + + + + + + {row.grainName()} + {row.acres()} Acres + + + + + + + ) + }} + /> + ) + } return ( + - - {/* {!isMobile && } - {!isMobile && } */} + + + - - - - - - Field Name - Land Location - Main Crop Type - Acres ({calcTotalAcres()} Total) - Create New Plan - - - {listFieldsInfo} -
-
-
- {/* need to re-factor the harvest table file before restoring these functions */} - {/* - - - - - */} - setOpenHarvestSettings(false)} field={fieldForPlan} + /> */} + { + //will need to re-load the fields after one is deleted + loadAdaptiveFields() + }} + open={openFieldSettings} + onClose={() => { + setOpenFieldSettings(false); + }} /> + {setShareOpen(false)}}/>
); } \ No newline at end of file diff --git a/src/field/FieldSettings.tsx b/src/field/FieldSettings.tsx index ca865a0..33f0c5d 100644 --- a/src/field/FieldSettings.tsx +++ b/src/field/FieldSettings.tsx @@ -152,9 +152,7 @@ export default function FieldSettings(props: Props) { const fieldInformation = () => { return ( - - - Field Information + setFeatColor(color)} /> - + + ); + }; + + return ( + + Field Information + {fieldInformation()} @@ -282,13 +287,6 @@ export default function FieldSettings(props: Props) { -
- ); - }; - - return ( - - {fieldInformation()} ); } diff --git a/src/field/Fieldminimap.tsx b/src/field/Fieldminimap.tsx new file mode 100644 index 0000000..d657ae5 --- /dev/null +++ b/src/field/Fieldminimap.tsx @@ -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(); + + 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 ? + ( + + + + ) + : + () +} \ No newline at end of file diff --git a/src/field/ShareAllFields.tsx b/src/field/ShareAllFields.tsx new file mode 100644 index 0000000..587cddd --- /dev/null +++ b/src/field/ShareAllFields.tsx @@ -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("") + const [user, setUser] = useState("") + const [sharedPermissions, setSharedPermissions] = useState([ + 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 ( + + + User + + { + setTeamShare(checked) + }} + name="storage" + /> + + Team + + {teamShare ? + + {setTeam(teamKey)}}/> + : + + { + setUser(e.target.value) + }} + /> + } + + ) + // 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 ( + + + Permissions + + {changePermissions(checked, pond.Permission.PERMISSION_READ)}} + value={pond.Permission.PERMISSION_READ as pond.Permission} + /> + } + label="View" + labelPlacement="end" + /> + {changePermissions(checked, pond.Permission.PERMISSION_WRITE)}} + value={pond.Permission.PERMISSION_WRITE as pond.Permission} + /> + } + label="Edit" + labelPlacement="end" + /> + {changePermissions(checked, pond.Permission.PERMISSION_SHARE)}} + value={pond.Permission.PERMISSION_SHARE as pond.Permission} + /> + } + label="Share" + labelPlacement="end" + /> + {changePermissions(checked, pond.Permission.PERMISSION_USERS)}} + value={pond.Permission.PERMISSION_USERS as pond.Permission} + /> + } + label="Users" + labelPlacement="end" + /> + {changePermissions(checked, pond.Permission.PERMISSION_FILE_MANAGEMENT)}} + value={pond.Permission.PERMISSION_FILE_MANAGEMENT as pond.Permission} + /> + } + label="Files" + labelPlacement="end" + /> + {/* billing */} + {changePermissions(checked, pond.Permission.PERMISSION_BILLING)}} + value={pond.Permission.PERMISSION_BILLING as pond.Permission} + /> + } + label="Billing" + labelPlacement="end" + /> + + + + ) + } + + const actions = () => { + //the buttons to shaare or cancel + return ( + + + + + ) + } + + return ( + + Share All Fields + + + {target()} + {permissions()} + + + + {actions()} + + + ) +} \ No newline at end of file diff --git a/src/gate/GateDevice.tsx b/src/gate/GateDevice.tsx index 955aa50..cbf87e6 100644 --- a/src/gate/GateDevice.tsx +++ b/src/gate/GateDevice.tsx @@ -338,14 +338,12 @@ export default function GateDevice(props: Props) { return ( - {/* - setDetail("analytics")} - value={"analytics"} - aria-label="Analysis Graphs"> - Analysis - - setDetail("sensors")} - value={"sensors"} - aria-label="Sensor Graphs"> - Sensors - - */} {flowEvents.length > 0 ? ( - + - + Total Events: @@ -271,7 +271,7 @@ export default function GateFlowGraph(props: Props) { - Total Time: + Time: {moment.duration(totalTimeS, "s").humanize()} @@ -280,9 +280,9 @@ export default function GateFlowGraph(props: Props) { - + - + Events Inside: @@ -293,7 +293,7 @@ export default function GateFlowGraph(props: Props) { - Event Time: + Time: {moment.duration(timeSInside, "s").humanize()} @@ -302,9 +302,9 @@ export default function GateFlowGraph(props: Props) { - + - + Events Outside: @@ -315,7 +315,7 @@ export default function GateFlowGraph(props: Props) { - Event Time: + Time: {moment.duration(timeSOutside, "s").humanize()} @@ -324,15 +324,15 @@ export default function GateFlowGraph(props: Props) { - + - + Performance: - {(eventsInside / totalEvents) * 100}% + {((eventsInside / totalEvents) * 100).toFixed(2)}% @@ -367,7 +367,7 @@ export default function GateFlowGraph(props: Props) { {runtime && ( - + Approximate Runtime: @@ -379,7 +379,7 @@ export default function GateFlowGraph(props: Props) { - + Cost to run PCA: @@ -394,7 +394,7 @@ export default function GateFlowGraph(props: Props) { - + Cost to run APU: @@ -409,7 +409,7 @@ export default function GateFlowGraph(props: Props) { - + Total Cost: diff --git a/src/gate/GateSVG.tsx b/src/gate/GateSVG.tsx index 3fc3422..20c46d0 100644 --- a/src/gate/GateSVG.tsx +++ b/src/gate/GateSVG.tsx @@ -249,9 +249,9 @@ export default function GateSVG(props: Props) { {convertFinalTemp(finalTemp).toFixed(2)}° - {user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS - ? "C" - : "F"} + {user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT + ? "°F" + : "°C"} ); @@ -260,9 +260,9 @@ export default function GateSVG(props: Props) { Ambient: {ambientTemp}° - {user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS - ? "C" - : "F"} + {user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT + ? "°F" + : "°C"} ); @@ -271,13 +271,13 @@ export default function GateSVG(props: Props) { 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"} ); diff --git a/src/harvestPlan/DuplicateHarvestPlan.tsx b/src/harvestPlan/DuplicateHarvestPlan.tsx index 5f083af..a3de595 100644 --- a/src/harvestPlan/DuplicateHarvestPlan.tsx +++ b/src/harvestPlan/DuplicateHarvestPlan.tsx @@ -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([]) + 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) => ( {option.fieldName()} @@ -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 diff --git a/src/harvestPlan/HarvestPlanActions.tsx b/src/harvestPlan/HarvestPlanActions.tsx index 441446a..f29c1e4 100644 --- a/src/harvestPlan/HarvestPlanActions.tsx +++ b/src/harvestPlan/HarvestPlanActions.tsx @@ -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 or Add Activity + Activities )} void; } @@ -175,7 +175,7 @@ export default function HarvestPlanDisplay(props: Props) { - Crop Plan{plan.key() !== "" ? " - " + plan.settings.title : " - None Found"} + {plan.key() !== "" ? plan.settings.title : "None Found"} {!isMobile && permissions.includes(pond.Permission.PERMISSION_WRITE) && ( ({ + 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([]) + 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[] = [ + //these columns would be the ones on by default + //field - hideable + { + title: "Field", + hidden: field !== undefined, + render: (row) => {row.field()} + }, + //title + { + title: "Plan", + render: (row) => {row.settings.title} + }, + //year + { + title: "Year", + render: (row) => {row.harvestYear()} + }, + //grain - crop_type + { + title: "Grain", + render: (row) => {GrainDescriber(row.cropType()).name} + }, + //variant - grain_type + { + title: "Variant", + render: (row) => {row.grainType()} + }, + //yield target + { + title: "Yield Target (bu)", + render: (row) => {row.yieldTarget()} + }, + //actual yield + { + title: "Actual Yield (bu)", + render: (row) => {row.actualYield()} + }, + //bushel price + { + title: "Bushel Price", + render: (row) => {row.bushelPrice()} + }, + //material cost + { + title: "Material Cost", + render: (row) => {row.totalMaterialCost()} + }, + //equipment cost + { + title: "Equipment Cost", + render: (row) => {row.totalEquipmentCost()} + } + //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 ( + + 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 ( + + }> + + {row.settings.title} - {row.harvestYear()} + + + + {/* grain */} + + Grain: + {GrainDescriber(row.cropType()).name} + + {/* variant */} + + Variant: + {row.grainType()} + + {/* yield target */} + {/* + Yield Target: + {row.yieldTarget()}bu + */} + {/* actual yield */} + + Actual Yield: + {row.actualYield()}bu + + {/* bushel price */} + + Bushel Price: + ${row.bushelPrice()} + + {/* material cost */} + + Material Cost: + ${row.totalMaterialCost() * field.acres()} + + {/* equipment cost */} + + Equipment Cost: + ${row.totalEquipmentCost() * field.acres()} + + {/* 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 */} + + Total Cost: + ${(row.totalEquipmentCost() + row.totalMaterialCost()) * field.acres()} + + + + ) + }} + /> + ) +} \ No newline at end of file diff --git a/src/integrations/LibraCart/LibraCartAccess.tsx b/src/integrations/LibraCart/LibraCartAccess.tsx index cbb5caa..0bc0c57 100644 --- a/src/integrations/LibraCart/LibraCartAccess.tsx +++ b/src/integrations/LibraCart/LibraCartAccess.tsx @@ -210,7 +210,7 @@ export default function LibraCartAccess() { - 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. diff --git a/src/maps/mapDrawers/FieldDrawer.tsx b/src/maps/mapDrawers/FieldDrawer.tsx index c4029aa..14f0da7 100644 --- a/src/maps/mapDrawers/FieldDrawer.tsx +++ b/src/maps/mapDrawers/FieldDrawer.tsx @@ -273,7 +273,7 @@ export default function FieldDrawer(props: Props) { - + ); diff --git a/src/maps/mapLayers/geoMapLayer.tsx b/src/maps/mapLayers/geoMapLayer.tsx index 5b90854..1923b64 100644 --- a/src/maps/mapLayers/geoMapLayer.tsx +++ b/src/maps/mapLayers/geoMapLayer.tsx @@ -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) { /> )} + {measurementCollection && + } ); } diff --git a/src/models/Device.ts b/src/models/Device.ts index b91cdd1..0aec35a 100644 --- a/src/models/Device.ts +++ b/src/models/Device.ts @@ -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) { diff --git a/src/models/Field.ts b/src/models/Field.ts index 25b3263..9ccdb4e 100644 --- a/src/models/Field.ts +++ b/src/models/Field.ts @@ -5,18 +5,21 @@ 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(); public status: pond.FieldStatus = pond.FieldStatus.create(); - public permissions: pond.Permission[] = []; + public permissions: pond.Permission[] = []; 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) { diff --git a/src/navigation/Router.tsx b/src/navigation/Router.tsx index afe12c5..c54c291 100644 --- a/src/navigation/Router.tsx +++ b/src/navigation/Router.tsx @@ -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() { } /> } /> } /> + } /> ) } @@ -284,6 +286,21 @@ export default function Router() { ); }; + const FieldsRoute = () => { + return ( + + } + /> + } + /> + + ); + } + if (isLoading) return null; // if (!isAuthenticated) { // loginWithRedirect() @@ -325,7 +342,6 @@ export default function Router() { {user.hasFeature("admin") && } /> } - } /> } /> {user.hasFeature("admin") && ( } /> diff --git a/src/pages/Bin.tsx b/src/pages/Bin.tsx index fcc1117..082a2c9 100644 --- a/src/pages/Bin.tsx +++ b/src/pages/Bin.tsx @@ -810,7 +810,8 @@ export default function Bin(props: Props) { {(bin.settings.mode === pond.BinMode.BIN_MODE_DRYING || - bin.settings.mode === pond.BinMode.BIN_MODE_HYDRATING) && ( + bin.settings.mode === pond.BinMode.BIN_MODE_HYDRATING || + bin.settings.mode === pond.BinMode.BIN_MODE_COOLDOWN) && ( Bin.any(b)); b.forEach(bin => { if (bin.empty()) { - console.log(bin.name()) empty.push(bin); } else if (bin.storage() === pond.BinStorage.BIN_STORAGE_FERTILIZER) { fert.push(bin); diff --git a/src/pages/Devices.tsx b/src/pages/Devices.tsx index 673f097..5681d1a 100644 --- a/src/pages/Devices.tsx +++ b/src/pages/Devices.tsx @@ -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)) diff --git a/src/pages/Field.tsx b/src/pages/Field.tsx new file mode 100644 index 0000000..19472ae --- /dev/null +++ b/src/pages/Field.tsx @@ -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 ( + + ); +} + +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(state?.field ? Field.create(state.field) : Field.create()) + const [permissions, setPermissions] = useState(state?.permissions ? state.permissions : []) + const isMobile = useMobile() + const [openFieldSettings, setOpenFieldSettings] = useState(false) + const [planLoading, setPlanLoading] = useState(false) + const [hPlan, setHPlan] = useState(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 ( + + + + {field.name()} + + + + { + setOpenFieldSettings(true) + }}> + {}}/> + + + ) + } + + const map = () => { + return ( + + + + ) + } + + const weather = () => { + return () + } + + const harvestPlans = () => { + return ( + { + if (updatedPlan) { + setHPlan(updatedPlan); + } + }} + />) + } + + const tasks = () => { + let taskLoadKeys: string[] = []; + if (!planLoading) { + field.key() !== "" && taskLoadKeys.push(field.key()); + //hPlan.key() !== "" && taskLoadKeys.push(hPlan.key()); + } + return () + } + + const desktopView = () => { + return ( + + + + + + {map()} + + + + + {weather()} + + + + + {tasks()} + + + + + + + Harvest Overview + + + + + {harvestPlans()} + + + + + + + + + + + ) + } + + const mobileView = () => { + return ( + + { + setTabVal(val) + }} + TabIndicatorProps={{ style: { background: "rgba(255,255,0,255)" } }}> + + + + + + + + + {map()} + + + + + {weather()} + + + + + + + {tasks()} + + + + + + + {harvestPlans()} + + + + + + + + + ) + } + + const fieldSettings = () => { + return ( + { + setOpenFieldSettings(false) + }} + selectedField={field} + updateFields={() => { + loadField() + }} + removeField={()=>{ + goTo() + }} + /> + ) + } + + return ( + + + {fieldActions()} + {isMobile ? mobileView() : desktopView()} + + {fieldSettings()} + + ) +} \ No newline at end of file diff --git a/src/pages/Heaters.tsx b/src/pages/Heaters.tsx index d2d19ba..226972c 100644 --- a/src/pages/Heaters.tsx +++ b/src/pages/Heaters.tsx @@ -39,6 +39,7 @@ 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 ?? []); }); diff --git a/src/pages/Tasks.tsx b/src/pages/Tasks.tsx index 85b97e1..2709b33 100644 --- a/src/pages/Tasks.tsx +++ b/src/pages/Tasks.tsx @@ -6,7 +6,7 @@ export default function Tasks() { return ( - + ); diff --git a/src/providers/pond/fieldAPI.tsx b/src/providers/pond/fieldAPI.tsx index 37be160..be980eb 100644 --- a/src/providers/pond/fieldAPI.tsx +++ b/src/providers/pond/fieldAPI.tsx @@ -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; - getField: (fieldId: string, otherTeam?: string) => Promise; + getField: (fieldId: string, otherTeam?: string) => Promise>; listFields: ( limit: number, offset: number, @@ -24,7 +25,15 @@ export interface IFieldAPIContext { field: pond.FieldSettings, asRoot?: true, otherTeam?: string - ) => Promise>; + ) => Promise>; + shareAll: ( + email: string, + permissions: pond.Permission[], + ) => Promise> + shareAllByKey: ( + key: string, + permissions: pond.Permission[] + ) => Promise> } export const FieldAPIContext = createContext({} as IFieldAPIContext); @@ -44,8 +53,8 @@ export default function FieldProvider(props: PropsWithChildren) { 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(pondURL("/field/" + fieldId + "?as=" + view)); + return get(pondURL("/field/" + fieldId)); }; const removeField = (key: string, otherTeam?: string) => { @@ -93,6 +102,36 @@ export default function FieldProvider(props: PropsWithChildren) { ); }; + const shareAll = (email: string, permissions: pond.Permission[]) => { + let url = pondURL("/shareAllFields") + if (as) url = url + "?as=" + as + return new Promise((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((resolve, reject) => { + post(url, { + key: key, + permissions: permissions.map(permission => permissionToString(permission)) + }).then(resp => { + return resolve(resp) + }).catch(err => { + return reject(err) + }) + }) + } + return ( ) { getField, listFields, removeField, - updateField + updateField, + shareAll, + shareAllByKey }}> {children} diff --git a/src/providers/pond/permissionAPI.tsx b/src/providers/pond/permissionAPI.tsx index 2311afd..91b7d6a 100644 --- a/src/providers/pond/permissionAPI.tsx +++ b/src/providers/pond/permissionAPI.tsx @@ -135,7 +135,6 @@ export default function PermissionProvider(props: PropsWithChildren) { let url = pondURL("/" + scope.kind + "s/" + scope.key + "/shareByKey") if (as) url = pondURL(`/${scope.kind}s/${scope.key}/shareByKey?as=${as}`) return new Promise((resolve, reject) => { - post(url, { key: key, permissions: permissions.map(permission => permissionToString(permission)) diff --git a/src/tasks/TaskViewer.tsx b/src/tasks/TaskViewer.tsx index d8c03db..12e82d7 100644 --- a/src/tasks/TaskViewer.tsx +++ b/src/tasks/TaskViewer.tsx @@ -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>(new Map()); - const [viewTask, setViewTask] = useState(Task.create()); + const [tasks, setTasks] = useState>(new Map([])); + const [viewTask, setViewTask] = useState(); const [newTaskDialog, setNewTaskDialog] = useState(false); const [editTask, setEditTask] = useState(); 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); @@ -181,6 +185,7 @@ export default function TaskViewer(props: ViewProps) { setLoaded(true); }) .catch(err => { + console.log(err) openSnack("Failed to load tasks"); }); }, [taskAPI, as, user, openSnack, nextMonth, prevMonth]); @@ -221,33 +226,6 @@ export default function TaskViewer(props: ViewProps) { }); }; - const tabIcons = () => { - return ( - - setValue(0)}> - - - setValue(1)}> - - - - ); - }; - function TabPanelMine(props: TabPanelProps) { const { children, value, index, ...other } = props; @@ -271,15 +249,7 @@ export default function TaskViewer(props: ViewProps) { return ( {isMobile || drawerView ? ( - - - - - {!drawerView && tabIcons()} + openSelectedTask(taskId)} /> + + + ) : ( @@ -441,7 +418,7 @@ export default function TaskViewer(props: ViewProps) { }; return ( - + {!loaded ? : viewer()} - { - setOpenDrawer(false)} - completeTask={markComplete} - deleteTask={deleteTask} - editTask={setTaskToEdit} - /> + {viewTask && + setOpenDrawer(false)} + completeTask={markComplete} + deleteTask={deleteTask} + editTask={setTaskToEdit} + /> } ); diff --git a/vite.config.ts b/vite.config.ts index d347c76..acc8cb4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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', + }, + }, ] }, }),